Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Atomic migrations #142

Merged
merged 9 commits into from
Dec 21, 2024
Merged

Atomic migrations #142

merged 9 commits into from
Dec 21, 2024

Conversation

robertherber
Copy link
Member

@robertherber robertherber commented Dec 5, 2024

To think about:

  • A way to handle retries? Use queues for this? Currently it will get stuck and require manual modifications in the database.
  • Should runOnStart in combination with waitForFinish really be the defaults? At least the duplicate error should not stop the server from starting, since it's expected when running multiple instances.
  • Are the errors exposed in the right way - so we can make the error property visible in for example Sentry in the event a migration fails.
  • Make sure that we don't run the next migration if the previous one hasn't finished in the correct state.

Summary by CodeRabbit

  • New Features

    • Introduced atomic migrations for improved data integrity.
    • Added a new error class for handling migration lock errors.
    • Enhanced migration functionality with improved error handling and logging.
    • Implemented lock handling mechanisms for concurrent migrations.
  • Bug Fixes

    • Improved error reporting for migration operations.
  • Tests

    • Added comprehensive test coverage for migration functionality, including success, failure, and concurrency scenarios.
  • Chores

    • Set up a testing environment for MongoDB-related tests, including setup and teardown functions.

Copy link

changeset-bot bot commented Dec 5, 2024

🦋 Changeset detected

Latest commit: c5fac73

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@zemble/migrations Patch
@zemble/mongodb Patch
minimal Patch
@zemble/cms-users Patch
@zemble/cms Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@robertherber robertherber self-assigned this Dec 5, 2024
@robertherber robertherber requested a review from Ryiguchi December 5, 2024 13:21
Copy link
Contributor

@Ryiguchi Ryiguchi left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link

coderabbitai bot commented Dec 21, 2024

Walkthrough

The pull request introduces enhancements to the migration system for the MongoDB package, focusing on implementing atomic migrations and robust error handling. Key changes include the addition of a new MigrationLockError, improvements to migration locking mechanisms, and comprehensive test coverage for various migration scenarios. These modifications ensure data integrity during concurrent migration processes by managing potential conflicts effectively.

Changes

File Change Summary
.changeset/thin-coins-wink.md Patch added for @zemble/mongodb package to improve migration atomicity
packages/migrations/MigrationLockError.ts New error class introduced for migration lock-related errors
packages/migrations/plugin.test.ts Added new test cases for migrateUp and beforeStart methods
packages/migrations/plugin.ts Enhanced migration error handling and control flow
packages/mongodb/migration-adapter-with-transactions.ts Improved error handling and collection management
packages/mongodb/migration-adapter.ts Added locking mechanisms and collection management functions
packages/mongodb/migration.test.ts New test suite for migration adapter functionality
packages/mongodb/test-setup.ts Added test environment setup and teardown utilities
.changeset/fifty-poems-joke.md Patch added for @zemble/migrations package to enhance lock handling

Sequence Diagram

sequenceDiagram
    participant Client
    participant MigrationPlugin
    participant MigrationAdapter
    participant Database

    Client->>MigrationPlugin: Initiate Migration
    MigrationPlugin->>MigrationAdapter: Attempt Lock
    MigrationAdapter->>Database: Check Existing Migrations
    alt Lock Successful
        MigrationAdapter->>Database: Apply Migration
        Database-->>MigrationAdapter: Migration Completed
        MigrationAdapter-->>MigrationPlugin: Migration Success
    else Lock Failed
        MigrationAdapter-->>MigrationPlugin: Migration Locked Error
    end
Loading

Poem

🐰 Migrations dance, atomic and bright,
Locks prevent chaos with all of their might.
No data shall scatter, no schema shall break,
Our rabbit-quick changes, precision we'll make!
MongoDB trembles, migrations take flight! 🔒✨


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

pkg-pr-new bot commented Dec 21, 2024

Open in Stackblitz

@zemble/auth-anonymous

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/auth-anonymous@142

@zemble/auth-api-token

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/auth-api-token@142

