Skip to content

Bug: timer reference overwritten in MongoDB connection poller causes 10s rejection timer to leak #454

@lonix

Description

@lonix

Problem

In src/services/config-service.ts inside initialize(), the timeoutId variable that holds the 10-second hard rejection timeout is overwritten on line 83 whenever the connection state is neither 0 nor 1 (e.g. state 2 = connecting, state 3 = disconnecting):

let timeoutId: ReturnType<typeof setTimeout>;
timeoutId = setTimeout(() => {
  reject(new Error("MongoDB connection timeout"));
}, 10000);                                         // ← outer 10s timer

const checkConnection = async (): Promise<void> => {
  try {
    if (mongoose.connection.readyState === 1) {
      clearTimeout(timeoutId);                     // clears the 100ms poll, NOT the 10s timer
      resolve();
    } else if (mongoose.connection.readyState === 0) {
      await mongoose.connect(...);
      clearTimeout(timeoutId);                     // same problem
      resolve();
    } else {
      timeoutId = setTimeout(checkConnection, 100);  // ← OVERWRITES the reference to the 10s timer
    }
  } catch (error) { reject(error); }
};

After the first poll cycle where state is 2 or 3, timeoutId points to the 100ms poll timer instead of the 10s rejection timer. When the connection finally succeeds and clearTimeout(timeoutId) is called, it clears the already-fired 100ms timer and the original 10s timer is leaked. That leaked timer will call reject() ~10 seconds later — after resolve() was already called — which is silently swallowed by the Promise machinery but can produce confusing debug output and wastes a timer slot.

Suggested fix

Use a separate variable for the poll timer so the original rejection timer reference is never overwritten:

const rejectTimeoutId = setTimeout(() => {
  reject(new Error("MongoDB connection timeout"));
}, 10000);

let pollTimeoutId: ReturnType<typeof setTimeout> | null = null;

const checkConnection = async (): Promise<void> => {
  try {
    if (mongoose.connection.readyState === 1) {
      clearTimeout(rejectTimeoutId);
      if (pollTimeoutId) clearTimeout(pollTimeoutId);
      resolve();
    } else if (mongoose.connection.readyState === 0) {
      await mongoose.connect(process.env.MONGODB_URI || "mongodb://mongodb:27017/koolbot");
      clearTimeout(rejectTimeoutId);
      if (pollTimeoutId) clearTimeout(pollTimeoutId);
      resolve();
    } else {
      pollTimeoutId = setTimeout(checkConnection, 100);
    }
  } catch (error) {
    clearTimeout(rejectTimeoutId);
    reject(error);
  }
};

Affected file

src/services/config-service.ts lines 64–88 (initialize())

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions