Skip to content

feat: separate process to do initial syncing#1065

Merged
chedieck merged 6 commits intomasterfrom
feat/separate-sync-process
Oct 20, 2025
Merged

feat: separate process to do initial syncing#1065
chedieck merged 6 commits intomasterfrom
feat/separate-sync-process

Conversation

@chedieck
Copy link
Collaborator

@chedieck chedieck commented Oct 15, 2025

Description

Removes the initial syncing of missing txs from the main thread, puts it on the bullmq worker thread.

This allows for better paralellization and I hope that will lead to the server being more stable during this initial syncing.

Test plan

Everything should work as before. To check the jobs logs, one could examine logs/jobs.log or yarn docker jl

Summary by CodeRabbit

  • New Features

    • Adds a background job that syncs missed blockchain transactions and links them to pricing after startup, improving data completeness.
    • Initializes blockchain clients for multiple networks in parallel and supports clean shutdown of those clients for more reliable multi-network operation.
  • Chores

    • Adjusts container shell shortcuts to improve shell invocation reliability.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 15, 2025

Walkthrough

Creates and enqueues a post-price-sync BullMQ job to sync missed blockchain transactions and attach prices, adds a worker to run that job and destroy the multi-chain client, always instantiates Chronik clients with new sync/destroy APIs, and tweaks three docker shell shortcut invocations.

Changes

Cohort / File(s) Summary of Edits
Job orchestration & worker
jobs/initJobs.ts, jobs/workers.ts
initJobs.ts creates a blockchainSync BullMQ queue, enqueues a syncBlockchainAndPrices job after price-sync setup, and starts the worker with the queue name. workers.ts adds export const syncBlockchainAndPricesWorker(queueName: string): Promise<void> which runs multiBlockchainClient.syncMissedTransactions() then connectAllTransactionsToPrices(), updates logging in syncCurrentPricesWorker, and destroys the global multiBlockchainClient on completion; failure paths log job id and error.
Chronik client lifecycle & sync APIs
services/chronikService.ts
Always instantiate Chronik clients for both networks (ecash and bitcoincash), await setup promises, remove pre-init price-sync calls, add syncMissedTransactions() on MultiBlockchainClient, and add destroy() on MultiBlockchainClient to close clients/WS endpoints; per-network instantiation completion logged.
Docker exec shortcuts
scripts/docker-exec-shortcuts.sh
For databaseshell/dbs, nodeshell/ns, and rootnodeshell/rns, changed invocation from bash -l to bash -c bash -l; behavior otherwise unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Init as InitJobs
  participant Queue as "BullMQ\n(blockchainSync)"
  participant Worker as "syncBlockchainAndPricesWorker"
  participant MBC as MultiBlockchainClient
  participant TxSvc as TransactionService
  participant ChronikE as Chronik(ecash)
  participant ChronikB as Chronik(bitcoincash)

  Init->>Init: start syncCurrentPrices setup
  Note over Init: after initial price sync is started
  Init->>Queue: create queue & enqueue sync job
  Queue-->>Worker: deliver job (queueName)

  Worker->>Worker: log start (job id)
  Worker->>MBC: syncMissedTransactions()
  par per-network
    MBC->>ChronikE: sync missed transactions
    MBC->>ChronikB: sync missed transactions
  end
  Worker->>TxSvc: connectAllTransactionsToPrices()
  Worker->>MBC: destroy()
  Worker->>Worker: log completion
  alt failure
    Worker->>Worker: log error (job id + message)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I twitch my whiskers, queues awake—
Missed blocks found in moonlit wake.
Two chains hum, connectors mend,
Workers hop until the end.
I stash the crumbs and dance—sync done. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat: separate process to do initial syncing" directly and clearly describes the main objective of the changeset. The raw_summary confirms that the primary change involves moving the initial syncing of missing transactions from the main thread to a separate BullMQ worker thread by creating a new syncBlockchainAndPricesWorker that executes after the prices synchronization. The title is concise, specific, and accurately reflects the core feature being introduced without being misleading or overly vague.
Description Check ✅ Passed The pull request description contains the two main required sections from the template: a substantive Description that explains the change (moving initial syncing from main thread to BullMQ worker for better parallelization and stability) and a Test plan that provides clear verification instructions. The "Related to #" field is left blank without an issue reference, which is a minor incompleteness, but this does not significantly impact the overall quality since the core descriptive content is present and informative.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/separate-sync-process

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/chronikService.ts (1)