@zemble/auth

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/auth@142

@zemble/auth-apple

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/auth-apple@142

@zemble/auth-otp

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/auth-otp@142

@zemble/bull

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/bull@142

@zemble/bun

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/bun@142

@zemble/core

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/core@142

create-zemble-app

npm i https://pkg.pr.new/kingstinct/zemble/create-zemble-app@142

create-zemble-plugin

npm i https://pkg.pr.new/kingstinct/zemble/create-zemble-plugin@142

@zemble/debug

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/debug@142

@zemble/email-resend

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/email-resend@142

@zemble/email-sendgrid

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/email-sendgrid@142

eslint-config-kingstinct

npm i https://pkg.pr.new/kingstinct/zemble/eslint-config-kingstinct@142

@zemble/firebase-auth

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/firebase-auth@142

@zemble/graphql

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/graphql@142

@zemble/kv

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/kv@142

@zemble/migrations

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/migrations@142

@zemble/mongodb

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/mongodb@142

@zemble/node

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/node@142

@zemble/pino

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/pino@142

@zemble/push-apple

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/push-apple@142

@zemble/push-expo

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/push-expo@142

@zemble/routes

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/routes@142

@zemble/react

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/react@142

@zemble/sms-twilio

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/sms-twilio@142

@zemble/sms-46elks

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/sms-46elks@142

@zemble/supabase

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/supabase@142

@zemble/utils

npm i https://pkg.pr.new/kingstinct/zemble/@zemble/utils@142

commit: c5fac73

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🔭 Outside diff range comments (1)
packages/mongodb/migration-adapter-with-transactions.ts (1)

Missing retry mechanism in migration implementation

After reviewing both the migration adapter implementations, there is no retry mechanism present to prevent stuck migrations. The code only implements:

  • Lock acquisition through acquireUpLock and acquireDownLock
  • Transaction handling in migration-adapter-with-transactions.ts
  • Error recording in the migrations collection

The current implementation will:

  1. Throw a MigrationLockError if a migration is already running
  2. Record errors in the database if a migration fails
  3. Not automatically retry failed migrations
🔗 Analysis chain

Line range hint 28-55: Verify retry mechanism implementation

The PR objectives mention implementing a retry mechanism to prevent stuck migrations, but this implementation doesn't show retry logic.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for retry-related implementations
rg -l 'retry|backoff|attempt' --type typescript

# Search for potential retry configurations
ast-grep --pattern 'const $CONFIG = {
  $$$
  retry: $_,
  $$$
}'

Length of output: 152


Script:

#!/bin/bash
# Search for retry-related implementations with ripgrep, using ts extension
rg -l 'retry|backoff|attempt' -g '*.ts'

# Search for retry configurations in TypeScript files
ast-grep --pattern 'interface $_ {
  $$$
  retry?: $_
  $$$
}'

# Look for retry-related function implementations
ast-grep --pattern 'function $_($$_) {
  $$$
  retry
  $$$
}'

# Check the specific migration adapter file content
cat packages/mongodb/migration-adapter-with-transactions.ts

Length of output: 3475


Script:

#!/bin/bash
# Let's check the migration-adapter.ts file for any retry logic in the lock acquisition
cat packages/mongodb/migration-adapter.ts

# Also check if there are any retry-related configurations in the core migration files
rg -g '*.ts' -A 5 'interface.*Migration' packages/core/
rg -g '*.ts' -A 5 'type.*Migration' packages/core/

Length of output: 4206

🧹 Nitpick comments (15)
packages/mongodb/migration-adapter.ts (5)

16-16: Consider creating the index in the background.
Creating a unique index can briefly lock the collection if not done in the background. Consider specifying "background: true" (for MongoDB versions <4.2) or "createIndexes" with "background: true" to avoid blocking writes during index creation.


22-40: Robust concurrency handling with acquireUpLock, but consider explicit logging for all error branches.
While the logic handles duplicate key errors, it might be helpful to add logs for other unexpected errors to aid debugging.


42-63: acquireDownLock concurrency approach looks consistent, but watch for partial migrations.
If the "down" migration partially applies changes before the lock error occurs, ensure you have a recovery plan in place. Also, consider consistent instrumentation/logging for easier troubleshooting.


Line range hint 70-83: Error storage is helpful, but consider more structured error data.
Storing the error message as JSON is good, yet storing structured fields (error name, stack, etc.) could improve querying in the database.


115-116: Status retrieval is straightforward, but watch out for large data sets.
If the number of entries grows significantly over time, consider pagination or capping.

packages/migrations/plugin.ts (3)

129-130: Remove or replace temporary console logging.
This violates the no-console lint rule and should be replaced with the configured logger.

- console.log('HEREERERE')
+ opts?.logger?.debug?.('HEREERERE')
🧰 Tools
🪛 eslint

[error] 129-129: Unexpected console statement.

(no-console)


146-154: Progress callback usage is a neat feature.
Reporting progress via adapter-specific logic helps track partial migration results. Make sure it’s tested for large data migrations or concurrency scenarios.


159-165: Catching MigrationLockError to skip the migration is a good approach.
Prevents repeated attempts, but ensure logs or alerts so you’re aware if migrations skip frequently.

packages/migrations/plugin.test.ts (1)

22-28: Verify migration behavior during startup

The test verifies that migrateUp isn't called, but consider adding tests for:

  • Migration behavior when runOnStart is true
  • Handling of duplicate migrations during startup
 test('beforeStart', async () => {
   const migrateUpMock = mock(migrateUp)
   const app = await createTestApp(plugin)

   await runBeforeServe(app)
   expect(migrateUpMock).toHaveBeenCalledTimes(0)
+
+  // Test with runOnStart enabled
+  const appWithRunOnStart = await createTestApp({
+    ...plugin,
+    config: { runOnStart: true }
+  })
+  await runBeforeServe(appWithRunOnStart)
+  expect(migrateUpMock).toHaveBeenCalledTimes(1)
 })
packages/mongodb/test-setup.ts (1)

26-28: Add error handling to collection cleanup

The tearDownAfterEach function should handle potential failures during collection cleanup.

 export const tearDownAfterEach = async () => {
-  await emptyAllCollections()
+  try {
+    await emptyAllCollections()
+  } catch (error) {
+    console.error('Failed to clean up collections:', error)
+    throw error
+  }
 }
packages/mongodb/migration.test.ts (2)

16-37: Add validation for migration record timestamps

The test should verify the chronological order and validity of timestamps.

   expect(migrations?.[0]).toStrictEqual(
     {
       name: 'testing',
       startedAt: expect.any(Date),
       _id: expect.any(ObjectId),
       completedAt: expect.any(Date),
       error: null,
     },
   )
+  // Verify timestamp order
+  const { startedAt, completedAt } = migrations[0]
+  expect(startedAt.getTime()).toBeLessThan(completedAt.getTime())
+  expect(startedAt.getTime()).toBeLessThan(Date.now())

1-93: Consider adding network failure scenarios

The test suite would benefit from testing network-related edge cases:

  • MongoDB connection drops during migration
  • Replica set primary changes
  • Write concern timeout

Would you like me to provide example test cases for these scenarios?

packages/mongodb/migration-adapter-with-transactions.ts (3)

46-54: Consider standardizing error objects for better monitoring

While the error handling is improved, consider structuring the error object to be more Sentry-friendly:

   $set: {
     name,
-    error: e instanceof Error ? e.message : JSON.stringify(e),
+    error: {
+      message: e instanceof Error ? e.message : JSON.stringify(e),
+      stack: e instanceof Error ? e.stack : undefined,
+      type: e.constructor.name,
+    },
     erroredAt: new Date(),
   },

87-88: Consider implementing pagination for status retrieval

For databases with many migrations, retrieving all records at once could be inefficient. Consider implementing pagination:

