Skip to content

Commit 37ee83d

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 37ee83d

6 files changed

Lines changed: 762 additions & 27 deletions

README.md

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ Payload: <mapping_key>
7171

7272
### Available Lookup Patterns
7373

74-
**Note**: While called "sfid", v1 committees and committee members actually store UUIDs in their "sfid" column, so references to `{*_sfid}` for these entities will contain UUIDs.
74+
**Note**: While called "sfid", the v1 committee and committee-member *record* identifiers — used
75+
in the single-token `committee.sfid.{v1_sfid}` and `committee_member.sfid.{v1_sfid}` keys below —
76+
are actually UUIDs, not Salesforce-style IDs; the "sfid" naming is historical. This does **not**
77+
apply to the committee-scoped committee-member key further down: its two tokens
78+
(`{committee_sfid}`, `{member_sfid}`) are genuine v1 API Salesforce-style identifiers (the
79+
committee's own SFID and the contact "MemberID"), not UUIDs — see LFXV2-2709.
7580

7681
The following table shows the supported mapping key patterns and their expected response formats:
7782

@@ -84,7 +89,8 @@ The following table shows the supported mapping key patterns and their expected
8489
| v1→v2 | `committee.sfid.{v1_sfid}` | `committee.sfid.123e4567-e89b-12d3-a456-426614174003` | `{v2_uuid}` | Committee SFID to UUID |
8590
| v2→v1 | `committee.uid.{v2_uuid}` | `committee.uid.123e4567-e89b-12d3-a456-426614174001` | `{project_sfid}:{committee_sfid}` | Committee UUID to compound SFID |
8691
| **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 |
92+
| 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 |
93+
| 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) |
8894
| 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 |
8995

9096
### User SFID Lookup API
@@ -194,6 +200,62 @@ Locally (with NATS port-forwarded):
194200
lfx-v1-sync-helper --backfill-committee-member-mappings [--dry-run]
195201
```
196202

203+
**Committee-member forward-mapping migration (`--backfill-committee-member-forward-mappings`):**
204+
205+
Migrates committee-member forward mappings written by the v2→v1 create path
206+
(`committee_member.sfid.{v1_sfid}`, keyed only on the contact SFID) to the committee-scoped
207+
format `committee_member.sfid.{committee_sfid}.{member_sfid}` — the fix for LFXV2-2709, where an
208+
unscoped key let MemberID reuse across committees silently overwrite an earlier committee's
209+
mapping. Skips v1-ingest forward keys (record sfid, a UUID) untouched. Because the collision
210+
already collapsed colliding committees' entries into a single flat key before this fix shipped,
211+
only the surviving (last-write) entry per contact can be migrated — this is cleanup, not data
212+
recovery. Writes are optimistic-concurrency guarded against the live sync-helper, idempotent, and
213+
safe to re-run.
214+
215+
**Rollout note:** this release has two competing rollout hazards, and the operator must pick
216+
which to trade against the other:
217+
218+
- With the default single-replica `RollingUpdate` strategy, a new pod (writing the committee-
219+
scoped key) can briefly run alongside an old pod (whose delete handling only checks the pre-
220+
fix flat key), which can orphan a scoped mapping that this backfill's flat-key scan cannot
221+
find.
222+
- Temporarily switching the Deployment's `spec.strategy.type` to `Recreate` avoids that
223+
overlap, but creates a short window with no subscriber. Indexer events
224+
(`lfx.committee.*` / `lfx.committee_member.*`) are consumed via plain Core NATS
225+
`QueueSubscribe` (fire-and-forget, no replay), so any event published during that gap is
226+
silently dropped — a subsequent v2→v1 create/update/delete could be lost.
227+
228+
Neither option is strictly safer than the other; pick based on current traffic and how much
229+
each failure mode matters for the environment. The `Recreate` cutover is the recommended
230+
default when it can be performed during a low-traffic maintenance window, since dropping a
231+
handful of events during a short gap is easier to reconcile (via a follow-up sync or manual
232+
touch) than silently orphaned scoped mappings that no existing backfill can detect. In a
233+
non-quiescent environment, prefer the default `RollingUpdate` and accept the (narrow) overlap
234+
window instead. A durable JetStream consumer for these indexer subjects would close both
235+
hazards but is outside the scope of this change.
236+
237+
Run once after deploying the fix, to migrate already-affected mappings. A dry-run pass is
238+
recommended first:
239+
240+
```sh
241+
kubectl --context lfx-v2-prod -n v1-sync-helper apply -f manifests/backfill-committee-member-forward-mappings-job.yaml
242+
```
243+
244+
Add `--dry-run` to the manifest args, apply, inspect logs
245+
(`inspected`/`migrated`/`skipped_record_key`/`malformed`/`tombstoned`/`unresolved`/`conflicted`
246+
counts). A Job's pod template is immutable, so delete the dry-run Job before removing
247+
`--dry-run` and re-applying for the live run:
248+
249+
```sh
250+
kubectl --context lfx-v2-prod -n v1-sync-helper delete job backfill-committee-member-forward-mappings
251+
```
252+
253+
Locally (with NATS port-forwarded):
254+
255+
```sh
256+
lfx-v1-sync-helper --backfill-committee-member-forward-mappings [--dry-run]
257+
```
258+
197259
## Architecture Diagrams
198260

199261
Regarding the following sequence diagrams:

0 commit comments

Comments
 (0)