Skip to content

fix: prevent lifecycle action races - #1562

Merged
kang-heewon merged 1 commit into
trunkfrom
issue-1546-versioned-lifecycle-rules
Jul 27, 2026
Merged

fix: prevent lifecycle action races#1562
kang-heewon merged 1 commit into
trunkfrom
issue-1546-versioned-lifecycle-rules

Conversation

@kang-heewon

@kang-heewon kang-heewon commented Jul 26, 2026

Copy link
Copy Markdown
Member

Outcome

Lifecycle production dispatch now atomically reserves idempotency keys and cooldown windows before actions begin. Infrastructure failures explicitly abort unfinished claims, so a transient state-store or save failure cannot poison retries.

Pause and activation transitions now synchronize through uniquely owned, expiring execution leases. Adapter calls begin inside the active-version boundary, leases release before awaiting adapter completion to permit reentrant pause, and abandoned leases wake blocked transitions at expiry. Registered static rules and persisted descriptors are defensively snapshotted so caller mutation cannot alter reviewed behavior.

This is a concurrency-hardening follow-up to #1551 and the acceptance criteria from #1546.

Verification

  • pnpm check — repository profile passed; 22/23 gates passed and one was not applicable
  • pnpm --filter @croco/lifecycle-core test — 31/31 passed
  • lifecycle-core typecheck, lint, and ESM/CJS/declaration build passed
  • pnpm --filter @croco/admin-ops test — 17/17 passed; typecheck, lint, and build passed
  • pnpm docs:api:check — 112/112 build tasks; 3,784 pages; generated docs matched
  • pnpm docs:api-triggers:check
  • pnpm public-api:check — 111 package snapshots matched
  • pnpm problem-registry:check — 478 codes matched 478 discoveries
  • pnpm architecture-policy:check — 4,702 imports across 113 packages passed
  • pnpm changeset-required:check — all affected publishable packages covered
  • pre-push workspace tests — 226/226 tasks
  • pre-push workspace typecheck — 225/225 tasks

Review gates

  • Correctness and regression: PASS — hostile coverage exercises pause at the dispatch boundary, duplicate lease ownership, abandoned lease expiry, reentrant pause, claim abort and retry, concurrent idempotency/cooldown, and post-registration mutation.
  • API, security, compatibility, and release: PASS — durable store contracts, migration guidance, generated API docs, public snapshots, and a minor changeset are synchronized; no secret or debug-output findings.
  • Maintainability and minimality: PASS — changes remain inside lifecycle-core, one admin-ops test adapter, and generated repository artifacts.
  • Independent adversarial review: APPROVE after all reproduced concurrency and lease-lifecycle findings were resolved.

Residual risk

Custom durable stores must implement atomic run claims and active-version lease acquisition in shared storage, wake waiting commands at lease expiry, and make abort and release idempotent. Existing custom LifecycleRunStore and LifecycleRuleStateStore implementations must adopt the documented new methods before upgrading.

Summary by CodeRabbit

  • 새로운 기능

    • 중복 실행 방지와 쿨다운 처리를 원자적으로 적용합니다.
    • 실행 중인 규칙 버전을 보호하는 만료형 실행 권한을 지원합니다.
    • 일시 중지, 버전 변경, 동시 실행 상황에서 안정적인 실행 경계를 제공합니다.
    • 실행 실패 시 미완료 예약을 자동 해제하고 재시도할 수 있습니다.
    • 라이프사이클 실행 및 규칙 실행 관련 공개 타입을 제공합니다.
  • 문서

    • 클레임, 실행 권한, 실행 결과 및 저장소 API 문서를 보강했습니다.
    • 규칙 데이터가 외부 변경으로부터 보호되는 동작을 명확히 했습니다.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kang-heewon, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c130c386-a13a-4099-841e-8666bf07ddf9

📥 Commits

Reviewing files that changed from the base of the PR and between 15f68c4 and 685da0f.

📒 Files selected for processing (22)
  • .changeset/steady-lifecycle-dispatch.md
  • packages/admin-ops/src/tests/RetryConsole.spec.ts
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryLifecycleRuleStateStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryLifecycleRunStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/LifecycleRuleRegistry.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/LifecycleRuleStateStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/LifecycleRunStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/InMemoryLifecycleRuleStateStoreOptions.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionClaim.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionClaimResult.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionResult.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRunClaim.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRunClaimResult.md
  • packages/lifecycle-core/README.md
  • packages/lifecycle-core/src/index.ts
  • packages/lifecycle-core/src/libs/InMemoryLifecycleRuleStateStore.ts
  • packages/lifecycle-core/src/libs/InMemoryLifecycleRunStore.ts
  • packages/lifecycle-core/src/libs/LifecycleRuleEvaluator.ts
  • packages/lifecycle-core/src/libs/LifecycleRuleRegistry.ts
  • packages/lifecycle-core/src/libs/types.ts
  • packages/lifecycle-core/src/tests/LifecycleRuleVersioning.spec.ts
  • public-api-surface.snapshot.json