1000-1021: Add explicit close/teardown for websockets and sockets

Provide a proper cleanup API and use it from the worker.

Add to ChronikBlockchainClient:

 export class ChronikBlockchainClient {
@@
   wsEndpoint!: Socket
@@
   private latencyTestFinished: boolean
@@
   constructor (networkSlug: string) {
@@
   }
+
+  public async close (): Promise<void> {
+    try { this.chronikWSEndpoint?.close() } catch {}
+    try { this.wsEndpoint?.close() } catch {}
+  }

Add to MultiBlockchainClient:

 class MultiBlockchainClient {
@@
   public async syncAndSubscribeAddresses (addresses: Address[]): Promise<SyncAndSubscriptionReturn> {
@@
   }
+
+  public async close (): Promise<void> {
+    await Promise.allSettled([
+      this.clients.ecash.close(),
+      this.clients.bitcoincash.close()
+    ])
+  }

Then call await multiBlockchainClient.close() from the worker (see workers.ts comment).

🧹 Nitpick comments (1)
jobs/initJobs.ts (1)

24-26: One-shot blockchain sync step looks good; consider housekeeping options

Set job options to avoid queue growth and define retry/backoff:

-  await blockchainQueue.add('syncBlockchainAndPrices', {}, { jobId: 'syncBlockchainAndPrices' })
+  await blockchainQueue.add('syncBlockchainAndPrices', {}, {
+    jobId: 'syncBlockchainAndPrices',
+    removeOnComplete: true,
+    attempts: 3,
+    backoff: { type: 'exponential', delay: 1000 }
+  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55e10cb and ee0b6ee.

📒 Files selected for processing (4)
  • jobs/initJobs.ts (2 hunks)
  • jobs/workers.ts (2 hunks)
  • scripts/docker-exec-shortcuts.sh (2 hunks)
  • services/chronikService.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
jobs/workers.ts (4)
services/chronikService.ts (1)
  • multiBlockchainClient (1101-1101)
services/transactionService.ts (1)
  • connectAllTransactionsToPrices (476-489)
redis/clientInstance.ts (1)
  • redisBullMQ (54-54)
constants/index.ts (1)
  • DEFAULT_WORKER_LOCK_DURATION (177-177)
jobs/initJobs.ts (2)
redis/clientInstance.ts (1)
  • redisBullMQ (54-54)
jobs/workers.ts (1)
  • syncBlockchainAndPricesWorker (33-59)
🪛 Shellcheck (0.11.0)
scripts/docker-exec-shortcuts.sh

[warning] 52-52: eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string).

(SC2294)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests

Comment on lines 33 to 59
export const syncBlockchainAndPricesWorker = async (queueName: string): Promise<void> => {
const worker = new Worker(
queueName,
async (job) => {
console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
await multiBlockchainClient.syncMissedTransactions()
await connectAllTransactionsToPrices()
// teardown
console.log('Cleaning up MultiBlockchainClient global instance...');
(global as any).multiBlockchainClient = null
},
{
connection: redisBullMQ,
lockDuration: DEFAULT_WORKER_LOCK_DURATION
}
)

worker.on('completed', job => {
console.log(`job ${job.id as string}: blockchain + prices sync finished`)
})

worker.on('failed', (job, err) => {
if (job != null) {
console.error(`job ${job.id as string}: FAILED — ${err.message}`)
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Teardown is ineffective; close clients and sockets instead of nulling global

Setting (global as any).multiBlockchainClient = null does not free the imported multiBlockchainClient reference and leaves Chronik/socket.io websockets open.

  • Add an explicit close/teardown on MultiBlockchainClient (and per-network clients), and call it here.
  • Remove the global nulling.

Apply this change here:

-      // teardown
-      console.log('Cleaning up MultiBlockchainClient global instance...');
-      (global as any).multiBlockchainClient = null
+      // teardown
+      console.log('Closing MultiBlockchainClient resources...');
+      await multiBlockchainClient.close()

And implement close() on the client (see services/chronikService.ts suggestion).

📝 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
export const syncBlockchainAndPricesWorker = async (queueName: string): Promise<void> => {
const worker = new Worker(
queueName,
async (job) => {
console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
await multiBlockchainClient.syncMissedTransactions()
await connectAllTransactionsToPrices()
// teardown
console.log('Cleaning up MultiBlockchainClient global instance...');
(global as any).multiBlockchainClient = null
},
{
connection: redisBullMQ,
lockDuration: DEFAULT_WORKER_LOCK_DURATION
}
)
worker.on('completed', job => {
console.log(`job ${job.id as string}: blockchain + prices sync finished`)
})
worker.on('failed', (job, err) => {
if (job != null) {
console.error(`job ${job.id as string}: FAILED — ${err.message}`)
}
})
}
export const syncBlockchainAndPricesWorker = async (queueName: string): Promise<void> => {
const worker = new Worker(
queueName,
async (job) => {
console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
await multiBlockchainClient.syncMissedTransactions()
await connectAllTransactionsToPrices()
// teardown
console.log('Closing MultiBlockchainClient resources...');
await multiBlockchainClient.close()
},
{
connection: redisBullMQ,
lockDuration: DEFAULT_WORKER_LOCK_DURATION
}
)
worker.on('completed', job => {
console.log(`job ${job.id as string}: blockchain + prices sync finished`)
})
worker.on('failed', (job, err) => {
if (job != null) {
console.error(`job ${job.id as string}: FAILED — ${err.message}`)
}
})
}
🤖 Prompt for AI Agents
In jobs/workers.ts around lines 33–59, the teardown currently just sets (global
as any).multiBlockchainClient = null which does not close sockets or free
resources; replace this with an explicit await multiBlockchainClient.close()
(and ensure each per-network client exposes/implements close() to shutdown
Chronik/socket.io connections) and remove the global nulling. Call close() in
the worker task after work completes (ideally in a finally block) and handle
errors from close() with a logged warning so sockets are properly closed and
resources released.

Comment on lines 51 to 53
"databaseshell" | "dbs")
eval "$base_command_db" bash -l "$@"
eval "$base_command_db" bash -c bash -l "$@"
;;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix shell invocation: 'bash -c bash -l' is not a login shell; also risks argument loss

  • bash -c bash -l executes a non-login bash; -l is not applied to the spawned shell. This breaks expected login env loading compared to previous bash -l.
  • For ns/rns, arguments are dropped; for dbs, quoting with eval is brittle. ShellCheck flags this (SC2294). Based on static analysis hints.

Apply this minimal fix:

-        eval "$base_command_db" bash -c bash -l "$@"
+        eval "$base_command_db" bash -l "$@"
-        eval "$base_command_node" bash -c bash -l
+        eval "$base_command_node" bash -l "$@"
-        eval "$base_command_node_root" bash -c bash -l
+        eval "$base_command_node_root" bash -l "$@"

Optional hardening (drop eval to preserve whitespace/args and avoid injection pitfalls):

-        eval "$base_command_db" bash -l "$@"
+        docker exec -it "$db_container_name" bash -l "$@"
-        eval "$base_command_node" bash -l "$@"
+        docker exec -it "$node_container_name" bash -l "$@"
-        eval "$base_command_node_root" bash -l "$@"
+        docker exec -it -u 0 "$node_container_name" bash -l "$@"

Also applies to: 72-74, 75-77

🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 52-52: eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string).

(SC2294)

🤖 Prompt for AI Agents
In scripts/docker-exec-shortcuts.sh around lines 51-53 (and similarly update
72-74, 75-77), the current invocation uses `eval` and `bash -c bash -l "$@"`
which does not start a true login shell and risks dropping or mangling
arguments; replace this with a direct docker exec invocation that starts bash as
a login shell and passes arguments safely: invoke bash with the -l option before
-c (so the spawned shell is a login shell), use -c to run the provided command,
include -- to stop option parsing, and pass the script's "$@" (or "$*" only if
you intentionally want a single string) directly without eval to preserve
whitespace and avoid injection; apply the same pattern to the ns/rns/dbs
branches.

Comment on lines +958 to 966
const asyncOperations: Array<Promise<void>> = []
this.clients = {
ecash: this.instantiateChronikClient('ecash', asyncOperations),
bitcoincash: this.instantiateChronikClient('bitcoincash', asyncOperations)
}
await Promise.all(asyncOperations)
this.setInitialized()
console.log('Finished initializing MultiBlockchainClient.')
})()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Race: Multi setInitialized before per-client Chronik is ready

Promise.all(asyncOperations) may be empty (e.g., JOBS_ENV set), so setInitialized() flips flags before ChronikBlockchainClient finishes its async constructor (before this.chronik is set). Downstream syncMissedTransactions() can then run against undefined this.chronik.

Always gate Multi init on each client's waitForLatencyTest():

   void (async () => {
-      const asyncOperations: Array<Promise<void>> = []
+      const asyncOperations: Array<Promise<void>> = []
       this.clients = {
-        ecash: this.instantiateChronikClient('ecash', asyncOperations),
-        bitcoincash: this.instantiateChronikClient('bitcoincash', asyncOperations)
+        ecash: this.instantiateChronikClient('ecash', asyncOperations),
+        bitcoincash: this.instantiateChronikClient('bitcoincash', asyncOperations)
       }
-      await Promise.all(asyncOperations)
+      // Always wait for base client readiness (chronik selected).
+      asyncOperations.push(this.clients.ecash.waitForLatencyTest())
+      asyncOperations.push(this.clients.bitcoincash.waitForLatencyTest())
+      await Promise.all(asyncOperations)
       this.setInitialized()
       console.log('Finished initializing MultiBlockchainClient.')
   })()

Alternatively, push waitForLatencyTest() inside instantiateChronikClient unconditionally.

🤖 Prompt for AI Agents
In services/chronikService.ts around lines 958-966, the code calls
Promise.all(asyncOperations) which can be empty so setInitialized() may run
before each ChronikBlockchainClient finishes async setup (this.chronik
undefined); ensure Multi init awaits each client's readiness by adding each
instantiated client's waitForLatencyTest() promise to asyncOperations (or
unconditionally call await client.waitForLatencyTest() inside
instantiateChronikClient), then await Promise.all(asyncOperations) before
calling setInitialized() so downstream code never sees an uninitialized
this.chronik.

Comment on lines +1078 to +1084
public async syncMissedTransactions (): Promise<void> {
await this.waitForStart()
await Promise.all([
this.clients.ecash.syncMissedTransactions(),
this.clients.bitcoincash.syncMissedTransactions()
])
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard sync against not-yet-ready clients

Even with Multi’s wait, explicitly ensure per-client readiness before syncing:

   public async syncMissedTransactions (): Promise<void> {
-    await this.waitForStart()
-    await Promise.all([
+    await this.waitForStart()
+    await Promise.all([
+      this.clients.ecash.waitForLatencyTest(),
+      this.clients.bitcoincash.waitForLatencyTest()
+    ])
+    await Promise.all([
       this.clients.ecash.syncMissedTransactions(),
       this.clients.bitcoincash.syncMissedTransactions()
     ])
   }
📝 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
public async syncMissedTransactions (): Promise<void> {
await this.waitForStart()
await Promise.all([
this.clients.ecash.syncMissedTransactions(),
this.clients.bitcoincash.syncMissedTransactions()
])
}
public async syncMissedTransactions (): Promise<void> {
await this.waitForStart()
await Promise.all([
this.clients.ecash.waitForLatencyTest(),
this.clients.bitcoincash.waitForLatencyTest()
])
await Promise.all([
this.clients.ecash.syncMissedTransactions(),
this.clients.bitcoincash.syncMissedTransactions()
])
}
🤖 Prompt for AI Agents
In services/chronikService.ts around lines 1078–1084, the method calls
syncMissedTransactions on clients immediately after Multi.waitForStart; change
it to explicitly ensure each client is ready before syncing by awaiting each
client's readiness (e.g., await this.clients.ecash.waitForStart() and await
this.clients.bitcoincash.waitForStart() or checking an isReady flag) and then
call their syncMissedTransactions; handle each client individually (try/catch
per client) so one failing or not-ready client does not block the other.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (2)
services/chronikService.ts (2)

958-966: Race condition persists: setInitialized may be called before clients are ready.

When JOBS_ENV is set (and not in test/app mode), asyncOperations remains empty because instantiateChronikClient only conditionally adds promises to it (lines 1004-1018). This means Promise.all(asyncOperations) resolves immediately, and setInitialized() is called before each ChronikBlockchainClient completes its async constructor (before this.chronik is set on line 141-144). Downstream code like syncMissedTransactions() may then attempt to use an uninitialized client.

Based on previous review feedback, always wait for each client's readiness:

  void (async () => {
    const asyncOperations: Array<Promise<void>> = []
    this.clients = {
      ecash: this.instantiateChronikClient('ecash', asyncOperations),
      bitcoincash: this.instantiateChronikClient('bitcoincash', asyncOperations)
    }
+    // Always wait for base client readiness (chronik selected).
+    asyncOperations.push(this.clients.ecash.waitForLatencyTest())
+    asyncOperations.push(this.clients.bitcoincash.waitForLatencyTest())
    await Promise.all(asyncOperations)
    this.setInitialized()
    console.log('Finished initializing MultiBlockchainClient.')
  })()

1078-1084: Add explicit per-client readiness checks before syncing.

The method only waits for this.waitForStart(), which checks the Multi client's initialization flag. However, if the race condition flagged earlier exists, this flag may be set before individual clients are ready. Additionally, explicitly checking each client's readiness provides defense-in-depth.

Based on previous review feedback, ensure each client is ready before syncing:

  public async syncMissedTransactions (): Promise<void> {
    await this.waitForStart()
+    await Promise.all([
+      this.clients.ecash.waitForLatencyTest(),
+      this.clients.bitcoincash.waitForLatencyTest()
+    ])
    await Promise.all([
      this.clients.ecash.syncMissedTransactions(),
      this.clients.bitcoincash.syncMissedTransactions()
    ])
  }
🧹 Nitpick comments (1)
services/chronikService.ts (1)

1091-1100: Consider logging destroy errors and setting a destroyed flag.

The current implementation silently swallows all errors during cleanup, which could hide issues like network timeouts or resource leaks. Additionally, there's no flag to prevent further use of the client after destruction.

Apply these improvements:

  public async destroy (): Promise<void> {
    await Promise.all(
      Object.values(this.clients).map(async (c) => {
        try {
          c.chronikWSEndpoint.close()
          c.wsEndpoint.close()
-        } catch {}
+        } catch (err: any) {
+          console.warn(`[CHRONIK] Error during client cleanup: ${err.message as string}`)
+        }
      })
    )
+    this.initializing = true  // Block further operations
  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee0b6ee and 44a2970.

📒 Files selected for processing (2)
  • jobs/workers.ts (2 hunks)
  • services/chronikService.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
services/chronikService.ts (1)
types/chronikTypes.ts (1)
  • SyncAndSubscriptionReturn (50-53)
jobs/workers.ts (2)
services/chronikService.ts (1)
  • multiBlockchainClient (1112-1112)
services/transactionService.ts (1)
  • connectAllTransactionsToPrices (476-489)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (2)
jobs/workers.ts (1)

4-5: LGTM!

The new imports are correctly sourced and match the usage in the new worker function.

services/chronikService.ts (1)

1020-1020: LGTM!

The added log statement provides useful feedback about the client instantiation completion.

Comment on lines +47 to +55
worker.on('completed', (job) => {
// teardown
void (async () => {
console.log('Cleaning up MultiBlockchainClient global instance...')
await multiBlockchainClient.destroy()
console.log('Done.')
console.log(`job ${job.id as string}: blockchain + prices sync finished`)
})()
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fire-and-forget async cleanup may not complete before process exit.

The void (async () => {...})() pattern in the completed handler creates an async operation that runs detached from the worker lifecycle. This means:

  • The destroy() call may not complete before the process exits
  • The completion log on Line 53 may print before cleanup finishes
  • Errors during cleanup are swallowed

Move the cleanup to the job handler with proper awaiting:

  const worker = new Worker(
    queueName,
    async (job) => {
-      console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
-      await multiBlockchainClient.syncMissedTransactions()
-      await connectAllTransactionsToPrices()
+      try {
+        console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
+        await multiBlockchainClient.syncMissedTransactions()
+        await connectAllTransactionsToPrices()
+      } finally {
+        console.log('Cleaning up MultiBlockchainClient global instance...')
+        await multiBlockchainClient.destroy()
+        console.log('Done.')
+      }
    },
    {
      connection: redisBullMQ,
      lockDuration: DEFAULT_WORKER_LOCK_DURATION
    }
  )

  worker.on('completed', (job) => {
-    // teardown
-    void (async () => {
-      console.log('Cleaning up MultiBlockchainClient global instance...')
-      await multiBlockchainClient.destroy()
-      console.log('Done.')
-      console.log(`job ${job.id as string}: blockchain + prices sync finished`)
-    })()
+    console.log(`job ${job.id as string}: blockchain + prices sync finished`)
  })
📝 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
worker.on('completed', (job) => {
// teardown
void (async () => {
console.log('Cleaning up MultiBlockchainClient global instance...')
await multiBlockchainClient.destroy()
console.log('Done.')
console.log(`job ${job.id as string}: blockchain + prices sync finished`)
})()
})
const worker = new Worker(
queueName,
async (job) => {
try {
console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
await multiBlockchainClient.syncMissedTransactions()
await connectAllTransactionsToPrices()
} finally {
console.log('Cleaning up MultiBlockchainClient global instance...')
await multiBlockchainClient.destroy()
console.log('Done.')
}
},
{
connection: redisBullMQ,
lockDuration: DEFAULT_WORKER_LOCK_DURATION
}
)
worker.on('completed', (job) => {
console.log(`job ${job.id as string}: blockchain + prices sync finished`)
})

Copy link
Contributor

@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

♻️ Duplicate comments (2)
jobs/workers.ts (2)

47-55: Fire-and-forget async cleanup still present; move to finally block in job handler.

The void (async () => {...})() pattern creates a detached async operation that may not complete before the worker process exits, meaning:

  • The destroy() call may not finish before exit
  • Errors during cleanup are silently swallowed
  • The completion log on Line 53 may print before cleanup finishes

This issue was previously flagged. The suggested fix is to move cleanup into the job handler with proper awaiting in a finally block (see comment on Lines 36-39).


36-39: Add try-finally block to ensure cleanup always runs.

The job handler lacks error handling and a finally block. If either syncMissedTransactions() or connectAllTransactionsToPrices() throws, the cleanup won't execute. Move the cleanup from the completed event handler into a finally block here to ensure resources are always released.

Apply this diff:

  const worker = new Worker(
    queueName,
    async (job) => {
-      console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
-      await multiBlockchainClient.syncMissedTransactions()
-      await connectAllTransactionsToPrices()
+      try {
+        console.log(`job ${job.id as string}: syncing missed transactions and connecting prices...`)
+        await multiBlockchainClient.syncMissedTransactions()
+        await connectAllTransactionsToPrices()
+      } finally {
+        console.log('Cleaning up MultiBlockchainClient global instance...')
+        await multiBlockchainClient.destroy()
+        console.log('Done.')
+      }
    },
    {
      connection: redisBullMQ,
      lockDuration: DEFAULT_WORKER_LOCK_DURATION
    }
  )

  worker.on('completed', (job) => {
-    // teardown
-    void (async () => {
-      console.log('Cleaning up MultiBlockchainClient global instance...')
-      await multiBlockchainClient.destroy()
-      console.log('Done.')
-      console.log(`job ${job.id as string}: blockchain + prices sync finished`)
-    })()
+    console.log(`job ${job.id as string}: blockchain + prices sync finished`)
  })
🧹 Nitpick comments (1)
jobs/workers.ts (1)

58-58: Consider using strict inequality for consistency.

Line 26 uses job !== undefined while Line 58 uses job != null. For consistency, prefer strict inequality.

-    if (job != null) {
+    if (job !== undefined) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44a2970 and dcb0f03.

📒 Files selected for processing (1)
  • jobs/workers.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
jobs/workers.ts (2)
services/chronikService.ts (1)
  • multiBlockchainClient (1112-1112)
services/transactionService.ts (1)
  • connectAllTransactionsToPrices (476-489)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests
🔇 Additional comments (2)
jobs/workers.ts (2)

4-5: LGTM!

The imports are correctly added to support the new blockchain and prices synchronization worker.


9-31: LGTM!

The logging updates make the messages more generic and consistent. No functional changes.

Copy link
Contributor

@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

♻️ Duplicate comments (2)
services/chronikService.ts (2)

958-966: Race condition persists: setInitialized called before clients are ready

This is the same critical race condition flagged in the previous review. When JOBS_ENV is set (and not in test mode), isRunningApp() returns false at line 989-996, so instantiateChronikClient doesn't push any async operations into asyncOperations (lines 1004-1018). This means asyncOperations is empty, Promise.all(asyncOperations) resolves immediately, and setInitialized() executes before each ChronikBlockchainClient finishes its async constructor (before this.chronik is assigned at lines 141-144). When workers call syncMissedTransactions(), they'll encounter undefined this.chronik.

Apply this diff to always await client readiness:

   void (async () => {
       const asyncOperations: Array<Promise<void>> = []
       this.clients = {
         ecash: this.instantiateChronikClient('ecash', asyncOperations),
         bitcoincash: this.instantiateChronikClient('bitcoincash', asyncOperations)
       }
+      // Always wait for each client's chronik to be selected
+      asyncOperations.push(this.clients.ecash.waitForLatencyTest())
+      asyncOperations.push(this.clients.bitcoincash.waitForLatencyTest())
       await Promise.all(asyncOperations)
       this.setInitialized()
       console.log('Finished initializing MultiBlockchainClient.')
   })()

1078-1084: Explicitly wait for each client's readiness before syncing

Related to the constructor race condition, waitForStart() only checks the Multi's initializing flag, which can be false even when individual clients haven't finished their async constructors. This method should explicitly ensure each client is ready before calling syncMissedTransactions().

Apply this diff to wait for per-client readiness:

   public async syncMissedTransactions (): Promise<void> {
     await this.waitForStart()
+    // Explicitly ensure each client is ready
+    await Promise.all([
+      this.clients.ecash.waitForLatencyTest(),
+      this.clients.bitcoincash.waitForLatencyTest()
+    ])
     await Promise.all([
       this.clients.ecash.syncMissedTransactions(),
       this.clients.bitcoincash.syncMissedTransactions()
     ])
   }
🧹 Nitpick comments (2)
services/chronikService.ts (2)

1091-1102: Guard against closing uninitialized clients

The destroy() method attempts to close chronikWSEndpoint and wsEndpoint, which are set in the async constructor (lines 146, 152). If destroy() is called before clients finish initializing, these properties will be undefined. While error handling exists, it's cleaner to check for readiness first.

Apply this diff to add defensive checks:

   public async destroy (): Promise<void> {
+    // Wait for clients to finish initializing before destroying
+    await this.waitForStart()
+    await Promise.all([
+      this.clients.ecash.waitForLatencyTest(),
+      this.clients.bitcoincash.waitForLatencyTest()
+    ])
     await Promise.all(
       Object.values(this.clients).map(async (c) => {
         try {
-          c.chronikWSEndpoint.close()
-          c.wsEndpoint.close()
+          if (c.chronikWSEndpoint) c.chronikWSEndpoint.close()
+          if (c.wsEndpoint) c.wsEndpoint.close()
         } catch (err: any) {
           console.error(`Failed to close connections for client: ${err.message as string}`)
         }
       })
     )
   }

1020-1020: Misleading log message timing

The log message "Finished instantiating" executes synchronously, but the ChronikBlockchainClient constructor contains async operations that continue after this method returns. The client isn't actually finished instantiating when this logs.

Consider one of these options:

-    console.log(`Finished instantiating ${networkSlug} client.`)
+    console.log(`Started instantiating ${networkSlug} client.`)

Or remove the log since the constructor already logs progress, and line 965 logs "Finished initializing MultiBlockchainClient."

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcb0f03 and b50f0f3.

📒 Files selected for processing (1)
  • services/chronikService.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
services/chronikService.ts (1)
types/chronikTypes.ts (1)
  • SyncAndSubscriptionReturn (50-53)

@chedieck chedieck merged commit 7bea95a into master Oct 20, 2025
3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 26, 2025
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