Skip to content

fix(organizations): expire handshakes that pass their deadline - #2551

Merged
vieiralucas merged 1 commit into
mainfrom
fix/handshake-expiry
Sep 24, 2026
Merged

vieiralucas merged 1 commit into
mainfrom
fix/handshake-expiry

Conversation

@vieiralucas

@vieiralucas vieiralucas commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

AWS gives a handshake 15 days and expires it on its own. fakecloud stamped an ExpirationTimestamp, reported it, and then never acted on it, so an overdue invitation stayed OPEN and acceptable forever. EXPIRED is a modeled member of both HandshakeState and ResponsibilityTransferStatus, and nothing ever produced it. The docs already claimed "expired handshakes flip to EXPIRED" -- this makes that true.

  • Every Organizations call sweeps past-due handshakes first. It cannot hang off the mutating paths alone: a DescribeHandshake has to report EXPIRED too, and an AcceptHandshake must be refused rather than reviving a lapsed offer. A read-locked staleness check keeps the write lock for requests that actually have something to expire, and it is self-limiting.
  • Expiry runs through resolve_handshake like every other terminal transition, so a responsibility transfer riding an expired handshake ends with it rather than sitting REQUESTED with an ActiveHandshakeId pointing at a handshake that is gone.
  • EndTimestamp is the handshake's deadline, not the moment the sweep noticed. resolve_handshake stamps "now", which is right for a decline or cancel because somebody acted then; an idle process would otherwise report a transfer ending days after the ExpirationTimestamp on the same record.
  • The sweep is persisted even when triggered by a read -- otherwise the expiry is lost on restart and the handshake comes back OPEN, past its deadline and acceptable again.
  • /_fakecloud/organizations/responsibility-transfers sweeps too: it read the same object and answered REQUESTED with a live activeHandshakeId while the API already said EXPIRED.

Surface impact

  • Reference docs / website: website/content/docs/services/organizations.md sharpened -- it already asserted expiry worked, now it says when and what happens to a riding transfer.
  • SDKs: no change. No new or changed API surface; the introspection row shape is unchanged (the route just reports swept state).
  • README / machine metadata / repo description: no change -- no new service, operation, or conformance variant.
  • Examples / changelog: no change.

Test plan

  • 190 organizations unit tests (4 new), all green
  • cargo nextest run -p fakecloud-conformance -E 'test(organizations)' -- 10/10
  • cargo nextest run -p fakecloud-e2e organizations + cloudformation_organizations -- 47/47
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • Each behavioral assertion checked to fail with the fix reverted

🤖 Generated with Claude Code

https://claude.ai/code/session_01EeYYZqG5anqYXR8761ZjWu


Summary by cubic

Expires Organization handshakes that pass their 15-day deadline. Overdue handshakes previously stayed OPEN and acceptable forever — EXPIRED was modeled but never produced, so the reported ExpirationTimestamp was decoration.

  • Every Organizations call sweeps past-due handshakes first, so DescribeHandshake reports EXPIRED and AcceptHandshake is refused instead of reviving a lapsed offer.
  • A responsibility transfer riding an expired handshake ends with it, stamped at the handshake's deadline rather than when the sweep noticed.
  • Sweeps are persisted even when triggered by a read, so an expiry isn't lost on restart.
  • The /_fakecloud/organizations/responsibility-transfers introspection route sweeps too, so it no longer contradicts the API.

Written for commit 7063da9. Summary will update on new commits.

Review in cubic

AWS gives a handshake 15 days and expires it on its own. fakecloud
stamped an `ExpirationTimestamp`, reported it, and then never acted on
it, so an overdue invitation stayed OPEN and acceptable forever and the
timestamp was decoration. `EXPIRED` is a modeled member of both
`HandshakeState` and `ResponsibilityTransferStatus`, and nothing ever
produced it.

Every Organizations call now sweeps past-due handshakes first. The
sweep cannot hang off the mutating paths alone: a `DescribeHandshake`
has to report `EXPIRED` too, and an `AcceptHandshake` must be refused
rather than reviving an offer that lapsed. A read-locked staleness
check keeps the write lock for the requests that actually have
something to expire, and it is self-limiting -- once swept, nothing is
stale.

Expiry runs through `resolve_handshake` like every other terminal
transition, so a responsibility transfer riding an expired handshake
ends with it instead of sitting REQUESTED with a live
`ActiveHandshakeId` pointing at a handshake that is gone. Its
`EndTimestamp` is the handshake's deadline, not the moment the sweep
noticed: `resolve_handshake` stamps "now", which is right for a decline
or a cancel because somebody acted then, but an idle process would
otherwise report a transfer ending days after the `ExpirationTimestamp`
on the same record.

The sweep is a mutation even when it happens on the way into a read, so
it is persisted there too -- otherwise the expiry is lost on restart
and the handshake comes back OPEN, past its deadline and acceptable
again. `/_fakecloud/organizations/responsibility-transfers` sweeps for
the same reason: it read the same object and answered `REQUESTED` with
a live `activeHandshakeId` while the API already said `EXPIRED`.

Tests: an overdue invitation reports EXPIRED and cannot be accepted or
enroll its target; an expired transfer ends at its deadline, drops its
handshake, agrees with introspection, and stops blocking a fresh offer;
an expiry noticed by a read reaches the snapshot store; and a handshake
inside its 15 days is left alone. Each behavioral assertion was checked
to fail with the fix reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EeYYZqG5anqYXR8761ZjWu
@vieiralucas
vieiralucas merged commit e2819ae into main Sep 24, 2026
157 checks passed
@vieiralucas
vieiralucas deleted the fix/handshake-expiry branch September 24, 2026 17:00
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