📝 Walkthrough

Walkthrough

Lifecycle dispatch에 원자적 run claim, 만료 가능한 실행 lease, 규칙 스냅샷, 실패 시 claim 중단, 공개 타입 및 관련 테스트·문서가 추가되었습니다.

Changes

Lifecycle dispatch coordination

Layer / File(s) Summary
Claim and lease contracts
packages/lifecycle-core/src/libs/types.ts, packages/lifecycle-core/src/index.ts, packages/docs/src/content/docs/api/lifecycle-core/..., packages/lifecycle-core/README.md
Run claim과 rule execution lease 타입·스토어 계약·공개 API·호환성 문서가 추가 및 갱신되었습니다.
State-store execution leases
packages/lifecycle-core/src/libs/InMemoryLifecycleRuleStateStore.ts
실행 claim의 중복·만료·해제 처리가 추가되고, 차단된 명령은 claim 해제 또는 만료까지 대기하며 반환 상태는 복제됩니다.
Run claims and guarded evaluation
packages/lifecycle-core/src/libs/InMemoryLifecycleRunStore.ts, packages/lifecycle-core/src/libs/LifecycleRuleEvaluator.ts, packages/lifecycle-core/src/libs/LifecycleRuleRegistry.ts
idempotency·cooldown claim과 abort/save 흐름이 구현되었고, evaluator는 활성 execution lease와 복제된 rule 데이터를 사용해 action을 실행합니다.
Concurrency validation and integration updates
packages/lifecycle-core/src/tests/LifecycleRuleVersioning.spec.ts, packages/admin-ops/src/tests/RetryConsole.spec.ts, .changeset/*
lease 중복·만료, pause 경계, 장애 복구, 원자적 claim, payload 불변성 시나리오와 테스트 스토어 계약이 갱신되었습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Evaluator
  participant RunStore
  participant Registry
  participant StateStore
  Evaluator->>RunStore: claim idempotency and cooldown
  RunStore-->>Evaluator: claim result
  Evaluator->>Registry: executeIfActive
  Registry->>StateStore: claim execution lease
  StateStore-->>Registry: lease result
  Registry-->>Evaluator: execution result
  Evaluator->>RunStore: save or abort claim
Loading

Possibly related PRs

  • croco-dev/framework#1551: Lifecycle rule activation, evaluation, registry, evaluator, and store contracts overlap with this dispatch-flow update.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 lifecycle 액션 경합 방지라는 핵심 변경을 간결하게 요약하며 PR의 목적과 일치합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-1546-versioned-lifecycle-rules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

📊 Benchmark Results

✅ All benchmarks passed

Benchmark p75 Threshold Baseline vs Baseline Status Notes
CrocoApp constructor 9.7μs 30.0ms 8.2μs +18.5% -
CrocoApp lambdaHandler (10 controllers) 291.2μs 50.0ms 258.4μs +12.7% -
Lambda cold-start simulation 423.6μs 80.0ms 418.1μs +1.3% -
Lambda cold-start with headers 392.0μs 80.0ms 369.7μs +6.1% -
Lambda cold-start with binary body 361.3μs 80.0ms 339.1μs +6.5% -
Lambda cold-start with query params 301.7μs 80.0ms 301.3μs +0.1% -
Lambda cold-start with authorizer context 297.6μs 80.0ms 299.8μs -0.7% -
Lambda cold-start realistic scenario 295.8μs 80.0ms 299.2μs -1.1% -
EventBusConfig.start (10 handlers) 1.9μs 10.0ms 1.4μs +29.2% -
EventPublisher.publishNow single event 2.1μs 2.0ms 1.7μs +22.0% -
DefaultHandlerResolver.resolve × 10 0.1μs 5.0ms 0.1μs +0.0% -
Container.get singleton (cold) 62.7μs 5.0ms 70.3μs -10.8% -
Container.register × 50 components 3.2ms 10.0ms 3.2ms +0.4% -
Container.validate (50 components) 3.7ms 20.0ms 3.4ms +9.9% -
Container.get singleton (warm) 1.7μs 500.0μs 1.6μs +1.8% -
TelemetryRuntime.init (lambda preset) 2.5μs 200.0ms 1.1ms -99.8% -
lambdaPreset config creation 1.4μs 2.0ms 1.4μs +0.7% -

Updated: 2026-07-27T14:04:53.425Z · Commit: c4e215a

@kang-heewon
kang-heewon force-pushed the issue-1546-versioned-lifecycle-rules branch 4 times, most recently from c34b8ed to 15f68c4 Compare July 27, 2026 13:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/lifecycle-core/src/libs/InMemoryLifecycleRuleStateStore.ts`:
- Around line 326-353: Resolve the clock mismatch in waitForExecutionClaimChange
by ensuring expiry wakeups use the same logical clock as this.now() and
pruneExpiredExecutionClaims, rather than relying on wall-clock setTimeout alone.
Either validate and document that the configured now function tracks real time,
or replace the timeout with the store’s logical-clock scheduling/polling
mechanism so expired claims are retried correctly.

In `@packages/lifecycle-core/src/libs/LifecycleRuleRegistry.ts`:
- Around line 412-419: Update the get(ruleId) lookup to query registrations
directly instead of calling getAll(), then deep-clone only the matching
registration or rule before returning it. Preserve the existing not-found
behavior and use the existing registrations map and clone helpers, consistent
with getRegistration.
- Around line 28-52: Update cloneValue and cloneRule to use structuredClone for
cloneable lifecycle data, preserving Map, Set, RegExp, class-instance values,
and prototypes across triggers, actions, and related descriptors. Ensure the
implementation retains the existing cloning behavior for supported fields while
accounting for structuredClone’s DataCloneError when the payload contract
permits functions or symbols.

In `@packages/lifecycle-core/src/tests/LifecycleRuleVersioning.spec.ts`:
- Around line 642-644: Replace the single Promise.resolve microtask yield in the
lease/pause test with a macrotask wait such as setTimeout(..., 0), or an
equivalent multi-tick flush, before asserting pauseCompleted remains false. Keep
the assertion and surrounding lease-gating behavior unchanged so the test
verifies pause is blocked across a meaningful event-loop delay.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8d9077c4-c975-4b9f-a4e9-d97412686fd0

📥 Commits

Reviewing files that changed from the base of the PR and between b07ae3a and 15f68c4.

📒 Files selected for processing (21)
  • .changeset/steady-lifecycle-dispatch.md
  • packages/admin-ops/src/tests/RetryConsole.spec.ts
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryLifecycleRuleStateStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/InMemoryLifecycleRunStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/classes/LifecycleRuleRegistry.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/LifecycleRuleStateStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/interfaces/LifecycleRunStore.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionClaim.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionClaimResult.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRuleExecutionResult.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRunClaim.md
  • packages/docs/src/content/docs/api/lifecycle-core/src/type-aliases/LifecycleRunClaimResult.md
  • packages/lifecycle-core/README.md
  • packages/lifecycle-core/src/index.ts
  • packages/lifecycle-core/src/libs/InMemoryLifecycleRuleStateStore.ts
  • packages/lifecycle-core/src/libs/InMemoryLifecycleRunStore.ts
  • packages/lifecycle-core/src/libs/LifecycleRuleEvaluator.ts
  • packages/lifecycle-core/src/libs/LifecycleRuleRegistry.ts
  • packages/lifecycle-core/src/libs/types.ts
  • packages/lifecycle-core/src/tests/LifecycleRuleVersioning.spec.ts
  • public-api-surface.snapshot.json

Comment thread packages/lifecycle-core/src/libs/InMemoryLifecycleRuleStateStore.ts
Comment thread packages/lifecycle-core/src/libs/LifecycleRuleRegistry.ts Outdated
Comment thread packages/lifecycle-core/src/libs/LifecycleRuleRegistry.ts
Comment thread packages/lifecycle-core/src/tests/LifecycleRuleVersioning.spec.ts Outdated
@kang-heewon
kang-heewon force-pushed the issue-1546-versioned-lifecycle-rules branch from 15f68c4 to 685da0f Compare July 27, 2026 13:56
@kang-heewon

Copy link
Copy Markdown
Member Author

@coderabbitai resolve

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@kang-heewon
kang-heewon merged commit 4137d6d into trunk Jul 27, 2026
10 checks passed
@kang-heewon
kang-heewon deleted the issue-1546-versioned-lifecycle-rules branch July 27, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant