Dynamic workflow recompiles every round when compiled CRD blob exceeds storage-cache entry cap: getRawBytes discards valid data on CACHE_WRITE_FAILED
- Dominant language
- Go
- Stars
- 7.5k
- Forks
- 886
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 120
Description
## Describe the bug
When a dynamic node's compiled-workflow cache blob is larger than the storage cache's
per-entry limit, flytepropeller recompiles the dynamic workflow on **every** evaluation
round, even though the blob is read back from blob storage successfully each time. On a
large dynamic fan-out this recompilation dominates round time and node admission slows to
a crawl; the workflow can effectively stall while showing RUNNING.
Log signature (one pair per evaluation round):
```
Failed to load cached flyte workflow, this will cause the dynamic workflow to be
recompiled. Error: [CACHE_WRITE_FAILED] Failed to Cache the metadata, caused by:
The entry size is larger than 1/1024 of cache size
```
## Root cause
`flytestdlib/storage/cached_rawstore.go` `ReadRaw` returns the successfully-read data
**together with a non-nil error** when the read succeeded but the freecache `Set` failed
(freecache rejects entries larger than 1/1024 of total cache size, so any blob over
`storage.cache.max_size_mbs / 1024` MB fails to memoize on every read).
flytestdlib PR flyteorg/flytestdlib#28 (2019) introduced `ErrFailedToWriteCache` /
`IsFailedWriteToCache` precisely so callers can treat this as a soft failure, and
`ReadProtobuf` in `protobuf_store.go` does check it and proceeds with the fetched bytes.
But the two sibling reads in
`flytepropeller/pkg/controller/nodes/task/remote_workflow_store.go` are asymmetric:
- `GetCompiledWorkflow` → `ReadProtobuf` → handles `IsFailedWriteToCache` correctly.
- `GetWorkflowCRD` → local `getRawBytes` → calls `store.ReadRaw` directly and does
`if err != nil { return nil, err }`, discarding the valid data.
So `RetrieveCache` fails, `dynamic_workflow.go` logs the warning above, increments the
`CacheError` metric, and recompiles — every round, forever, for any dynamic workflow whose
CRD JSON blob exceeds the entry cap. The `metastore:cache_write_err` counter climbs in
lockstep (we observed tens of thousands over one workflow execution).
Still present on master (`remote_workflow_store.go` and `cached_rawstore.go` unchanged);
observed in production on v1.15.3.
## Expected behavior
A cache-*write* failure after a successful backing-store read shouldn't fail the read.
`getRawBytes` should mirror the existing `ReadProtobuf` pattern:
```go
rawReader, err := r.store.ReadRaw(ctx, source)
if err != nil && !storage.IsFailedWriteToCache(err) {
return nil, err
}
```
(plus nil-guard on the reader), so oversized blobs cost one blob-store GET per round
instead of a full recompile.
## Steps to reproduce
1. Configure `storage.cache.max_size_mbs: 1024` (entry cap becomes 1 MB).
2. Run a dynamic workflow large enough that its compiled CRD JSON (`futures_compiled.pb`)
exceeds 1 MB — a wide ArrayNode fan-out gets there quickly.
3. Watch propeller logs: the CACHE_WRITE_FAILED warning pair repeats every round and the
dynamic workflow recompiles each time; `cache_write_err` grows unboundedly.
## Context
- flyte v1.15.3 (flyte-core helm chart), storage type s3.
- Related history: flyteorg/flytepropeller#254 introduced the compiled-workflow cache
split where the asymmetry lives; flyteorg/flytestdlib#28 introduced the soft-failure
contract that `getRawBytes` misses; #4611 discusses the cache mechanism.
Contributor guide
Research direction
Start in flytepropeller/pkg/controller/nodes/task/remote_workflow_store.go at getRawBytes, then compare its ReadRaw handling with ReadProtobuf in protobuf_store.go. Read flytestdlib/storage/cached_rawstore.go to confirm the soft-failure contract. Done means a successful backing-store read is retained when cache writing fails, so oversized CRD blobs do not trigger repeated recompilation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100