Harden agent file handling and TypeScript runtime packaging - #3797
Conversation
✅ Deploy Preview for golemcloud canceled.
|
# Conflicts: # .github/workflows/benchmark.yaml # .github/workflows/ci.yaml # .github/workflows/skill-harness.yaml # Cargo.lock # Cargo.toml # Makefile.toml # golem-worker-executor/src/worker/mod.rs
# Conflicts: # cli/golem-cli/src/command_handler/component/ifs.rs # golem-worker-executor/src/services/worker.rs # golem-worker-executor/src/worker/mod.rs
|
📖 Docs preview: https://docs-pu6jgqb6n-golem-cloud.vercel.app Built from commit |
# Conflicts: # golem-worker-executor/src/services/worker.rs
|
I don't like the additional "deletion lock" introduced in There seems to be two major problems this lock is trying to solve:
Detailed agent-written spec about this follows: Replace the external deletion lock with worker-owned deletionRemove the newly added Make deletion an operation owned by the existing Keep the separate 1. Add genuinely existing-only worker acquisition“Existing-only” means:
Implement this through the actual constructor/metadata-loading path, inside the cache’s coordinated initialization. A separate existence check followed by get-or-create is not sufficient. Keep intentional create-or-load behavior for normal creation/invocation paths. Do not overload invocation freshness semantics merely to express existing-only lifecycle operations. Update all six helper call sites and resume. Preserve their status-specific policies:
Remove creation-only arguments from the existing-only API. In particular, an update’s target revision must not become a fallback creation revision. 2. Make deletion ownership distinguishable from deletion completionAn illustrative internal API is: enum DeleteOutcome {
Started(DeletionHandle),
AlreadyDeleting(DeletionHandle),
}
impl DeletionHandle {
async fn wait(&self) -> Result<(), WorkerExecutorError>;
}Exact names are flexible; the contract is not:
The existing gRPC delete adapter should await either outcome and return its result. This does not require a new wire/API response variant. 3. Claim ownership before the first deletion side effectClaim deletion before calling Publish the claim under a short worker-state critical section, then release the lock before awaiting work. From that point:
Do not simply replace a Do not hold 4. Completion means the entire deletion has finishedA successful deletion handle resolves only after:
The existing stopping notification is therefore not the deletion completion handle. Shutdown can complete while durable cleanup is still running. 5. Keep the deleting worker authoritative until cleanup finishesThis is essential to removing the external registry. A deleting worker must remain discoverable in the active cache until its deletion owner finishes cleanup—even after persistent metadata has been removed. A concurrent delete must be able to find the worker and join its operation during that interval. Audit the other retirement paths:
They must not remove an entry whose deletion operation owns cleanup. They also must not wait for deletion from inside the invocation loop, because deletion may itself be waiting for that loop to exit; defer retirement to the deletion owner instead. Make removal identity-checked: a stale A completed old worker must retain its terminal deletion result. Calling deletion through an old 6. Define cancellation and failure explicitlyRecommended behavior:
If cleanup fails:
This specifies in-process ownership and retries, not a new persisted deletion/recovery protocol. 7. Required regression testsUse deterministic barriers around the actual acquisition/deletion paths, not only atomics modeling metadata existence. Scenario Also retain coverage for the previous concern: interruption/shutdown must be able to perform metadata reads before deletion acquires the metadata write lock. The acceptance invariant is: once a caller has acquired a particular worker and claimed its deletion, exactly one operation owns cleanup of that worker; overlapping callers can observe or await it, no lifecycle lookup creates an absent agent, and old cleanup cannot affect a replacement. |
|
Other than the above, all looks good! |
# Conflicts: # golem-worker-executor/src/services/worker.rs # golem-worker-executor/src/worker/lifecycle.rs
# Conflicts: # sdks/ts/packages/golem-ts-sdk/tsconfig.type-tests.json
# Conflicts: # golem-worker-executor-test-utils/src/lib.rs
# Conflicts: # .agents/skills/understanding-durable-execution/SKILL.md
# Conflicts: # golem-worker-executor/src/worker/mod.rs
| use tokio_util::sync::CancellationToken; | ||
| use tracing::{Instrument, Span, debug, info}; | ||
|
|
||
| struct DeletionStageHook { |
There was a problem hiding this comment.
Can we keep the inherit_test_dep!s at the top of the file please? :)
| use wasmtime::component::Instance; | ||
|
|
||
| #[allow(clippy::redundant_closure)] // Adapts a reusable `Fn() -> Future` to `AsyncFnOnce`. | ||
| async fn get_or_insert_create_or_load<K, V, F, Fut>( |
There was a problem hiding this comment.
This has a very weird name :) and I doubt it is necessary. (If it is, it should live in our Cache type, and definitely not implemented like this). I don't even understand what this is trying to solve
| let cache_key = owned_agent_id.clone(); | ||
| let deps = deps.clone(); | ||
| let invocation_context_stack = invocation_context_stack.clone(); | ||
| let initialize = || { |
There was a problem hiding this comment.
The way the "get but existing only" has been implemented in this service is not good enough. The cache is not supposed to be called with different construction codes in get_or_insert and that's what the above hack is trying to workaround.
The proper implementation would be to introduce a new initial state to the Worker state machine, from where the caller can check existance or ensure existance, while the worker instance is already registered the cache.
Something like:
// Illustrative API:
let worker = active_agents.get_or_add_unresolved(agent_id).await;
// Request-specific operation, serialized by the Worker state machine:
worker.ensure_existing(principal).await?;
// or:
worker.ensure_created(create_parameters).await?;| } | ||
|
|
||
| struct DeletionCompletion { | ||
| result: StdMutex<Option<Result<(), WorkerExecutorError>>>, |
There was a problem hiding this comment.
Could this be a one-shot channel or something similar higher level abstraction?
| } | ||
| } | ||
|
|
||
| pub(super) struct WorkerLifecycleState { |
There was a problem hiding this comment.
I expected this state to live in WorkerInstance::Deleting or a new state of the worker, not as a wrapper on top of it
| struct OpenOplogEntry { | ||
| pub oplog: Weak<dyn Oplog>, | ||
| pub initial: Arc<AtomicBool>, | ||
| generation: Arc<()>, |
There was a problem hiding this comment.
I'm not sure where to comment about this, it is about all the changes in the oplog submodules not just this line.
Adding this generation, and the explicit removes is trying to fix a race condition where the old open oplog instance's drop closes a recreated one, after an agent gets deleted and reopened.
Even though it solves that problem, if this happens, we have a much bigger problem! It means we have two open oplog instances simulateneously pointing to the same physical oplog. This is the split brain situation we can have among executors, just locally. What happens with drop is just a small part of it; it makes it possible to correupt the whole oplog.
This global Oplog cache with the weak references etc was exactly crated to prevent this - the "singleton-ness" of an oplog instance is stronger than the lifecycle of workers, anything accessing the oplog in any way must get the same instance.
So I don't want this patch; we either never have this problem in practice (just not proven by the type system) in which case we don't need to modify the oplog implementation at all. If we do have this race condition, we have to solve it properly and not just protect against a late drop
# Conflicts: # .agents/skills/understanding-durable-execution/SKILL.md
Retain final module-owned cleanup separately from immutable deletion attempt results. Explicit retries join pending cleanup or retry verified filesystem deletion through its owning generation before advancing storage and cache removal. Cover unload timeouts, cancelled and concurrent retries, persistent native cleanup failures, completed stages, late results, and replacement filesystem isolation. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a981-f819-754d-9503-e35c4b7517ab Co-authored-by: Amp <amp@ampcode.com>
Serialize cold oplog lifecycle operations and retain completion for transport, metadata, state-actor, upload, archive, and monitor work before storage removal. Keep fork source reads existing-only and source-exclusive; fork target construction safety remains deferred. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a976-b1c0-7199-916f-f0259069b64d Co-authored-by: Amp <amp@ampcode.com>
…locker 2) Amp-Thread-ID: https://ampcode.com/threads/T-01a0a97d-6b06-762b-bdb5-123789950ffc Co-authored-by: Amp <amp@ampcode.com>
…ailure paths Amp-Thread-ID: https://ampcode.com/threads/T-01a0a97d-6b06-762b-bdb5-123789950ffc Co-authored-by: Amp <amp@ampcode.com>
Explain independent unload cleanup and the bounded observer in inline comments. Remove the primary oplog actor's unwind catcher, which does not run under production panic=abort profiles. Preserve waiting for pending uploads on normal shutdown. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a8c2-fae7-7747-a839-809318218e49 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a8c2-fae7-7747-a839-809318218e49 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0a8c2-fae7-7747-a839-809318218e49 Co-authored-by: Amp <amp@ampcode.com>
golem agent file-contents ... --output -for raw stdout bytes while routing progress and errors to stderr with a scoped guardGOLEM_HTTP_COMPONENT_UPLOAD_*, with no higher-level upload retry policy0.4.4