Repository navigation
feat: migrate credly badge poll to one sync unit per template - #5206
Conversation
PR SummaryMedium Risk Overview
Template resolution ( Safety/contract: Deploy note: the legacy org-wide unit must be paused before deploy (its channel is not a template id). Reviewed by Cursor Bugbot for commit f965b2b. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
🟡 Changes recommended
Non-atomic onboarding and unsafe cursor seeding can leave mappings inconsistent or permanently miss badges, while expiry processing and removed-template cleanup are also missing.
4 open issues
What changed in this PR
Moves Credly badge polling to one sync unit per project integration and badge template, aligning ingestion with the connector framework’s channel-based routing model and preparing independent backfills.
Changes:
- Scopes badge polling to a template and validates returned badges against that scope.
- Adds template onboarding with backfill watermarks, mapping freezes, and segment-aware dependencies.
- Removes the release queue and org-wide seed command, updates operational tools, and adds budget admission support.
| File | Description |
|---|---|
services/libs/data-access-layer/src/credly/releases.ts |
Removes release-queue data access. |
services/libs/data-access-layer/src/credly/releases.test.ts |
Removes obsolete release-queue tests. |
services/libs/data-access-layer/src/connectors/syncUnits.ts |
Adds channel-ID lookup and platform-scoped ownership checks. |
services/libs/data-access-layer/src/connectors/syncUnits.test.ts |
Tests lookup and updated ownership checks. |
services/libs/connectors/src/types.ts |
Adds dependency segment context and run-cost estimates. |
services/libs/connectors/src/pool/tokenPool.ts |
Exports the probe staleness threshold. |
services/libs/connectors/src/connectors/octolens/deps.test.ts |
Updates fixtures for segment context. |
services/libs/connectors/src/connectors/credly/syncs/badgePoll.ts |
Replaces org-wide routing with template-scoped polling. |
services/libs/connectors/src/connectors/credly/syncs/badgePoll.test.ts |
Adapts polling tests to single-template units. |
services/libs/connectors/src/connectors/credly/index.ts |
Registers budget support and removes fan-out declaration. |
services/libs/connectors/src/connectors/credly/discover.test.ts |
Verifies updated manifest capabilities. |
services/libs/connectors/src/connectors/credly/deps.ts |
Replaces routing and release dependencies with segment context. |
services/libs/connectors/src/connectors/credly/deps.test.ts |
Tests segment requirements and remaining data access. |
services/libs/connectors/src/connectors/credly/budget.ts |
Defines the fixed-window Credly budget probe. |
services/libs/connectors/src/connectors/credly/budget.test.ts |
Tests budget size and refresh timing. |
services/libs/connectors/src/connectors/credly/apiClient.ts |
Checks that filtered badges belong to the requested template. |
services/libs/connectors/src/connectors/credly/apiClient.test.ts |
Covers template-filter contract validation. |
services/apps/connectors_worker/src/onboardGithubRepo.ts |
Passes GitHub’s platform to ownership checks. |
services/apps/connectors_worker/src/onboardGithubRepo.test.ts |
Verifies the platform argument. |
services/apps/connectors_worker/src/onboardCredlyTemplate.ts |
Creates template polling units with ownership protection. |
services/apps/connectors_worker/src/onboardCredlyTemplate.test.ts |
Covers creation, ownership conflicts, and existing units. |
services/apps/connectors_worker/src/credlyTemplateResolve.ts |
Onboards mapped templates and seeds their cursors. |
services/apps/connectors_worker/src/credlyTemplateResolve.test.ts |
Adds decision-application and onboarding coverage. |
services/apps/connectors_worker/src/bin/seed-credly-sync-unit.ts |
Removes the org-wide seed command. |
services/apps/connectors_worker/src/bin/resolve-credly-templates.ts |
Arms backfills directly on template units. |
services/apps/connectors_worker/src/bin/dry-run-credly-badge-poll.ts |
Requires a template and destination for dry runs. |
services/apps/connectors_worker/src/activities/syncRunActivities.ts |
Passes the resolved segment into dependencies. |
services/apps/connectors_worker/src/activities/dispatcherActivities.ts |
Uses connector-specific admission cost estimates. |
services/apps/connectors_worker/src/activities/completenessActivities.ts |
Supplies segment context during validation. |
services/apps/connectors_worker/package.json |
Removes the obsolete seed script entry. |
docs/knowledge/sys-connectors.md |
Updates documented Credly table responsibilities. |
Review effort: Balanced
| } | ||
| return stored | ||
| }) | ||
| const outcome = await upsertCredlyTemplateDecision(qx, { ...decision, integrationId }) |
| if (onboarded.outcome === 'created' || onboarded.reason === 'already_present') { | ||
| await lockCredlyTemplateMapping(qx, decision.badgeTemplateId, { segmentId, integrationId }) |
| platform: PlatformType.CREDLY, | ||
| channelId: badgeTemplateId, | ||
| channelName: badgeTemplateName, | ||
| syncName: 'badgePoll', |
There was a problem hiding this comment.
🟡 Changes recommended
Non-atomic decision updates, onboarding, and mapping freezing allow concurrent resolvers to leave a live unit syncing one segment while its unfrozen decision points to another.
6 open issues
Make ownership decision, onboarding, and mapping freeze atomic Seed cursors at the visibility-lag boundary Migrate or remove seeding with organization IDs as template IDs · New Remove release modes that enqueue work no consumer processes · New Provision expiry processing during fresh setup Decommission units when a complete listing confirms template removal
Review effort: Balanced
6cabafd to
65e7324
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The mapping decision, unit creation, and lock commit separately, allowing concurrent runs or crashes to leave a live syncing unit with an unlocked mapping pointing to a different integration.
5 open issues
Make ownership decision, onboarding, and mapping freeze atomic Remove release modes that enqueue work no consumer processes Migrate or remove seeding with organization IDs as template IDs Provision expiry processing during fresh setup Decommission units when a complete listing confirms template removal
1 resolved since last review
Review effort: Balanced
Credly's single org-wide badgePoll unit, fanned out to segments at emit time via a routing table, serialized its entire backfill behind the shared dispatcher queue — the same bottleneck PR #5189 worked around instead of fixing. Credly's API filters by badge_template_id, so it can follow GitHub's pattern instead: one sync unit per (project integration, badge template), the channel itself the mapping. - runBadgePoll is scoped to one template (ctx.channel.channelId), fetched with the template filter and a contract assertion that every returned badge actually belongs to it (apiClient.ts). No more routing map, locking, or release-queue loop inside the sync. - CredlyDeps drops loadTemplateRouting/lockTemplate/the release-queue methods, gains segmentId — threaded through a new DepsFactoryInput field populated from the same resolveRunSegmentId call every run already makes. - onboardCredlyTemplate (new, modeled on onboardGithubRepo) onboards one sync unit per newly-mapped template from credlyTemplateResolve.ts, arming watermark.backfill from the template's creation date only on a genuine first creation. hasActiveSyncUnitsForChannelElsewhere gets a platform parameter so Credly can reuse it instead of copying it. - The template-mapping decision stays in credlyTemplateMappings exactly as before (the resolver's own record of why a template is mapped or held); it only stops being consulted for per-badge routing at run time, and is still the only thing creating or decommissioning units. - Restores the lock credlyTemplateMappings relied on to freeze a mapping once its unit is live, which the new onboarding path had dropped — found by review, otherwise a later re-decision could silently repoint the mapping while badges kept landing on the old, still-live unit. - Removes what the new architecture leaves with no real caller: the single org-wide seed-credly-sync-unit.ts script (its channel was the organization id, no longer a valid template filter) and the entire credlyTemplateReleases queue plus its CLI modes. --release is rewritten to arm a template's own live unit directly instead of queuing to a table nothing drains anymore. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
The dead-code removal (seed-credly-sync-unit.ts, the credlyTemplateReleases queue and its CLI modes) moves to its own PR, stacked on this one, so the per-template rewrite can be reviewed and tested on its own. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
A fresh template's backfill top and live cursor were seeded at raw now, skipping the WINDOW_LAG_MS margin every other window in badgePoll.ts respects. A badge Credly stamps just before onboarding but doesn't surface until shortly after fell before the new cursor and was never revisited. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
f87a0b4 to
9bc1cb3
Compare
A template mapped before per-template sync units existed was already fully covered by the retired org-wide poller, so this one-time script seeds its unit live-only (since: now - WINDOW_LAG_MS, no backfill) instead of re-walking years of history Credly has already given us once. The day-to-day resolver keeps using a full backfill for a template mapped for the first time, correctly, since it has no prior coverage to assume. Idempotent and dry-run by default: skips any template that already has a unit (including the 14 onboarded manually while testing #5206/#5207/#5211 today), and only --apply writes anything. Retiring the old org-wide unit stays a deliberate manual step, since its timing relative to the deploy matters and a script can't safely infer it. Also fixes findSyncUnitByChannelId to take a platform parameter, same as findSyncUnitByChannelName and the earlier hasActiveSyncUnitsForChannelElsewhere widening: this script is its second real caller, and without it two different platforms could collide on the same channelId under one integration. Restores the stale-mapping warning (integration deleted or moved) that the release-queue removal silently dropped. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9bc1cb3. Configure here.
A cross-template replacement is not malformed data, just out of scope for this channel's own unit. Throwing aborted the whole page before it was saved, and since the mismatch is deterministic every retry hit the same wall. Skip it like the existing not-found branch instead. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
A template mapped before per-template sync units existed was already fully covered by the retired org-wide poller, so this one-time script seeds its unit live-only (since: now - WINDOW_LAG_MS, no backfill) instead of re-walking years of history Credly has already given us once. The day-to-day resolver keeps using a full backfill for a template mapped for the first time, correctly, since it has no prior coverage to assume. Idempotent and dry-run by default: skips any template that already has a unit (including the 14 onboarded manually while testing #5206/#5207/#5211 today), and only --apply writes anything. Retiring the old org-wide unit stays a deliberate manual step, since its timing relative to the deploy matters and a script can't safely infer it. Also fixes findSyncUnitByChannelId to take a platform parameter, same as findSyncUnitByChannelName and the earlier hasActiveSyncUnitsForChannelElsewhere widening: this script is its second real caller, and without it two different platforms could collide on the same channelId under one integration. Restores the stale-mapping warning (integration deleted or moved) that the release-queue removal silently dropped. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Nothing in the codebase reads or writes this table anymore: the DAL (releases.ts) and its CLI modes (--release-unreleased, the drain loop) were removed in #5207 once badgePoll.ts moved to one sync unit per template, each backfilling itself. Do not merge/deploy this before the rest of the stack (#5206, #5207, #5211, #5212) is live in production: prod still runs the old code that reads this table until then, and dropping it first would break it. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
🟡 Changes recommended
Non-atomic decision updates, unit creation, and mapping freezes can leave live units emitting under incorrect mappings, while skipped cross-template renewals can be permanently lost.
4 open issues
2 resolved since last review
Review effort: Balanced
| { badgeId, replacementTemplateId: replacement.badge_template.id, badgeTemplateId }, | ||
| 'credly replacement badge belongs to a different template, skipping it here', | ||
| ) | ||
| continue |
A template mapped before per-template sync units existed was already fully covered by the retired org-wide poller, so this one-time script seeds its unit live-only (since: now - WINDOW_LAG_MS, no backfill) instead of re-walking years of history Credly has already given us once. The day-to-day resolver keeps using a full backfill for a template mapped for the first time, correctly, since it has no prior coverage to assume. Idempotent and dry-run by default: skips any template that already has a unit (including the 14 onboarded manually while testing #5206/#5207/#5211 today), and only --apply writes anything. Retiring the old org-wide unit stays a deliberate manual step, since its timing relative to the deploy matters and a script can't safely infer it. Also fixes findSyncUnitByChannelId to take a platform parameter, same as findSyncUnitByChannelName and the earlier hasActiveSyncUnitsForChannelElsewhere widening: this script is its second real caller, and without it two different platforms could collide on the same channelId under one integration. Restores the stale-mapping warning (integration deleted or moved) that the release-queue removal silently dropped. Refs linuxfoundation/lfx-cdp-insights-ops#1005 Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>



Second PR of the Credly per-template sync units migration (linuxfoundation/lfx-cdp-insights-ops#1005, part 2 of the plan after #5203). Collapses
runBadgePollfrom one org-wide unit fanning out at emit time into one sync unit per (project integration, badge template) — the actual fix for the serial-backfill bottleneck.What changes
runBadgePollis now scoped to a single template (ctx.channel.channelId), fetched withfilter[badge_template_id]and a new contract assertion (apiClient.ts) that every returned badge actually belongs to it, per the framework's "assert every returned record belongs to its channel" rule. The routing map, per-template locking and the release-queue loop inside the sync are gone — a unit only ever has one target.CredlyDepsdropsloadTemplateRouting/lockTemplate/the release-queue methods, gainssegmentId— threaded through a newDepsFactoryInput.segmentIdfield, populated from theresolveRunSegmentIdcall every run already makes (syncRunActivities.ts/completenessActivities.ts). No behavior change for other connectors.onboardCredlyTemplate(new, modeled ononboardGithubRepo): onboards one sync unit per newly-mappedtemplate fromcredlyTemplateResolve.ts'sapplyDecisions, armingwatermark.backfillfrom the template's creation date only on a genuine first creation (never resets an existing unit's progress).hasActiveSyncUnitsForChannelElsewheregets aplatformparameter so Credly can reuse it instead of copying it, per the README.credlyTemplateMappingskeeps its exact role as the resolver's own decision record (mapped/held/why) — it stops being read for per-badge routing at run time, and is now purely what creates sync units, matching the framework rule that a held queue may live outside the emit path as long as its only output is units created.seed-credly-sync-unit.ts(its channel was the organization id — no longer a valid template filter, would create a broken unit if run) and the entirecredlyTemplateReleasesqueue + DAL + its--release-unreleasedCLI mode (nothing drains it anymore).--release <id>is rewritten to arm a template's own live unit directly viamergeSyncUnitWatermarkinstead of queuing to a table nothing reads.Caught in review and fixed before this PR: a dropped lock (
lockCredlyTemplateMappingis now called right after a unit goes live, restoring the "freeze a mapping once it's syncing" invariant the oldlockBeforeEmitprovided) and a watermark gap (a freshly onboarded unit's live-poll cursor now starts exactly where its backfill'stopends, instead of defaulting to "now - 1 day" and potentially missing badges in between).Not fixed, tracked separately: the
credlyTemplateReleasestable itself is left in Postgres, unused — dropping it is a deliberate migration for a later PR, not bundled here. The admission-time budget race flagged on #5203 is unchanged by this PR; it becomes relevant once real concurrency exists (next PR), not before.Why
Investigated with Mouad why Credly can't parallelize its backfill like GitHub. GitHub's parallelism comes from one independent sync unit per repo x sync; Credly's API supports
filter[badge_template_id], so it can follow the same pattern. This is also required by the connector framework rules (services/libs/connectors/README.md, #5192): a mapping table driving runtime routing duplicates whatsync_unitsalready does, and fan-out is only for providers that can't filter by channel.Deployment note
The existing org-wide
badgePollunit'schannelIdis the organization id, not a template — once this code deploys, that unit would sendfilter[badge_template_id]=<organization id>to Credly. It needs to be paused before or at deploy time; the next PR (one-time migration: seed per-template units for all currently mapped templates, retire this unit) handles the cutover.Tests
onboardCredlyTemplate.test.ts(new): create path, owned-by-another-integration skip, already-present rename.badgePoll.test.ts: rewritten for the single-template scope (window/backfill/replacement-chain math preserved, routing/locking/release-loop tests removed as the behavior no longer exists), plus a new case for a replacement badge belonging to another template.apiClient.test.ts: new cases for the per-template contract assertion.credlyTemplateResolve.test.ts: newapplyDecisionscoverage (none existed before) for onboarding, locking, and the watermark it seeds.tsc -bclean;pnpm lint --deny-warningsclean. DB-backed integration tests (syncUnits.test.ts,credly/badges.test.ts,credly/index.test.ts) need a running Postgres, not available in this environment — unaffected by this diff, same pre-existing dependency as before.Refs linuxfoundation/lfx-cdp-insights-ops#1005
🤖 Generated with Claude Code