-  const res = await collection.find().toArray()
+  const res = await collection.find()
+    .sort({ completedAt: -1 })
+    .limit(1000)  // or accept as parameter
+    .toArray()

93-94: Consider atomic progress updates

Progress updates should be atomic to prevent race conditions. Consider using findOneAndUpdate with optimistic locking:

   await collection.findOneAndUpdate({
     name: migrationStatus.name,
+    erroredAt: { $exists: false }  // Only update if not errored
   }, {
     $set: {
       name: migrationStatus.name,
       progress: migrationStatus.progress,
     },
   }, { upsert: true })
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e24bfe and 4c0a140.

📒 Files selected for processing (8)
  • .changeset/thin-coins-wink.md (1 hunks)
  • packages/migrations/MigrationLockError.ts (1 hunks)
  • packages/migrations/plugin.test.ts (1 hunks)
  • packages/migrations/plugin.ts (3 hunks)
  • packages/mongodb/migration-adapter-with-transactions.ts (3 hunks)
  • packages/mongodb/migration-adapter.ts (4 hunks)
  • packages/mongodb/migration.test.ts (1 hunks)
  • packages/mongodb/test-setup.ts (1 hunks)
🧰 Additional context used
🪛 eslint
packages/migrations/plugin.ts

[error] 129-129: Unexpected console statement.

(no-console)

🔇 Additional comments (14)
packages/mongodb/migration-adapter.ts (5)

2-2: Import of MigrationLockError is appropriate.
This error class helps differentiate lock conflicts from other database errors, maintaining code clarity and enabling specific handling logic.


68-69: Neat usage of getCollection for each migration run.
Centralizing the collection retrieval fosters consistency and reduces duplication.


95-101: Clean approach to “down” migration completion.
Deleting the migration document upon successful down ensures a clean reset. Great for maintaining clarity of completed migrations.


103-103: Persisting error details in down migration is consistent.
Matches the approach in the up migration. This symmetrical handling is beneficial for debugging.


121-122: Storing progress updates is a valuable feature.
Great for tracking intermediate states. Ensure that partial migration states are thoroughly tested.

packages/migrations/plugin.ts (5)

5-5: Importing MigrationLockError is consistent with the new locking design.
This ensures the plugin can properly handle lock-related errors.


133-139: Graceful handling of cancelledBecauseOfLock.
By halting further migrations when a lock is encountered, the code avoids collisions in a multi-instance environment.


155-158: Returning success is straightforward.
The return object structure is clear for future expansions (e.g., storing details about migrations applied).


167-168: Re-throwing other errors ensures they surface properly.
This pattern avoids swallowing unexpected exceptions.


171-172: Enforcing the presence of an up function.
This quick fail is beneficial for preventing partial or undefined migrations.

.changeset/thin-coins-wink.md (1)

1-5: Descriptive changeset message.
Provides context for future reference when reviewing the version history of @zemble/mongodb.

packages/migrations/MigrationLockError.ts (1)

1-6: New MigrationLockError class is properly structured.
Extending Error with a named constructor clarifies its intent. Make sure to capture and log the stack when throwing.

packages/mongodb/migration-adapter-with-transactions.ts (2)

2-2: LGTM! Import changes support atomic migrations

The addition of lock-related imports aligns with the PR objectives for implementing atomic migrations and preventing concurrent execution.


28-30: LGTM! Atomic migration implementation with proper locking

The lock acquisition before starting the transaction ensures atomic migrations and prevents concurrent execution.

