-
Notifications
You must be signed in to change notification settings - Fork 93
feat(query-coordinator): Add query job coordination loop. #2525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0b03851
dbc1986
d8f992e
4b2acb6
d13c6e6
0ba6fe5
1198719
6298f54
ff5bc49
37396cd
da02988
bf2ebc0
ba425f5
3cb68c5
2ce8882
344281d
0c9157c
df784e1
3b3b9c4
1df4e42
0296f0c
3f36b5a
2096e9b
cb87c6e
f8a68a6
d5c0450
ca2ebe2
9429396
323acfc
8f509ef
b67cb70
61fd27e
3bee52b
c39b086
3cee914
789f671
184d940
c479fad
3b6bc31
cfa0a97
672af8b
c592252
e5937b7
89ca7b5
4e063fb
f5860de
842c93c
2e689fe
b2fed1f
e5f0501
e0566a7
218f792
a714f6a
012c8b2
f15fa49
4891cf8
4a45d04
ce692e8
8e90645
61414eb
5e84b4f
bd8d4b4
401f988
db78dca
c2208f4
e40fcbb
e724c84
6d1dfde
ae0ccc0
4a1df55
c12a79f
d64afcb
ddbe7f5
ccdec3e
f1d676f
fa632f6
2d98db2
4737028
3ef96b1
4ce7da4
d084465
4d23f5d
7b6234b
ea5427a
53382b7
34a6e2d
5144ab6
c8bc5b5
3da99fd
ef54028
a3488cf
9b523e7
4703162
1dec117
64bc384
0122923
36574ba
6229829
72b149e
cf99d24
ff42918
619c90e
1470116
a36bcd5
2db7f80
68abfda
066d9d7
aed17a1
de2b8d8
d30ddeb
5792345
5d1a0c1
af65cb9
f3d7a8e
825ee2d
918c141
0a17071
4ea5188
28fd191
8e33c4c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ use serde::Deserialize; | |
| use crate::clp_config::AwsAuthentication; | ||
| use crate::clp_config::S3Config; | ||
| use crate::dataset::resolve_dataset_name; | ||
| use crate::types::non_empty_string::ExpectedNonEmpty; | ||
|
|
||
| /// Mirror of `clp_py_utils.clp_config.ClpConfig`. | ||
| /// | ||
|
|
@@ -280,6 +281,17 @@ pub struct ResultsCache { | |
| pub db_name: String, | ||
| } | ||
|
|
||
| impl ResultsCache { | ||
| /// Returns the MongoDB URI for the results cache database. | ||
| #[must_use] | ||
| pub fn uri(&self) -> NonEmptyString { | ||
| NonEmptyString::from_string(format!( | ||
| "mongodb://{}:{}/{}", | ||
| self.host, self.port, self.db_name | ||
| )) | ||
|
Comment on lines
+287
to
+291
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: Repository: y-scope/clp Length of output: 37942 🏁 Script executed: Repository: y-scope/clp Length of output: 50367 🤖 get_repo_knowledge executed:
Length of output: 47732 Security Misconfiguration Reachability: Internal Make TLS configurable for remote result-cache connections.
Do not force TLS for every deployment because the defaults support local and bundled plaintext MongoDB. Add a configurable TLS option to the shared result-cache configuration and apply the resulting URI consistently to the coordinator and other consumers. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| impl Default for ResultsCache { | ||
| fn default() -> Self { | ||
| Self { | ||
|
|
@@ -381,6 +393,20 @@ impl ArchiveOutput { | |
| .to_string_lossy() | ||
| .into_owned() | ||
| } | ||
|
|
||
| /// Derives the S3 object key of an archive in a dataset. | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// The dataset's archive storage directory joined with `archive_id`, where a `None` dataset | ||
| /// resolves to `default`. | ||
| #[must_use] | ||
| pub fn dataset_archive_object_key(&self, dataset: Option<&str>, archive_id: &str) -> String { | ||
| format!( | ||
| "{}/{archive_id}", | ||
| self.dataset_archive_storage_directory(dataset) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| impl Default for ArchiveOutput { | ||
|
|
@@ -477,6 +503,37 @@ impl Default for Telemetry { | |
| } | ||
| } | ||
|
|
||
| /// Query coordinator configuration. | ||
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] | ||
| #[serde(default)] | ||
| pub struct QueryCoordinator { | ||
| pub resource_group: SpiderResourceGroup, | ||
| pub job_polling_interval_millisecs: NonZeroU64, | ||
| pub max_concurrent_jobs: NonZeroUsize, | ||
| pub result_polling: PollingBackoff, | ||
| } | ||
|
Comment on lines
+509
to
+514
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: Repository: y-scope/clp Length of output: 38465 🏁 Script executed: Repository: y-scope/clp Length of output: 50368 🏁 Script executed: Repository: y-scope/clp Length of output: 50367 🏁 Script executed: Repository: y-scope/clp Length of output: 50367 🏁 Script executed: Repository: y-scope/clp Length of output: 50367 🏁 Script executed: Repository: y-scope/clp Length of output: 50367 Expose 🤖 Prompt for AI Agents |
||
|
|
||
| impl Default for QueryCoordinator { | ||
| fn default() -> Self { | ||
| Self { | ||
| resource_group: SpiderResourceGroup { | ||
| name: NonEmptyString::new("query-coordinator".to_owned()) | ||
| .expect("default resource group name should not be empty"), | ||
| }, | ||
| job_polling_interval_millisecs: NonZeroU64::new(100) | ||
| .expect("default jobs poll delay should not be zero"), | ||
| max_concurrent_jobs: NonZeroUsize::new(1000) | ||
| .expect("default maximum number of concurrent jobs should not be zero"), | ||
| result_polling: PollingBackoff { | ||
| init_backoff_millisecs: NonZeroU64::new(100) | ||
| .expect("default result polling init backoff should not be zero"), | ||
| max_backoff_millisecs: NonZeroU64::new(1000) | ||
| .expect("default result polling max backoff should not be zero"), | ||
| }, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Compression coordinator configuration. | ||
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] | ||
| #[serde(default)] | ||
|
|
@@ -699,6 +756,38 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn dataset_archive_object_key_joins_prefix_dataset_and_id() { | ||
| use non_empty_string::NonEmptyString; | ||
|
|
||
| use crate::clp_config::AwsAuthentication; | ||
| use crate::clp_config::S3Config; | ||
| use crate::types::non_empty_string::ExpectedNonEmpty; | ||
|
|
||
| let archive_output = ArchiveOutput { | ||
| storage: ArchiveOutputStorage::S3 { | ||
| staging_directory: "var/data/staged-archives".to_owned(), | ||
| s3_config: S3Config { | ||
| bucket: NonEmptyString::from_static_str("bucket"), | ||
| region_code: None, | ||
| key_prefix: NonEmptyString::from_static_str("LIB1/"), | ||
| endpoint_url: None, | ||
| aws_authentication: AwsAuthentication::Default, | ||
| }, | ||
| }, | ||
| ..ArchiveOutput::default() | ||
| }; | ||
|
|
||
| assert_eq!( | ||
| archive_output.dataset_archive_object_key(None, "abc"), | ||
| "LIB1/default/abc" | ||
| ); | ||
| assert_eq!( | ||
| archive_output.dataset_archive_object_key(Some("mydataset"), "abc"), | ||
| "LIB1/mydataset/abc" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn deserialize_database_ignores_provided_table_prefix() { | ||
| let database_json = serde_json::json!({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| pub mod compression; | ||
| pub mod query; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| //! Protocol types exchanged with the Spider (Huntsman) tasks that run CLP query jobs. | ||
|
|
||
| use std::num::NonZeroU32; | ||
|
|
||
| use non_empty_string::NonEmptyString; | ||
| use serde::Deserialize; | ||
| use serde::Serialize; | ||
|
|
||
| /// `clp-s` options for a query job. | ||
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] | ||
| #[serde(deny_unknown_fields)] | ||
| pub struct ClpSQueryOption { | ||
| /// The query string passed positionally to `clp-s`. | ||
| pub query_string: NonEmptyString, | ||
|
|
||
| /// The per-archive result limit. When absent, the task omits `--max-num-results` and uses the | ||
| /// `clp-s` default. | ||
| pub max_num_results: Option<NonZeroU32>, | ||
|
|
||
| /// Inclusive `--tge` bound in Unix epoch milliseconds. | ||
| pub begin_timestamp_millisecs: Option<i64>, | ||
|
|
||
| /// Inclusive `--tle` bound in Unix epoch milliseconds. | ||
| pub end_timestamp_millisecs: Option<i64>, | ||
|
|
||
| /// Whether `clp-s` performs a case-insensitive search. | ||
| pub ignore_case: bool, | ||
| } | ||
|
|
||
| /// The output handler that `clp-s` writes a query task's results to. | ||
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] | ||
| #[serde(deny_unknown_fields, tag = "type")] | ||
| pub enum OutputHandle { | ||
| /// The results cache, addressed by a MongoDB URI whose path names the database. The collection | ||
| /// is the query job's ID. | ||
| #[serde(rename = "results_cache")] | ||
| ResultsCache { uri: NonEmptyString }, | ||
|
|
||
| /// A file per archive. Not yet supported by the Spider query flow. | ||
| #[serde(rename = "file")] | ||
| File, | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use std::num::NonZeroU32; | ||
|
|
||
| use non_empty_string::NonEmptyString; | ||
|
|
||
| use super::ClpSQueryOption; | ||
| use super::OutputHandle; | ||
| use crate::types::non_empty_string::ExpectedNonEmpty; | ||
|
|
||
| #[test] | ||
| fn clp_s_query_option_with_timestamp_bounds_round_trips_through_msgpack() { | ||
| let expected = ClpSQueryOption { | ||
| query_string: NonEmptyString::from_static_str("level:error"), | ||
| max_num_results: Some(NonZeroU32::new(1_000).expect("1,000 is nonzero")), | ||
| begin_timestamp_millisecs: Some(1_700_000_000_001), | ||
| end_timestamp_millisecs: Some(1_700_000_000_999), | ||
| ignore_case: true, | ||
| }; | ||
|
|
||
| let serialized = rmp_serde::to_vec(&expected).expect("query options should serialize"); | ||
| let actual: ClpSQueryOption = | ||
| rmp_serde::from_slice(&serialized).expect("query options should deserialize"); | ||
|
|
||
| assert_eq!(expected, actual); | ||
| } | ||
|
|
||
| #[test] | ||
| fn clp_s_query_option_without_timestamp_bounds_round_trips_through_msgpack() { | ||
| let expected = ClpSQueryOption { | ||
| query_string: NonEmptyString::from_static_str("*"), | ||
| max_num_results: Some(NonZeroU32::new(1).expect("1 is nonzero")), | ||
| begin_timestamp_millisecs: None, | ||
| end_timestamp_millisecs: None, | ||
| ignore_case: false, | ||
| }; | ||
|
|
||
| let serialized = rmp_serde::to_vec(&expected).expect("query options should serialize"); | ||
| let actual: ClpSQueryOption = | ||
| rmp_serde::from_slice(&serialized).expect("query options should deserialize"); | ||
|
|
||
| assert_eq!(expected, actual); | ||
| } | ||
|
|
||
| #[test] | ||
| fn clp_s_query_option_without_max_num_results_round_trips_through_msgpack() { | ||
| let expected = ClpSQueryOption { | ||
| query_string: NonEmptyString::from_static_str("*"), | ||
| max_num_results: None, | ||
| begin_timestamp_millisecs: None, | ||
| end_timestamp_millisecs: None, | ||
| ignore_case: false, | ||
| }; | ||
|
|
||
| let serialized = rmp_serde::to_vec(&expected).expect("query options should serialize"); | ||
| let actual: ClpSQueryOption = | ||
| rmp_serde::from_slice(&serialized).expect("query options should deserialize"); | ||
|
|
||
| assert_eq!(expected, actual); | ||
| } | ||
|
|
||
| #[test] | ||
| fn output_handle_results_cache_round_trips_through_msgpack() { | ||
| let expected = OutputHandle::ResultsCache { | ||
| uri: NonEmptyString::from_static_str("mongodb://results-cache:27017/clp-query-results"), | ||
| }; | ||
|
|
||
| let serialized = rmp_serde::to_vec(&expected).expect("output handle should serialize"); | ||
| let actual: OutputHandle = | ||
| rmp_serde::from_slice(&serialized).expect("output handle should deserialize"); | ||
|
|
||
| assert_eq!(expected, actual); | ||
| } | ||
|
|
||
| #[test] | ||
| fn output_handle_file_round_trips_through_msgpack() { | ||
| let expected = OutputHandle::File; | ||
|
|
||
| let serialized = rmp_serde::to_vec(&expected).expect("output handle should serialize"); | ||
| let actual: OutputHandle = | ||
| rmp_serde::from_slice(&serialized).expect("output handle should deserialize"); | ||
|
|
||
| assert_eq!(expected, actual); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Add a migration for existing
query_jobstables.CREATE TABLE IF NOT EXISTSdoes not alter an existing table. On an upgraded deployment,query_jobskeeps the old schema and lacksstatus_msg,spider_id, anddispatch_time. Every coordinator statement that references those columns then fails with an unknown-column error:coordination.rslines 250-253, 364-368, 448-457, and 525-529, plusjob_handle.rslines 244-247 and 347-350. The query coordinator cannot dispatch or finalize any job.Add an idempotent
ALTER TABLEmigration, following the existingJOB_START_TIME_STATUSpattern at lines 92-105.🐛 Proposed migration
Insert before the
QUERY_TASKS_TABLE_NAMEstatement:Import
ER_DUP_FIELDNAME(error 1060) alongside the existingER_DUP_KEYNAMEimport.Also applies to: 144-145, 148-149
🧰 Tools
🪛 OpenGrep (1.29.0)
[ERROR] 131-152: SQL query built via f-string passed to execute()/executemany(). Use parameterized queries with placeholders instead.
(coderabbit.sql-injection.python-fstring-execute)
🤖 Prompt for AI Agents