-
Notifications
You must be signed in to change notification settings - Fork 2
perf(daemon): bound catch-up chunk convergence to the chunk's own input #4694
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
Changes from all commits
b1ac686
6812abc
5b5b567
1a0ac23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -426,6 +426,7 @@ def execute_many(paths: Sequence[Path]) -> StageExecuteReturn: | |
| execute=execute, | ||
| check_many=check_many, | ||
| execute_many=execute_many, | ||
| whole_archive=True, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -480,6 +481,7 @@ def execute_many(paths: Sequence[Path]) -> StageExecuteReturn: | |
| execute=execute, | ||
| check_many=check_many, | ||
| execute_many=execute_many, | ||
| whole_archive=True, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -539,7 +541,6 @@ def execute(path: Path) -> StageExecuteReturn: | |
| session_ids=session_ids, | ||
| page_size=_DAEMON_INSIGHT_REBUILD_PAGE_SIZE, | ||
| ) | ||
| _record_fts_freshness_after_insights(conn) | ||
| conn.commit() | ||
| logger.info( | ||
| "insights: refreshed sessions=%d profiles=%d work_events=%d phases=%d threads=%d", | ||
|
|
@@ -614,7 +615,6 @@ def execute_many(paths: Sequence[Path]) -> StageExecuteReturn: | |
| session_ids=session_ids, | ||
| page_size=_DAEMON_INSIGHT_REBUILD_PAGE_SIZE, | ||
| ) | ||
| _record_fts_freshness_after_insights(conn) | ||
| conn.commit() | ||
| logger.info( | ||
| "insights: batch refreshed paths=%d sessions=%d profiles=%d work_events=%d phases=%d threads=%d", | ||
|
|
@@ -681,7 +681,6 @@ def execute_sessions(session_ids: Sequence[str]) -> StageExecuteReturn: | |
| session_ids=ids, | ||
| page_size=_DAEMON_INSIGHT_REBUILD_PAGE_SIZE, | ||
| ) | ||
| _record_fts_freshness_after_insights(conn) | ||
| conn.commit() | ||
| remaining = _stale_session_profile_ids(conn, ids) | ||
| logger.info( | ||
|
|
@@ -1067,6 +1066,70 @@ def execute_many(_paths: Sequence[Path]) -> StageExecuteReturn: | |
| check_many=check_many, | ||
| execute_many=execute_many, | ||
| false_means_pending=True, | ||
| whole_archive=True, | ||
| ) | ||
|
|
||
|
|
||
| def make_fts_readiness_stage(db_path: Path) -> ConvergenceStage: | ||
| """Publish the exact archive-wide FTS readiness audit once per pass. | ||
|
|
||
| The audit aggregates every ``blocks`` and ``messages_fts`` row, so it is | ||
| whole-archive work: partition-scoped FTS repair and per-session insight | ||
| rebuilds leave it to this stage, which runs after both, once per | ||
| whole-archive convergence pass. | ||
| """ | ||
|
|
||
| def archive_db() -> Path: | ||
| return _active_archive_index_path(db_path) or db_path | ||
|
|
||
| def publish() -> StageExecuteReturn: | ||
| database = archive_db() | ||
| if not database.exists(): | ||
| return True | ||
| try: | ||
| conn = _open_archive_insight_write_connection(database) | ||
| try: | ||
| _record_fts_freshness_after_insights(conn) | ||
| conn.commit() | ||
| finally: | ||
| conn.close() | ||
| except Exception as exc: | ||
| if _is_transient_sqlite_lock(exc): | ||
| logger.info("fts readiness: audit deferred because sqlite is busy: %s", exc) | ||
| return False | ||
| logger.warning("fts readiness: audit failed", exc_info=True) | ||
| raise | ||
| return True | ||
|
|
||
| def check(path: Path) -> bool: | ||
| return archive_db().exists() | ||
|
|
||
| def execute(path: Path) -> StageExecuteReturn: | ||
| return publish() | ||
|
|
||
| def check_many(paths: Sequence[Path]) -> set[Path]: | ||
| return set(paths) if paths and archive_db().exists() else set() | ||
|
|
||
| def execute_many(paths: Sequence[Path]) -> StageExecuteReturn: | ||
| return publish() if paths else True | ||
|
|
||
| def check_sessions(session_ids: Sequence[str]) -> set[str]: | ||
| return set(session_ids) if session_ids and archive_db().exists() else set() | ||
|
|
||
| def execute_sessions(session_ids: Sequence[str]) -> StageExecuteReturn: | ||
| return publish() if session_ids else True | ||
|
|
||
| return ConvergenceStage( | ||
| name="fts_readiness", | ||
| description="Publish the exact archive-wide FTS readiness audit", | ||
| check=check, | ||
| execute=execute, | ||
| check_many=check_many, | ||
| execute_many=execute_many, | ||
| check_sessions=check_sessions, | ||
| execute_sessions=execute_sessions, | ||
| false_means_pending=True, | ||
| whole_archive=True, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -1107,6 +1170,7 @@ def make_default_convergence_stages( | |
| make_claude_workflow_stage(db_path), | ||
| make_delegation_work_evidence_stage(db_path), | ||
| make_derived_stage(db_path), | ||
| make_fts_readiness_stage(db_path), | ||
| make_standing_query_stage(db_path, evaluator=ArchiveCanonicalPlanEvaluator(db_path)), | ||
| ) | ||
| ) | ||
|
|
@@ -2449,9 +2513,8 @@ def _archive_insights_execute_ids( | |
| finally: | ||
| if marker_conn is not None: | ||
| marker_conn.close() | ||
| # The rebuild commits its own rows. Publish and commit the final exact FTS | ||
| # state in the same production stage before reporting success. | ||
| _record_fts_freshness_after_insights(conn) | ||
| # The rebuild commits its own rows; the exact archive-wide FTS audit is | ||
| # published once per whole-archive pass by ``make_fts_readiness_stage``. | ||
| conn.commit() | ||
|
Comment on lines
+2516
to
2518
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.
When a hot session's stage-specific AGENTS.md reference: AGENTS.md:L111-L115 Useful? React with 👍 / 👎. |
||
| remaining = _archive_stale_session_profile_ids(conn, list(session_ids)) | ||
| logger.info( | ||
|
|
@@ -2474,6 +2537,7 @@ def _archive_insights_execute_ids( | |
| "make_delegation_work_evidence_stage", | ||
| "make_default_convergence_stages", | ||
| "make_embed_stage", | ||
| "make_fts_readiness_stage", | ||
| "make_fts_stage", | ||
| "make_derived_stage", | ||
| "make_raw_authority_verdict_cache_stage", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,7 +80,11 @@ def run_once_sync( | |
| ) | ||
| with open_daemon_connection(self._db_path, timeout=30.0) as conn: | ||
| result = FtsDerivationAdapter().converge(conn, keys=partition_keys) | ||
| if result.outcome is FtsOutcome.DONE: | ||
| # The readiness projection is an exact archive-wide audit; | ||
| # a partition-scoped pass cannot claim it and must not pay | ||
| # for it (``make_fts_readiness_stage`` publishes it once per | ||
| # whole-archive convergence pass). | ||
| if result.outcome is FtsOutcome.DONE and partition_keys is None: | ||
| self._publish_readiness_projection(conn) | ||
| state = { | ||
| FtsOutcome.DONE: FtsOwnerState.READY_EXACT, | ||
|
Comment on lines
+87
to
90
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.
With Useful? React with 👍 / 👎. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -759,8 +759,14 @@ async def ingest_files( | |
| skipped_file_count: int = 0, | ||
| emit_event: bool = True, | ||
| max_pass_seconds: float | None = None, | ||
| whole_archive_convergence: bool = True, | ||
| ) -> LiveBatchMetrics: | ||
| """Ingest files in batch, run post-ingest convergence, and return metrics.""" | ||
| """Ingest files in batch, run post-ingest convergence, and return metrics. | ||
|
|
||
| ``whole_archive_convergence=False`` bounds post-ingest convergence to | ||
| this batch's own subjects (a catch-up chunk); the caller runs one | ||
| whole-archive pass at the end of its catch-up. | ||
| """ | ||
| authorization = self.require_cursor_authority(paths) | ||
| refused_paths = self._refused_paths | ||
| self._refused_paths = frozenset() | ||
|
|
@@ -906,6 +912,8 @@ async def flush_append_plans() -> None: | |
| "watcher.live_ingest.append_convergence", | ||
| self._converge_paths, | ||
| [plan.path for plan in append_result.succeeded], | ||
| whole_archive=whole_archive_convergence, | ||
| session_ids=tuple(append_result.session_ids_by_path.values()), | ||
| ) | ||
| convergence_time_s += elapsed | ||
| release_process_memory() | ||
|
|
@@ -1156,6 +1164,8 @@ async def flush_append_plans() -> None: | |
| "watcher.live_ingest.full_convergence", | ||
| self._converge_paths, | ||
| full_result.succeeded, | ||
| whole_archive=whole_archive_convergence, | ||
| session_ids=full_result.changed_session_ids, | ||
|
Comment on lines
1164
to
+1168
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.
When the final catch-up chunk contains changed files from multiple full-ingest source groups, or an append group plus full-ingest groups, AGENTS.md reference: AGENTS.md:L109-L113 Useful? React with 👍 / 👎. |
||
| ) | ||
| convergence_time_s += elapsed | ||
| release_process_memory() | ||
|
|
@@ -1733,7 +1743,11 @@ def _record_convergence_outcome(self, path: Path, debts: Iterable[ConvergenceDeb | |
| record_convergence_outcome(self._cursor, path, debts, archive_root=archive_root) | ||
|
|
||
| def _converge_paths( | ||
| self, paths: Iterable[Path] | ||
| self, | ||
| paths: Iterable[Path], | ||
| *, | ||
| whole_archive: bool = True, | ||
| session_ids: Iterable[str] = (), | ||
| ) -> tuple[set[Path], float, dict[str, float], list[ConvergenceDebt]]: | ||
| unique_paths = tuple(sorted(dict.fromkeys(paths))) | ||
| if not unique_paths: | ||
|
|
@@ -1745,23 +1759,33 @@ def _converge_paths( | |
| try: | ||
| converge_batch = getattr(self._converger, "converge_batch", None) | ||
| if callable(converge_batch): | ||
| states, timings = converge_batch(unique_paths) | ||
| # The keyword is passed only when it narrows the pass, so | ||
| # convergers without the parameter keep their whole-archive | ||
| # default. | ||
| states, timings = ( | ||
| converge_batch(unique_paths) if whole_archive else converge_batch(unique_paths, whole_archive=False) | ||
| ) | ||
| batch_completed = { | ||
| path for path in unique_paths if path in states and bool(getattr(states[path], "converged", False)) | ||
| } | ||
| debt_items = convergence_debt_from_states(unique_paths, states) | ||
| # #1654: after convergence, check for new hook events | ||
| # that carry paste evidence and update matching messages. | ||
| batch_stage_timings = {stage_name: float(elapsed) for stage_name, elapsed in timings.items()} | ||
| # #1654: after convergence, check for new hook events that | ||
| # carry paste evidence and update matching messages. Scoped to | ||
| # this batch's sessions so the scan is bounded by the batch, | ||
| # not by the archive's whole hook history. | ||
| t_paste = time.perf_counter() | ||
| try: | ||
| from polylogue.sources.live.hook_paste_enrichment import enrich_paste_from_hooks | ||
|
|
||
| enrich_paste_from_hooks(self._cursor._db_path) | ||
| enrich_paste_from_hooks(self._cursor._db_path, session_ids=tuple(dict.fromkeys(session_ids))) | ||
|
Comment on lines
1779
to
+1781
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.
When a AGENTS.md reference: AGENTS.md:L109-L115 Useful? React with 👍 / 👎. |
||
| except Exception: | ||
| logger.debug("hook_paste: enrichment failed (non-fatal)", exc_info=True) | ||
| batch_stage_timings["hook_paste_enrichment"] = time.perf_counter() - t_paste | ||
| return ( | ||
| batch_completed, | ||
| time.perf_counter() - started, | ||
| {stage_name: float(elapsed) for stage_name, elapsed in timings.items()}, | ||
| batch_stage_timings, | ||
| debt_items, | ||
| ) | ||
|
|
||
|
|
||
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.
For a batch stage such as
raw_authority_verdict_cache, returningFalsewhile bounded work remains causescheck_many()to run a second time during the post-execution recheck, but this records only the initial probe; the same timing is also omitted when the initial probe raises because the recorder is confined to theelsebranch. On a large archive either omitted scan can dominate convergence time, so the returned<stage>.checkledger does not provide the fully attributed cost promised byconverge_batch(). Time each check invocation, including rechecks and exceptional exits.Useful? React with 👍 / 👎.