Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/tracedecay-global-db/src/registered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ impl From<Option<RefusedAuthorityV1>> for SessionAuthorityV1 {
}
}

// Short-lived store owners have no periodic daemon compactor. Keep their
// one admission reclaim bounded; daemon admission performs no foreground reclaim.
const SHORT_LIVED_RECLAIM_PAGES: u64 = 4096;

/// The sole map owner for one registered global-database publication.
///
/// It can issue independently counted client leases, but cannot be cloned or
Expand Down Expand Up @@ -213,6 +217,18 @@ impl RegisteredGlobalDbOwnerV1 {
}
RegisteredSchemaAttachmentV1::SessionsRefused(refused) => Some(refused),
};
// A short-lived client has no later daemon maintenance turn. Reclaim
// pages released by earlier writes after schema convergence, without
// introducing the same foreground work into daemon admission. Refused
// authorities remain byte-preserving until their typed reset.
if refused_authority.is_none() {
// Defer only optional SQLite maintenance contention. A revoked
// write authority or closed writer must still fail admission.
registered
.database
.reclaim_short_lived_pages(SHORT_LIVED_RECLAIM_PAGES)
.await?;
}
drop(registered);
Ok(Self {
database,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,65 @@ async fn schema_snapshot(
snapshot
}

#[asupersync::test]
async fn short_lived_admission_reclaims_freed_pages_without_daemon_maintenance() {
crate::register_registered_schema_installer();
let directory = TempDir::new().unwrap();
let path = directory.path().join("project/sessions.db");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let scope = || TestDatabaseRuntimeScope::ProjectSessions {
project_id: ProjectId::new("project.short-lived-reclaim").unwrap(),
};

drop(
open_registered_test_database_fixture(&path, scope())
.await
.unwrap(),
);
let connection = rusqlite::Connection::open(&path).unwrap();
let auto_vacuum: i64 = connection
.query_row("PRAGMA auto_vacuum", [], |row| row.get(0))
.unwrap();
assert_eq!(
auto_vacuum, 2,
"new stores must support incremental reclamation"
);
connection
.execute_batch(
"CREATE TABLE ephemeral_reclaim(payload BLOB);
INSERT INTO ephemeral_reclaim VALUES (zeroblob(8388608));
DROP TABLE ephemeral_reclaim;",
)
.unwrap();
let before_pages: i64 = connection
.query_row("PRAGMA page_count", [], |row| row.get(0))
.unwrap();
let before_free: i64 = connection
.query_row("PRAGMA freelist_count", [], |row| row.get(0))
.unwrap();
assert!(
before_free > 0,
"the fixture must contain reclaimable pages"
);
drop(connection);

// Reopen through the actual short-lived owner: there is no background
// compactor after this call, so admission must make physical progress.
let (lease, owner) = open_registered_test_database_fixture(&path, scope())
.await
.unwrap();
let (_, after_pages, after_free) = lease.storage_page_counts().await.unwrap();
assert!(
after_pages < before_pages as u64,
"short-lived admission did not reduce the physical page count: {before_pages} -> {after_pages}"
);
assert!(
after_free < before_free as u64,
"short-lived admission left the original free-page backlog: {before_free} -> {after_free}"
);
drop((lease, owner));
}

#[asupersync::test]
async fn partial_workflow_store_requires_reset_without_mutation() {
assert_workflow_schema_reset_without_mutation(
Expand Down Expand Up @@ -857,6 +916,11 @@ mod published_artifact_ceiling {
INSERT INTO workflow_artifact_payloads
SELECT 'sha256:large-' || i, 1048576, randomblob(1048576) FROM n;",
);
store.run(
"CREATE TABLE freed_pages(payload BLOB);
INSERT INTO freed_pages VALUES (zeroblob(2097152));
DROP TABLE freed_pages;",
);
let before = store.state();
let wal_path = format!("{}-wal", store.path.display());
let wal_bytes = || fs::metadata(&wal_path).map_or(0, |metadata| metadata.len());
Expand All @@ -874,6 +938,11 @@ mod published_artifact_ceiling {
);
assert!(owner.reset_required().is_none());
let lease = owner.issue_lease().unwrap();
let (_, _, free_pages) = lease.storage_page_counts().await.unwrap();
assert!(
free_pages > 0,
"admission reclaimed pages before convergence"
);
let issuer = owner.weak_lease_issuer();
assert_workflow_recovering(&lease).await;
assert_workflow_recovering(&issuer.issue_lease().unwrap()).await;
Expand Down
5 changes: 0 additions & 5 deletions crates/tracedecay-global-db/src/schema_stages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ use tracedecay_sessions::runtime::git_correlation::{
};
use tracedecay_store::StoreShardScopeV1;

const CANONICAL_BODY_RECLAIM_PAGES: u64 = 4096;

const REGISTRY_SCHEMA: &str = "
CREATE TABLE IF NOT EXISTS projects (
path TEXT PRIMARY KEY,
Expand Down Expand Up @@ -1453,9 +1451,6 @@ pub(crate) async fn ensure_attached_registered_schema(
)
}
})?;
database
.run_incremental_vacuum(CANONICAL_BODY_RECLAIM_PAGES)
.await?;
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Ok(match refused_authority {
Some(refused) => RegisteredSchemaAttachmentV1::SessionsRefused(refused),
None => RegisteredSchemaAttachmentV1::Admitted(RegisteredSchemaConvergence {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ use tracedecay_rusqlite_runtime::workflow::{
};

use super::{
CANONICAL_BODY_RECLAIM_PAGES, CancellableSchemaTransaction, WorkflowSchemaAdmission,
inspect_workflow_schema_for_admission, read_recorded_workflow_identity,
schema_install_checkpoint, settle_schema_transaction, workflow_schema_refusal,
CancellableSchemaTransaction, WorkflowSchemaAdmission, inspect_workflow_schema_for_admission,
read_recorded_workflow_identity, schema_install_checkpoint, settle_schema_transaction,
workflow_schema_refusal,
};
use crate::registered::RefusedAuthorityV1;
use crate::{global_db_operation_error, global_db_operation_message};

const WORKFLOW_ARTIFACT_RECLAIM_PAGES: u64 = 4096;
const ARTIFACT_TABLE: &str = "workflow_artifact_payloads";

const PUBLISHED_ARTIFACT_TABLE_SQL: &str = "CREATE TABLE workflow_artifact_payloads (
Expand Down Expand Up @@ -292,7 +293,7 @@ pub(crate) async fn reclaim_freed_artifact_pages(
}
remaining = free_pages;
database
.run_incremental_vacuum(CANONICAL_BODY_RECLAIM_PAGES)
.run_incremental_vacuum(WORKFLOW_ARTIFACT_RECLAIM_PAGES)
.await?;
}
}
Expand Down
36 changes: 24 additions & 12 deletions crates/tracedecay-lcm/src/summary_convergence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@ pub async fn record_invalidation_page_yield(
Ok(())
}

/// A failed attempt is settled against the raw revision it actually read.
/// Older dirty raw work may remain after invalidation fails; it must not
/// discard a retry deadline or resurrect a terminal refusal. A truly new
/// raw revision increments the generation, so its stale outcome loses the CAS.
pub async fn record_outcome(
conn: &(impl Executor + ?Sized),
candidate: &LcmSummaryConvergenceCandidate,
Expand All @@ -659,30 +663,38 @@ pub async fn record_outcome(
"UPDATE lcm_summary_convergence_queue
SET attempted_raw_store_id = ?3,
state = CASE
WHEN newest_raw_store_id > ?3 OR EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
WHEN newest_raw_store_id > ?3 OR (
?4 IN ('pending', 'current') AND EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
)
) THEN 'pending'
ELSE ?4
END,
failure_code = CASE
WHEN newest_raw_store_id > ?3 OR EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
WHEN newest_raw_store_id > ?3 OR (
?4 IN ('pending', 'current') AND EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
)
) THEN NULL
ELSE ?5
END,
failure_count = CASE
WHEN newest_raw_store_id > ?3 OR EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
WHEN newest_raw_store_id > ?3 OR (
?4 IN ('pending', 'current') AND EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
)
) THEN 0
ELSE ?6
END,
next_attempt_at_ms = CASE
WHEN newest_raw_store_id > ?3 OR EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
WHEN newest_raw_store_id > ?3 OR (
?4 IN ('pending', 'current') AND EXISTS (
SELECT 1 FROM lcm_summary_convergence_dirty_raw AS dirty
WHERE dirty.provider = ?1 AND dirty.session_id = ?2
)
) THEN 0
ELSE ?7
END,
Expand Down
Loading
Loading