Skip to content

feat(npmjs): keep known versions in a DynamoDB table - #1941

Merged
mrgrain merged 1 commit into
mainfrom
mrgrain/feat/npmjs/known-versions-table
Oct 7, 2026
Merged

mrgrain merged 1 commit into
mainfrom
mrgrain/feat/npmjs/known-versions-table

Conversation

@mrgrain

@mrgrain mrgrain commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

The npm follower records every package version it has processed, so it doesn't stage the same version twice. Until now this list lived in a single JSON file in the staging bucket, about 20 MB and 285k versions in prod. Every run loaded the whole file and wrote the whole file back. That only works only with exactly one worker: two runs at once would each overwrite the other's changes. This is the first step towards letting several workers share the work, which we need to really fix the problems with laggy packuments.

Known versions now live in a DynamoDB table, keyed by package name and version, with the modified timestamp as the value. The follower only queries a package when at least one of its versions is a construct library, since nothing else is ever recorded. Writes are conditional, so the stored timestamp only moves forward, no matter which (of future multiple) writers saves first. Known versions are saved before the follower state after every chunk, so a crash can at worst re-stage a version, never skip one. The table is kept on stack deletion and has point-in-time recovery, because losing it means re-processing every construct version ever published. Billing defaults to on-demand and can be changed with the new knownVersionsTableBilling prop.

Migration

Existing deployments migrate on their own on the first run after the upgrade. If the marker object couchdb-known-versions.migrated is missing, the follower copies the known versions file (or, for older deployments, the legacy marker file) into the table, and writes the marker last. If the copy fails part-way, the next run simply does it again; nothing else happens in a run until the migration is done. A new deployment has no file to copy, so it writes the marker right away and fills the table during the normal backfill.

The old file is left in place and is no longer written. We keep it to mitigate the risk of a rollback. Deleting it would make the rolled-back code start from nothing and re-stage every version of every construct package that changes.
Keeping it means the old code just reads a slightly stale file and only re-stages the versions modified since the migration. The file can be deleted by hand once we are happy with the table.

Review guide

The main new logic is in known-versions.lambda-shared.ts (the table access) and known-versions-migration.lambda-shared.ts.staging-bucket.lambda-shared.ts only holds two S3 helpers moved out of the follower, so the follower and the migration can share them.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails
npm/@aws-sdk/client-dynamodb 3.1146.0 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 1Found 4/30 approved changesets -- score normalized to 1
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Security-Policy🟢 10security policy file detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/dynamodb-codec 3.973.46 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 1Found 4/30 approved changesets -- score normalized to 1
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Security-Policy🟢 10security policy file detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/endpoint-cache 3.972.11 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 1Found 4/30 approved changesets -- score normalized to 1
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Security-Policy🟢 10security policy file detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/@aws-sdk/middleware-endpoint-discovery 3.972.31 🟢 6.9
Details
CheckScoreReason
Code-Review⚠️ 1Found 4/30 approved changesets -- score normalized to 1
Maintained🟢 1030 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 5detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License🟢 10license file detected
Signed-Releases⚠️ 0Project has not signed or included provenance with any releases.
Security-Policy🟢 10security policy file detected
Branch-Protection🟢 5branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST🟢 10SAST tool is run on all commits
Binary-Artifacts🟢 9binaries present in source code
Pinned-Dependencies🟢 8dependency not pinned by hash detected -- score normalized to 8
Fuzzing🟢 10project is fuzzed
npm/mnemonist 0.38.3 🟢 3.2
Details
CheckScoreReason
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Packaging⚠️ -1packaging workflow not detected
Code-Review🟢 3Found 11/30 approved changesets -- score normalized to 3
Maintained⚠️ 12 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 1
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/obliterator 1.6.1 UnknownUnknown

Scanned Files

  • yarn.lock

@mrgrain
mrgrain disabled auto-merge October 6, 2026 13:38
@mrgrain
mrgrain force-pushed the mrgrain/feat/npmjs/known-versions-table branch from 8addb71 to 79af0f3 Compare October 6, 2026 13:48
Comment thread src/package-sources/npmjs.ts Outdated
Comment thread src/package-sources/npmjs.ts
billing: this.props.knownVersionsTableBilling ?? Billing.onDemand(),
encryption: TableEncryptionV2.dynamoOwnedKey(),
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
removalPolicy: RemovalPolicy.RETAIN,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But a new deployment is going to recreate a fresh table under a different name, no? Do we mean to give this table a fixed name as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong feelings. In practice this only happens with migrations and you'll need a plan to sort it out either way:

  • both random or fixed name can be solved with imports
  • a random name allows to create a new table and than copy the data somehow
  • a fixed name would prevent replacement, which might be desired