Comment on lines +214 to +236
const { cancelledBecauseOfLock } = await completer
if (cancelledBecauseOfLock) {
const hasMigrationsCompleted = async () => {
const migrations = await getMigrations(migrationsPathOfApp, await config?.createAdapter?.(app))
const migrationsToProcess = migrations.filter((migration) => !migration.isMigrated)
if (migrationsToProcess.length === 0) {
return true
}
logger.info({ migrationsToProcess }, `Waiting for ${migrationsToProcess.length} migrations to complete...`)
return false
}

const waitForMigrationsToComplete = async () => {
const hasCompleted = await hasMigrationsCompleted()
if (!hasCompleted) {
await new Promise((resolve) => { setTimeout(resolve, 1000) })
return waitForMigrationsToComplete()
}
return undefined
}

return waitForMigrationsToComplete()
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Retry mechanism for migrations that are cancelled because of a lock.
The recursive polling approach is straightforward. Consider adding a maximum wait time or exponential backoff if migrations remain locked for extended periods.

Comment on lines +14 to +20
test('migrateUp', async () => {
await createTestApp(plugin)
const { count } = await migrateUp({

})
expect(count).toBe(0)
})
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance test coverage for migrateUp

The current test only verifies the count is 0 but doesn't test the actual migration functionality. Consider adding test cases for:

  • Successful migration with actual changes
  • Error handling scenarios
  • Retry mechanism behavior
 test('migrateUp', async () => {
   await createTestApp(plugin)
-  const { count } = await migrateUp({
-
-  })
-  expect(count).toBe(0)
+  // Test successful migration
+  const { count: successCount } = await migrateUp({
+    migrations: [{
+      name: 'test-migration',
+      up: async () => { /* test migration logic */ }
+    }]
+  })
+  expect(successCount).toBe(1)
+
+  // Test error handling
+  await expect(migrateUp({
+    migrations: [{
+      name: 'failing-migration',
+      up: async () => { throw new Error('Migration failed') }
+    }]
+  })).rejects.toThrow('Migration failed')
 })

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +8 to +18
export const setupBeforeAll = async () => {
const { privateKey, publicKey } = await generateKeys()

setupEnvOverride({ PUBLIC_KEY: publicKey, PRIVATE_KEY: privateKey })

await startInMemoryInstanceAndConfigurePlugin({
replicaSetOptions: {},
})

await createTestApp(plugin)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and connection validation to setup

The setup function should handle potential failures and verify the MongoDB connection is successful.

 export const setupBeforeAll = async () => {
+  try {
     const { privateKey, publicKey } = await generateKeys()
 
     setupEnvOverride({ PUBLIC_KEY: publicKey, PRIVATE_KEY: privateKey })
 
     await startInMemoryInstanceAndConfigurePlugin({
       replicaSetOptions: {},
     })
 
+    // Verify MongoDB connection
+    const app = await createTestApp(plugin)
+    const isConnected = await app.providers.mongodb?.client.db().command({ ping: 1 })
+    if (!isConnected) {
+      throw new Error('MongoDB connection failed')
+    }
+
     await createTestApp(plugin)
+  } catch (error) {
+    console.error('Test setup failed:', error)
+    throw error
+  }
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +62 to +93
test('run concurrent migrations', async () => {
const mongodbPlugin = plugin
const adapter = MongoMigrationAdapterWithTransaction({
providers: mongodbPlugin.providers,
collectionName: 'migrations',
})
const migrationRunner = mock(async () => {
await new Promise((resolve) => { setTimeout(resolve, 100) })
})

const migration1 = adapter.up('testing', migrationRunner)

const migration2 = adapter.up('testing', migrationRunner)

const migration3 = adapter.up('testing', migrationRunner)

await Promise.allSettled([migration1, migration2, migration3])

const migrations = await mongodbPlugin.providers.mongodb?.db.collection('migrations').find().toArray()

expect(migrations).toHaveLength(1)
expect(migrationRunner).toHaveBeenCalledTimes(1)
expect(migrations?.[0]).toStrictEqual(
{
name: 'testing',
startedAt: expect.any(Date),
_id: expect.any(ObjectId),
completedAt: expect.any(Date),
error: null,
},
)
})
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance concurrent migration testing

The concurrent migration test could be more robust by:

  1. Testing with different delays
  2. Verifying lock acquisition
  3. Testing timeout scenarios
 test('run concurrent migrations', async () => {
   const mongodbPlugin = plugin
   const adapter = MongoMigrationAdapterWithTransaction({
     providers: mongodbPlugin.providers,
     collectionName: 'migrations',
   })
-  const migrationRunner = mock(async () => {
-    await new Promise((resolve) => { setTimeout(resolve, 100) })
-  })
+  const delays = [100, 50, 150] // Different delays to test various scenarios
+  const migrationRunners = delays.map(delay => mock(async () => {
+    await new Promise((resolve) => { setTimeout(resolve, delay) })
+  }))

-  const migration1 = adapter.up('testing', migrationRunner)
-  const migration2 = adapter.up('testing', migrationRunner)
-  const migration3 = adapter.up('testing', migrationRunner)
+  const migrations = migrationRunners.map(runner =>
+    adapter.up('testing', runner)
+  )

-  await Promise.allSettled([migration1, migration2, migration3])
+  const results = await Promise.allSettled(migrations)

+  // Verify only one succeeded and others were rejected with lock error
+  const succeeded = results.filter(r => r.status === 'fulfilled')
+  const failed = results.filter(r => r.status === 'rejected')
+  expect(succeeded).toHaveLength(1)
+  expect(failed).toHaveLength(2)
+  failed.forEach(result => {
+    expect(result.status === 'rejected' && result.reason.message)
+      .toContain('Migration lock')
+  })

   const migrations = await mongodbPlugin.providers.mongodb?.db.collection('migrations').find().toArray()

   expect(migrations).toHaveLength(1)
-  expect(migrationRunner).toHaveBeenCalledTimes(1)
+  // Verify only one runner was called
+  const totalCalls = migrationRunners.reduce((sum, runner) =>
+    sum + runner.mock.calls.length, 0)
+  expect(totalCalls).toBe(1)
 })

+test('migration timeout', async () => {
+  const adapter = MongoMigrationAdapterWithTransaction({
+    providers: plugin.providers,
+    collectionName: 'migrations',
+    lockTimeout: 100 // Short timeout for testing
+  })
+
+  const longRunningMigration = adapter.up('timeout-test', async () => {
+    await new Promise(resolve => setTimeout(resolve, 200))
+  })
+
+  await expect(longRunningMigration).rejects.toThrow('Migration timeout')
+})

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +72 to +81
await collection.findOneAndUpdate({
name,
}, {
$set: {
name,
error: JSON.stringify(e),
error: e instanceof Error ? e.message : JSON.stringify(e),
erroredAt: new Date(),
},
}, { upsert: true })
throw e
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Error update should use the session

The error update operation is not using the session, which could lead to inconsistency if the transaction is rolled back:

   await collection.findOneAndUpdate({
     name,
   }, {
     $set: {
       name,
       error: e instanceof Error ? e.message : JSON.stringify(e),
       erroredAt: new Date(),
     },
-  }, { upsert: true })
+  }, { upsert: true, session })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await collection.findOneAndUpdate({
name,
}, {
$set: {
name,
error: JSON.stringify(e),
error: e instanceof Error ? e.message : JSON.stringify(e),
erroredAt: new Date(),
},
}, { upsert: true })
throw e
await collection.findOneAndUpdate({
name,
}, {
$set: {
name,
error: e instanceof Error ? e.message : JSON.stringify(e),
erroredAt: new Date(),
},
}, { upsert: true, session })
throw e

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
.changeset/fifty-poems-joke.md (1)

4-5: Enhance the changeset description to better reflect the changes.

The current description "Add lock handling" could be more descriptive. Consider expanding it to better capture the scope and impact of the changes.

-Add lock handling
+Add atomic migration handling with locks
+
+- Implement migration locking mechanism to prevent concurrent migrations
+- Add MigrationLockError for better error handling
+- Prevent migrations from getting stuck by implementing retry mechanism
+- Improve error visibility for monitoring
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c0a140 and c5fac73.

📒 Files selected for processing (1)
  • .changeset/fifty-poems-joke.md (1 hunks)

@robertherber robertherber merged commit bca9796 into main Dec 21, 2024
10 checks passed
@robertherber robertherber deleted the atomic-migrations branch December 21, 2024 02:55
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