Optimizing ESHOPMAN's Redis Locking: Preventing Connection Leaks in Testing & Migrations

Understanding ESHOPMAN's Redis Locking Provider and Connection Management

At Move My Store, we're always diving deep into the technical nuances of ESHOPMAN to empower our community. Today, we're shedding light on an important aspect of ESHOPMAN's internal architecture: the Redis-based locking provider. This component is crucial for managing concurrent operations within your headless commerce setup, but a recent community discussion highlighted a key area for optimization regarding its connection lifecycle.

The core of the issue revolves around how ESHOPMAN's Redis-based locking provider manages its ioredis client connections. It was observed that the provider initiates a Redis connection upon loading but lacks a proper mechanism to close this connection, particularly during application shutdown. This can lead to persistent, unclosed connections, consuming resources unnecessarily.

Key Observations from the ESHOPMAN Community:

  • Missing Shutdown Path: Unlike other Redis-backed ESHOPMAN modules (such as the event-bus or cache providers), the locking provider does not implement a shutdown method to disconnect its Redis client. This means connections remain open even after the application or module is theoretically no longer active.
  • Migration-Only Loads: Connections are inadvertently opened during 'migration-only' ESHOPMAN internal processes. These operations, like database migrations or link synchronizations, typically don't require active locking, yet the module's loader still establishes a Redis connection.
  • Impact on Testing Environments: This behavior is particularly noticeable in ESHOPMAN integration test suites. Each test file can leave multiple Redis client connections open, leading to resource exhaustion and potential instability. Developers often rely on a --forceExit flag for their test runner, which bypasses proper cleanup and can mask underlying resource management issues. Without it, these leaked connections could prevent Node.js processes from exiting cleanly.

The problem stems from the module's loader, which connects to Redis and registers the client without a corresponding cleanup routine. Even if the initial connection fails, ioredis continues retry attempts in the background, further exacerbating resource drain.

Expected Behavior for Robust ESHOPMAN Development

Ideally, the ESHOPMAN locking provider should:

  • Gracefully close its Redis client during application shutdown, similar to how other ESHOPMAN Redis modules handle their connections.
  • Avoid opening Redis connections altogether during 'migration-only' loads, as these operations do not necessitate active locking.

Reproducing the Connection Leak

A helpful reproduction script was shared within the ESHOPMAN community to demonstrate this behavior. It simulates the sequence of module loads performed by ESHOPMAN's integration test runner, showcasing how connections accumulate:

// node repro.js, run from the root of an ESHOPMAN project
const net = require("net")
const req = require("module").createRequire(process.cwd() + "/")
const { MedusaModule } = req("@medusajs/framework/modules-sdk")
const { createMedusaContainer, Modules } = req("@medusajs/framework/utils")
const { asValue } = req("@medusajs/framework/awilix")

// Minimal stand-in for Redis: answers +OK to every command
let openC
const stub = net.createServer((s) => {
  openConnections++
  s.on("data", (d) => s.write("+OK\r
".repeat((d.toString().match(/(^|
)\*/g) || []).length)))
  s.on("close", () => openConnections--)
})
const logger = { info() {}, warn() {}, error: console.error, debug() {} }

async function bootLocking(port, migrationOnly) {
  const sharedC
  sharedContainer.register({ logger: asValue(logger) })
  await MedusaModule.bootstrap({
    moduleKey: Modules.LOCKING,
    defaultPath: "@medusajs/medusa/locking",
    declaration: {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-redis",
            id: "locking-redis",
            is_default: true,
            options: { redisUrl: `redis://127.0.0.1:${port}`, redisOptions: { enableReadyCheck: false } },
          },
        ],
      },
    },
    sharedContainer,
    migrationOnly,
  })
}

stub.listen(0, "127.0.0.1", async () => {
  const { port } = stub.address()
  // Simulates the order of loads ESHOPMAN's integration test runner performs per test file:
  await bootLocking(port, true) // migrateDatabase -> MedusaAppMigrateUp (migrationOnly)
  MedusaModule.clearInstances()
  await bootLocking(port, true) // syncLinks -> MedusaAppGetLinksExecutionPlanner (migrationOnly)
  MedusaModule.clearInstances() // the runner's clearInstances()
  await bootLocking(port, false) // appLoader.load() (full load)
  console.log("open Redis connections before shutdown:", openConnections)

  await MedusaModule.onApplicationShutdown()
  MedusaModule.clearInstances()
  await new Promise((r) => setTimeout(r, 200))
  console.log("open Redis connections after onApplicationShutdown():", openConnections)
  process.exit(0)
})

This script effectively illustrates how three Redis connections remain open even after MedusaModule.onApplicationShutdown() is called, confirming the resource leak.

Moving Forward

This insight is crucial for ESHOPMAN developers aiming for robust, efficient, and stable applications. Addressing this connection management oversight will lead to cleaner testing environments, reduced resource consumption, and a more predictable ESHOPMAN development experience. We encourage the ESHOPMAN community to consider implementing proper shutdown hooks and conditional connection logic for the locking provider to align with best practices across the platform.

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools