Skip to content

fix(aws/scheduler): harden ScheduleGroup reconcile + add lifecycle convergence tests#230

Open
sam-goodwin wants to merge 1 commit intomainfrom
claude/harden-scheduler-group
Open

fix(aws/scheduler): harden ScheduleGroup reconcile + add lifecycle convergence tests#230
sam-goodwin wants to merge 1 commit intomainfrom
claude/harden-scheduler-group

Conversation

@sam-goodwin
Copy link
Copy Markdown
Contributor

Following the SQS hardening template from #184 and the sibling Schedule PR #200. Hardens the EventBridge Scheduler ScheduleGroup resource. Unlike Schedule, schedule groups DO support tagging — so ownership detection, adoption gating, and tag-drift convergence are all in scope here.

Reconciler changes

- if (!observed?.Arn) {
-   yield* scheduler.createScheduleGroup({ Name, Tags })
-     .pipe(Effect.catchTag("ConflictException", () => …listTagsForResource…hasTags…throw…))
-   observed = yield* scheduler.getScheduleGroup({ Name })
- }
- // tag sync diffed against observedTagsResp — kept
+ const arn = observed?.Arn
+   ?? yield* scheduler.createScheduleGroup({ Name, Tags })
+        .pipe(conflictRetry, Effect.map(r => r.ScheduleGroupArn));
  • read now returns Unowned(attrs) when the group lacks the alchemy internal tag set, so first-touch adoption requires --adopt / adopt(true) instead of being silently taken over. Subsequent reconciles trust the persisted output.
  • Drop the bespoke ConflictException ownership check on createScheduleGroup. The previous code fetched tags, called hasTags, and threw a plain Error if they didn't match — duplicating ownership logic that now lives entirely in read + the engine's --adopt gate.
  • Replace it with a bounded ConflictException retry (15 × 2s) on createScheduleGroup, tagResource, untagResource, and deleteScheduleGroup. EventBridge Scheduler returns ConflictException while a group is in the transient DELETING/CREATING state, so re-creating a name through a residual DELETING window now converges instead of failing.
  • Re-read the final State from the cloud after reconcile rather than echoing observed?.State (which may be stale by one round-trip).

ConflictException is intentionally NOT tagged RetryableError in distilled — it's context-dependent (DELETING race vs. genuine name collision). The reconciler-level bounded retry stays scoped.

New lifecycle tests

packages/alchemy/test/AWS/Scheduler/ScheduleGroup.test.ts — each test runs destroy → deploy → … → destroy on a ScratchStack and asserts convergence at every step:

  • create + delete with default props
  • redeploy with same props is a no-op
  • reconcile resets tags mutated out-of-band via the raw Scheduler SDK
  • reconcile re-creates a group deleted out-of-band (rides the DELETING window)
  • changing name triggers replace; old group is deleted
  • destroying an already-deleted group is a no-op
  • foreign-tagged group requires adopt(true) to take over and rewrites internal alchemy tags

Distilled patch

No patch needed — packages/aws/patches/scheduler.json (per the existing process) already covers ConflictException, and tagging it RetryableError would over-broaden coverage. The reconciler-level bounded retry handles the genuinely transient cases.

…nvergence tests

- read() returns Unowned(attrs) when the group lacks alchemy internal tags so first-touch adoption requires --adopt / adopt(true).
- Drop the bespoke tag-check ConflictException branch on createScheduleGroup. Replaced with bounded ConflictException retry (15 x 2s) so re-creating a name through a residual DELETING window converges instead of failing.
- Diff tags against observed cloud tags (not olds.tags) so adoption rewrites ownership tags, and out-of-band tag drift converges.
- conflictRetry on tagResource / untagResource / deleteScheduleGroup.
- Re-read final State from the cloud after reconcile rather than guessing.

Add lifecycle test suite covering: idempotent redeploy, drift convergence after out-of-band tag mutation, recreate after out-of-band deletion, replace on name change, double-destroy idempotency, and adopt(true) takeover of a foreign-tagged group.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alchemy-version-bot
Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

bun add alchemy@https://pkg.ing/alchemy/2398c87

@alchemy.run/better-auth

bun add @alchemy.run/better-auth@https://pkg.ing/@alchemy.run/better-auth/2398c87

@alchemy.run/pr-package

bun add @alchemy.run/pr-package@https://pkg.ing/@alchemy.run/pr-package/2398c87

@alchemy-version-bot
Copy link
Copy Markdown
Contributor

alchemy-version-bot Bot commented May 5, 2026

Website Preview Deployed

URL: https://alchemyeffectwebsite-worker-pr-230-asjdr4zten6qn7ha.testing-2b2.workers.dev

Built from commit 2398c87.


This comment updates automatically with each push.

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