Skip to content

Commit e069802

Browse files
jordaneclaude
andcommitted
Scope committee_member.sfid forward mapping key by committee
The v2->v1 create path keyed the forward mapping on the v1 API MemberID (contact SFID) alone, which the same contact reuses across every committee it belongs to. Adding that contact to a second committee silently overwrote the first committee's forward mapping, orphaning it so a later v1-WAL delete couldn't find it. Scope the create-path key by committee SFID and update the delete path to look up the scoped key. Add a one-shot backfill to migrate existing flat keys to the scoped format and tombstone stale entries whose committee can no longer be resolved. Issue: LFXV2-2709 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Jordan Evans <jevans@linuxfoundation.org>
1 parent 099e6b6 commit e069802

6 files changed

Lines changed: 743 additions & 26 deletions

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ The following table shows the supported mapping key patterns and their expected
8484
| v1→v2 | `committee.sfid.{v1_sfid}` | `committee.sfid.123e4567-e89b-12d3-a456-426614174003` | `{v2_uuid}` | Committee SFID to UUID |
8585
| v2→v1 | `committee.uid.{v2_uuid}` | `committee.uid.123e4567-e89b-12d3-a456-426614174001` | `{project_sfid}:{committee_sfid}` | Committee UUID to compound SFID |
8686
| **Committee Members** |
87-
| v1→v2 | `committee_member.sfid.{v1_sfid}` | `committee_member.sfid.123e4567-e89b-12d3-a456-426614174004` | `{committee_uuid}:{member_uuid}` | Member SFID to compound UUID |
87+
| v1→v2 | `committee_member.sfid.{v1_sfid}` | `committee_member.sfid.123e4567-e89b-12d3-a456-426614174004` | `{committee_uuid}:{member_uuid}` | Member record SFID to compound UUID |
88+
| v1→v2 | `committee_member.sfid.{committee_sfid}.{member_sfid}` | `committee_member.sfid.a0941000002wBjEAAU.0034100000abcDEAAY` | `{committee_uuid}:{member_uuid}` | Member SFID to compound UUID, scoped by committee since the v1 API MemberID (contact SFID) is reused across committees for the same contact (LFXV2-2709) |
8889
| v2→v1 | `committee_member.uid.{v2_member_uuid}` | `committee_member.uid.123e4567-e89b-12d3-a456-426614174002` | `{project_sfid}:{committee_sfid}:{member_sfid}` | Member UUID to compound SFID |
8990

9091
### User SFID Lookup API
@@ -194,6 +195,49 @@ Locally (with NATS port-forwarded):
194195
lfx-v1-sync-helper --backfill-committee-member-mappings [--dry-run]
195196
```
196197

198+
**Committee-member forward-mapping migration (`--backfill-committee-member-forward-mappings`):**
199+
200+
Migrates committee-member forward mappings written by the v2→v1 create path
201+
(`committee_member.sfid.{v1_sfid}`, keyed only on the contact SFID) to the committee-scoped
202+
format `committee_member.sfid.{committee_sfid}.{member_sfid}` — the fix for LFXV2-2709, where an
203+
unscoped key let MemberID reuse across committees silently overwrite an earlier committee's
204+
mapping. Skips v1-ingest forward keys (record sfid, a UUID) untouched. Because the collision
205+
already collapsed colliding committees' entries into a single flat key before this fix shipped,
206+
only the surviving (last-write) entry per contact can be migrated — this is cleanup, not data
207+
recovery. Writes are optimistic-concurrency guarded against the live sync-helper, idempotent, and
208+
safe to re-run.
209+
210+
**Rollout note:** with the default single-replica `RollingUpdate` deployment, a new pod (writing
211+
the committee-scoped key) can briefly run alongside an old pod (whose delete handling only
212+
checks the pre-fix flat key), which can orphan a scoped mapping that this backfill's flat-key
213+
scan can't find. Deploy this release with the Deployment's `spec.strategy.type` temporarily set
214+
to `Recreate` (rather than the default `RollingUpdate`), so all old pods fully terminate before
215+
any new pod starts — this avoids old and new pods ever processing events concurrently, without
216+
needing an invalid `maxUnavailable`/`maxSurge` combination (Kubernetes rejects both set to zero,
217+
since a rollout can never make progress that way).
218+
219+
Run once after deploying the fix, to migrate already-affected mappings. A dry-run pass is
220+
recommended first:
221+
222+
```sh
223+
kubectl --context lfx-v2-prod -n v1-sync-helper apply -f manifests/backfill-committee-member-forward-mappings-job.yaml
224+
```
225+
226+
Add `--dry-run` to the manifest args, apply, inspect logs
227+
(`inspected`/`migrated`/`skipped_record_key`/`malformed`/`tombstoned`/`unresolved`/`conflicted`
228+
counts). A Job's pod template is immutable, so delete the dry-run Job before removing
229+
`--dry-run` and re-applying for the live run:
230+
231+
```sh
232+
kubectl --context lfx-v2-prod -n v1-sync-helper delete job backfill-committee-member-forward-mappings
233+
```
234+
235+
Locally (with NATS port-forwarded):
236+
237+
```sh
238+
lfx-v1-sync-helper --backfill-committee-member-forward-mappings [--dry-run]
239+
```
240+
197241
## Architecture Diagrams
198242

199243
Regarding the following sequence diagrams:

0 commit comments

Comments
 (0)