Comment thread src/package-sources/npmjs/known-versions-migration.lambda-shared.ts Outdated
Comment thread src/package-sources/npmjs/known-versions.lambda-shared.ts Outdated
Comment thread src/package-sources/npmjs/known-versions.lambda-shared.ts Outdated
Comment thread src/package-sources/npmjs/known-versions.lambda-shared.ts Outdated
Comment on lines +227 to +228
partitionKey: {
name: KnownVersionsAttribute.NAME,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are probably going to want another filter in the future:

                         fresh                                                              
         ┌───────────┐ sequence   ┌─────────┐         ┌───────────┐    fresh                
  feed   │   Feed    │  numbers   │         │         │ Packument │  versions       Doc     
────────▶│ Follower  │───────────▶│  Queue  │────────▶│ Retriever │───────────▶ processing  
         │           │            │         │         │           │                 ...     
         └───────────┘            └─────────┘         └───────────┘                         
               ▲                                            ▲                               
                                                            │                               
               │                                            │       RIGHT NOW:              
                                                            │       filter packument        
               │                                            └────── versions against        
                                                                    seen versions here      
               │                                                                            
                                                                     FUTURE:                
               └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ filter sequence        
                                                                     numbers here           

When that happens, we might as well use the same DynamoDB table for that scratch space, by leveraging a single table design.

What that effectively means is:

  • Mix the entity type into the primary key. So instead of "aws-cdk-lib", "@cdklabs/construct-hub" we will store something like "version:aws-cdk-lib" and "version:@cdklabs/construct-hub" and also "seq: 134844620" in that table.
  • Let go of the semantic meanings of key fields. The name field is not necessarily a "name" anymore, it's just a "primary key". The version field is not necessarily a "version" anymore, it's just a sort key.

The table looks like this:

pk sk modified
version:aws-cdk-lib 2.153.0 1791370179355
version:aws-cdk-lib 2.154.0 1791370178389
version:@cdklabs/construct-hub 1.3.0 1791370179383
seq:134844620
seq:134847364
version:@cdklabs/construct-hub 1.3.1 1791370203858

The table will just be "the" Npm follower deduplication table.

Alternative table schema

Or perhaps we will do the following schema for sequence numbers:

pk sk modified
seq 134844620
seq 134847364

Which would have the advantage that we can use Query to retrieve a whole bunch of sequence number records at the same time; otherwise we would do BatchGetItem. Both are feasible, we need to figure a bit which one is better.

@mrgrain mrgrain Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any relationship between versions and sequence numbers, which is the typical case for a single table design (as opposed to your relational database normalized tables). So yes, I was planning on two tables here.

We could treat this as a generic work-log of sorts, but I am not sure what we are gaining from it.

Comment thread src/package-sources/npmjs/npm-js-follower.lambda.ts Outdated
Comment thread src/package-sources/npmjs/npm-js-follower.lambda.ts Outdated
@mrgrain
mrgrain force-pushed the mrgrain/feat/npmjs/known-versions-table branch 2 times, most recently from 378c7ca to 1ff5fc3 Compare October 7, 2026 13:10
The follower kept the package versions it has processed in a single JSON
file in S3. Every run loaded all of it and wrote all of it back, so only
one worker could ever use it.

Known versions now live in a DynamoDB table, keyed by package name and
version. Writes are conditional, so the stored modified timestamp only
moves forward. The table billing mode is configurable through the new
knownVersionsTableBilling prop and defaults to on-demand.

Existing deployments migrate automatically: when the migration marker
object is missing, the first run copies the known versions file (or the
legacy marker file) into the table and then writes the marker. The old
file is left in place and no longer updated, so a rollback only
re-stages versions modified since the migration.
@mrgrain
mrgrain force-pushed the mrgrain/feat/npmjs/known-versions-table branch from eafb0bf to bb4c939 Compare October 7, 2026 13:19
@mrgrain
mrgrain added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 7aa8b77 Oct 7, 2026
8 checks passed
@mrgrain
mrgrain deleted the mrgrain/feat/npmjs/known-versions-table branch October 7, 2026 13:40
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.

2 participants