Skip to content

[FEATURE] Framework-level logic-change detection for live components #2124

Description

@pyjuan91

What is the use case?

A durable live source (e.g. OCI Object Storage with a Streaming cursor, or Kafka with committed offsets) can skip its startup full scan (update_all()) on rerun when (a) bootstrap already happened, (b) the stream is durable, and (c) processing logic is unchanged — the durable stream replays the downtime backlog, so a fresh scan is redundant. Condition (c) is the hard part: #2116 ships this for OCI via a user-declared logic_version that the user must bump by hand whenever their transform changes. A forgotten bump silently skips a scan it shouldn't, keeping stale results. That's a deliberate stepping stone, not the safe end state, the framework already fingerprints processing logic for incremental change detection, so it should compute the "did my logic change?" signal itself rather than trust a hand-maintained string.

Describe the solution you'd like

Framework-computed logic-change detection over a live component's subtree, so a durable connector can gate its startup scan without a user-maintained version.

Why this isn't a thin read primitive. Logic-change detection already exists for the synchronous build tree: each #[coco.fn] has a logic fingerprint, per-component deps persist as ComponentMemoizationInfo.logic_deps , reuse is gated by all_contained_with_env(logic_deps), and synchronous child deps roll up to the parent via ComponentRunOutcome.logic_deps. That rollup does not happen across a live subtree: live children build in isolated contexts with parent_context: None, the live op outcome HandleOutcome carries no logic_deps , and a live component resolves its own parent-readiness with an empty ComponentRunOutcome. So a live component's persisted deps cover only its own body (the static mount glue), the user's real per-item transform lives in child components and never enters an aggregate the component can read back on rerun. Detecting "did anything in my live subtree change?" needs the engine to aggregate logic deps across the subtree, which doesn't exist today.

Proposed direction (high level, design-sync-first). At bootstrap, collect the union of logic_deps across the live component's subtree, persist it via the #2078 committed-state primitives (StateKind::Live), on rerun, skip the startup scan iff bootstrap_done ∧ durable ∧ all_contained_with_env(stored_set). This reuses the existing containment check (an edited or deleted function's fingerprint is no longer registered -> not contained -> scan), so a real logic change can't be silently missed and the failure-safe default is "scan." It replaces #2116's declared logic_version with a framework-computed signal.

Two questions I'd like to settle before implementing. I've put my current lean on each, happy to be talked out of either.

  1. How to aggregate the subtree's logic deps: a read-side walk of the persisted subtree memo entries after the bootstrap scan, vs. pushing deps up through the live drain path. My lean: the read-side walk. It leaves the live drain/readiness hot path untouched and picks up memoized (cache-hit) children for free, at the cost of coupling to the stable-path prefix layout. The push-up route has to thread a deps channel through the intentionally-decoupled drain path and define a capture window, which is more invasive.

  2. Whether the signal must also capture processing-affecting config/args. Today logic_deps covers function code hashes + tracked-context changes, but not a constant arg like chunk_size=512 passed to a child processor, editing it to 1024 moves no registered fingerprint, so a naive aggregate would wrongly skip the rescan. Folding the child's processor_fp in doesn't work either, since for mount_each the per-item value is also a bound arg (value = data the stream replays, constant config = logic). My lean: require processing-affecting config to flow through tracked context, so it's captured for free with no new surface. The concern I'd want your read on: that leaves a residual silent-skip if config is passed as a plain arg, the same failure shape as feat(oci): skip startup full scan on unchanged logic #2116's forgotten version bump, just much smaller. So, is shrinking that hole acceptable for the first cut, or is closing it structurally (separating constant config from per-item value at the API and fingerprinting the config explicitly) the bar?

Additional context

Follow-up to #2017, where @georgeh0 confirmed we'd want framework-level logic-change detection and @badmonster0 greenlit a separate issue to track it. Builds on #2078 (committed-state primitives) and replaces the stepping stone in #2116 (OCI declared-version), its logic_version kwarg becomes redundant and should be removed when this lands.

Scope / non-goals: durability detection stays connector-owned (Kafka offsets / OCI cursor), this is not generic memoization of live components or per-item skip. The framework provides the logic-change signal, the connector keeps owning the durable-stream guarantee and the skip wiring.

I'd like to sync on the two questions above before starting, happy to take the implementation.


❤️ Contributors, please refer to 📙Contributing Guide.
Unless the PR can be sent immediately (e.g. just a few lines of code), we recommend you to leave a comment on the issue like I'm working on it or Can I work on this issue? to avoid duplicating work. Our Discord server is always open and friendly.

Metadata

Metadata

Assignees

Labels

No labels
No labels
No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions