fix(workspace): preserve worker projection convergence
This commit is contained in:
@@ -798,6 +798,13 @@ CREATE TABLE worker_registry_projection_revisions (
|
|||||||
workspace_id TEXT PRIMARY KEY,
|
workspace_id TEXT PRIMARY KEY,
|
||||||
revision INTEGER NOT NULL
|
revision INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
CREATE TABLE worker_registry_projection_removals (
|
||||||
|
workspace_id TEXT NOT NULL,
|
||||||
|
runtime_id TEXT NOT NULL,
|
||||||
|
worker_id TEXT NOT NULL,
|
||||||
|
projection_revision INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, runtime_id, worker_id)
|
||||||
|
);
|
||||||
CREATE TABLE worker_registry_projection_diagnostics (
|
CREATE TABLE worker_registry_projection_diagnostics (
|
||||||
diagnostic_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
diagnostic_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ CREATE TABLE IF NOT EXISTS worker_registry_projection_revisions (
|
|||||||
workspace_id TEXT PRIMARY KEY,
|
workspace_id TEXT PRIMARY KEY,
|
||||||
revision INTEGER NOT NULL
|
revision INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS worker_registry_projection_removals (
|
||||||
|
workspace_id TEXT NOT NULL,
|
||||||
|
runtime_id TEXT NOT NULL,
|
||||||
|
worker_id TEXT NOT NULL,
|
||||||
|
projection_revision INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, runtime_id, worker_id)
|
||||||
|
);
|
||||||
CREATE TABLE IF NOT EXISTS worker_registry_projection_diagnostics (
|
CREATE TABLE IF NOT EXISTS worker_registry_projection_diagnostics (
|
||||||
diagnostic_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
diagnostic_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
@@ -5589,6 +5596,10 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
&record.worker.worker_id,
|
&record.worker.worker_id,
|
||||||
&record.created_at,
|
&record.created_at,
|
||||||
)?;
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM worker_registry_projection_removals WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3",
|
||||||
|
params![record.workspace_id, record.worker.runtime_id, record.worker.worker_id],
|
||||||
|
)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -5698,7 +5709,7 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
worker_projection_cursor(&tx, workspace_id, runtime_id)?;
|
worker_projection_cursor(&tx, workspace_id, runtime_id)?;
|
||||||
if connection_generation < current_generation
|
if connection_generation < current_generation
|
||||||
|| (connection_generation == current_generation
|
|| (connection_generation == current_generation
|
||||||
&& snapshot_revision <= current_snapshot)
|
&& snapshot_revision < current_snapshot)
|
||||||
{
|
{
|
||||||
let revision = current_worker_projection_revision(&tx, workspace_id)?;
|
let revision = current_worker_projection_revision(&tx, workspace_id)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
@@ -5903,8 +5914,26 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
) -> Result<WorkerRegistryProjectionCommit> {
|
) -> Result<WorkerRegistryProjectionCommit> {
|
||||||
self.with_conn_mut(|conn| {
|
self.with_conn_mut(|conn| {
|
||||||
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
let published_revision = tx
|
||||||
|
.query_row(
|
||||||
|
"SELECT projection_revision FROM worker_registry_projection_removals WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3",
|
||||||
|
params![workspace_id, worker.runtime_id, worker.worker_id],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)
|
||||||
|
.optional()?;
|
||||||
|
if let Some(revision) = published_revision {
|
||||||
|
tx.commit()?;
|
||||||
|
return Ok(WorkerRegistryProjectionCommit {
|
||||||
|
revision: revision.max(0) as u64,
|
||||||
|
changed_workers: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
let changed_workers = vec![worker.clone()];
|
let changed_workers = vec![worker.clone()];
|
||||||
let revision = commit_worker_projection_changes(&tx, workspace_id, &changed_workers)?;
|
let revision = commit_worker_projection_changes(&tx, workspace_id, &changed_workers)?;
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO worker_registry_projection_removals (workspace_id, runtime_id, worker_id, projection_revision) VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
params![workspace_id, worker.runtime_id, worker.worker_id, revision as i64],
|
||||||
|
)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
Ok(WorkerRegistryProjectionCommit {
|
Ok(WorkerRegistryProjectionCommit {
|
||||||
revision,
|
revision,
|
||||||
@@ -8714,11 +8743,15 @@ fn upsert_worker_observation(
|
|||||||
params![workspace_id, runtime_id, worker.worker_id.as_str()],
|
params![workspace_id, runtime_id, worker.worker_id.as_str()],
|
||||||
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?, row.get::<_, i64>(2)?, row.get::<_, i64>(3)?)),
|
|row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?, row.get::<_, i64>(2)?, row.get::<_, i64>(3)?)),
|
||||||
).optional()?;
|
).optional()?;
|
||||||
if let Some((_, _, generation, revision)) = existing.as_ref() {
|
if let Some((old_availability, _, generation, revision)) = existing.as_ref() {
|
||||||
let generation = (*generation).max(0) as u64;
|
let generation = (*generation).max(0) as u64;
|
||||||
let revision = (*revision).max(0) as u64;
|
let revision = (*revision).max(0) as u64;
|
||||||
|
let restores_unavailable = old_availability == "unavailable"
|
||||||
|
&& availability == SubscriptionWorkerAvailability::Observed;
|
||||||
if connection_generation < generation
|
if connection_generation < generation
|
||||||
|| (connection_generation == generation && worker.subject_revision <= revision)
|
|| (connection_generation == generation
|
||||||
|
&& (worker.subject_revision < revision
|
||||||
|
|| (worker.subject_revision == revision && !restores_unavailable)))
|
||||||
{
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
@@ -14946,6 +14979,31 @@ INSERT INTO worker_registry (
|
|||||||
.reconcile_worker_registry_snapshot("local-dev", "embedded", 1, 11, &[], "4")
|
.reconcile_worker_registry_snapshot("local-dev", "embedded", 1, 11, &[], "4")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(duplicate.changed_workers.is_empty());
|
assert!(duplicate.changed_workers.is_empty());
|
||||||
|
|
||||||
|
let reconnected = store
|
||||||
|
.reconcile_worker_registry_snapshot(
|
||||||
|
"local-dev",
|
||||||
|
"embedded",
|
||||||
|
1,
|
||||||
|
11,
|
||||||
|
&[observed.clone()],
|
||||||
|
"5",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(reconnected.changed_workers, vec![catalog.worker.clone()]);
|
||||||
|
let projection = store
|
||||||
|
.worker_registry_projection("local-dev", &catalog.worker)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
projection.observation.unwrap().availability,
|
||||||
|
SubscriptionWorkerAvailability::Observed,
|
||||||
|
"an equal-revision reconnect snapshot must restore availability"
|
||||||
|
);
|
||||||
|
let duplicate_reconnect = store
|
||||||
|
.reconcile_worker_registry_snapshot("local-dev", "embedded", 1, 11, &[observed], "6")
|
||||||
|
.unwrap();
|
||||||
|
assert!(duplicate_reconnect.changed_workers.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -14984,6 +15042,15 @@ INSERT INTO worker_registry (
|
|||||||
.delete_worker_registry("local-dev", &worker_ref)
|
.delete_worker_registry("local-dev", &worker_ref)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
let first_removal = store
|
||||||
|
.publish_worker_registry_removal("local-dev", &worker_ref)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(first_removal.changed_workers, vec![worker_ref.clone()]);
|
||||||
|
let duplicate_removal = store
|
||||||
|
.publish_worker_registry_removal("local-dev", &worker_ref)
|
||||||
|
.unwrap();
|
||||||
|
assert!(duplicate_removal.changed_workers.is_empty());
|
||||||
|
assert_eq!(duplicate_removal.revision, first_removal.revision);
|
||||||
let event = SubscriptionWorker {
|
let event = SubscriptionWorker {
|
||||||
worker_id: SubscriptionWorkerId::new("removed").unwrap(),
|
worker_id: SubscriptionWorkerId::new("removed").unwrap(),
|
||||||
runtime_id: Some("embedded".to_string()),
|
runtime_id: Some("embedded".to_string()),
|
||||||
|
|||||||
@@ -155,6 +155,9 @@ impl WorkerProjectionService {
|
|||||||
let commit = self
|
let commit = self
|
||||||
.store
|
.store
|
||||||
.publish_worker_registry_removal(self.workspace_id.as_str(), &worker)?;
|
.publish_worker_registry_removal(self.workspace_id.as_str(), &worker)?;
|
||||||
|
if commit.changed_workers.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let _ = self.events.send(WorkerProjectionEvent {
|
let _ = self.events.send(WorkerProjectionEvent {
|
||||||
revision: commit.revision,
|
revision: commit.revision,
|
||||||
changes: vec![WorkerProjectionChange::Removed(worker)],
|
changes: vec![WorkerProjectionChange::Removed(worker)],
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[
|
|||||||
"worker_registry_observations",
|
"worker_registry_observations",
|
||||||
"worker_registry_projection_cursors",
|
"worker_registry_projection_cursors",
|
||||||
"worker_registry_projection_diagnostics",
|
"worker_registry_projection_diagnostics",
|
||||||
|
"worker_registry_projection_removals",
|
||||||
"worker_registry_projection_revisions",
|
"worker_registry_projection_revisions",
|
||||||
"worker_removal_operations",
|
"worker_removal_operations",
|
||||||
"worker_retention_audit_events",
|
"worker_retention_audit_events",
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ async fn run_workspace_workers(
|
|||||||
};
|
};
|
||||||
let mut workers = records
|
let mut workers = records
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(project_registry_worker)
|
.filter_map(|record| project_registry_worker(&api, record))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
sort_workers(&mut workers);
|
sort_workers(&mut workers);
|
||||||
if send_frame(
|
if send_frame(
|
||||||
@@ -342,7 +342,7 @@ async fn run_workspace_workers(
|
|||||||
for change in event.changes {
|
for change in event.changes {
|
||||||
let payload = match change {
|
let payload = match change {
|
||||||
crate::worker_projection::WorkerProjectionChange::Upsert(record) => {
|
crate::worker_projection::WorkerProjectionChange::Upsert(record) => {
|
||||||
let Some(mut worker) = project_registry_worker(record) else {
|
let Some(mut worker) = project_registry_worker(&api, record) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
worker.subject_revision = event.revision;
|
worker.subject_revision = event.revision;
|
||||||
@@ -388,6 +388,7 @@ async fn run_workspace_workers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn project_registry_worker(
|
fn project_registry_worker(
|
||||||
|
api: &WorkspaceApi,
|
||||||
record: crate::store::WorkerRegistryProjectionRecord,
|
record: crate::store::WorkerRegistryProjectionRecord,
|
||||||
) -> Option<SubscriptionWorker> {
|
) -> Option<SubscriptionWorker> {
|
||||||
let runtime_id = record.registry.worker.runtime_id.clone();
|
let runtime_id = record.registry.worker.runtime_id.clone();
|
||||||
@@ -422,9 +423,27 @@ fn project_registry_worker(
|
|||||||
worker.workspace_id = Some(record.registry.workspace_id);
|
worker.workspace_id = Some(record.registry.workspace_id);
|
||||||
worker.display_name = Some(record.registry.display_name);
|
worker.display_name = Some(record.registry.display_name);
|
||||||
worker.profile = record.registry.profile;
|
worker.profile = record.registry.profile;
|
||||||
|
if !project_repository_key(api, &mut worker) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some(worker)
|
Some(worker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn project_repository_key(api: &WorkspaceApi, worker: &mut SubscriptionWorker) -> bool {
|
||||||
|
let Some(repository_id) = worker.repository_id.take() else {
|
||||||
|
worker.repository_key = None;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
let Ok(Some(repository)) = api
|
||||||
|
.store
|
||||||
|
.get_repository(&api.config.workspace_id, &repository_id)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
worker.repository_key = Some(repository.repository_key);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn sort_workers(workers: &mut [SubscriptionWorker]) {
|
fn sort_workers(workers: &mut [SubscriptionWorker]) {
|
||||||
workers.sort_by(|left, right| {
|
workers.sort_by(|left, right| {
|
||||||
left.runtime_id
|
left.runtime_id
|
||||||
|
|||||||
Reference in New Issue
Block a user