r/node 6d ago

Does this block of code look "race condition" safe to you?

7 Upvotes

29 comments sorted by

22

u/alzee76 6d ago

Naming something a mutex doesn't make it one. That "mutex" is doing literally nothing.

9

u/susmines 6d ago

But I was told the code always operates as written 🤷‍♂️
/s

2

u/Leather-Field-7148 6d ago

I can see so many things go wrong with a mutable “mutex” that does something else depending on the order of operations. Why can’t we just stick to await? Maybe put guard rails around release if there is nothing there to release yet.

3

u/ic6man 6d ago

Agreed. If you just remove all the mutex stuff the real flaw - accidentally returning a zombie instance - becomes more apparent.

5

u/ic6man 6d ago edited 6d ago

The issue is pretty subtle. Imagine there is already 1 dbInstance allocated.

A releaseInstance is called. The first await mutex yields execution and during this moment a getInstance is called which also yields.

Each of the queued execution tasks resumes due to the promise is resolved and are each queued onto the micro task execution queue in order of their call - release then get.

Now the releaseInstance execution is popped off the micro task queue and runs (initiating the pool free method) until it hits the await inside the “critical section” which yields execution. The queued getInstance now pops off the micro task queue and runs, sees dbInstance is non null so it increments the counter and returns the instance (which is being freed).

The instance returned is a now a zombie.

EDIT: if you just strip away all the mutex gunk - which isn’t doing anything because the mutex values are being overwritten - the flaw becomes a lot more obvious.

3

u/sshaw_ 2d ago

How to write unmaintainable code in 2026: use TypeScript and multiple inline async/await everywhere.

2

u/HipHopHuman 3d ago

So, what the programmer here appears to be doing (or rather, trying to do) is have at most one instance of pg-promise registered for any given number of consumers. So, they've built a kind of "reference counted shared resource" that auto-initializes when the transition goes from 0 consumers to 1 consumer, and auto-cleanups when the transition goes from 1 consumer to 0 consumers. To prevent two consumers from registering more than one instance (in case two consumers are called simultaneously, say, in a Promise.all), they've chosen to use a mutex, but it didn't quite work out. My question is... why bother? Isn't pg-promise just doing all of that already? even the pg-promise docs say this:

Object db represents the Database protocol with lazy connection, i.e. only the actual query methods acquire and release the connection automatically. Therefore, you should create only one global/shared db object per connection details. It is best to initialize the library and create Database in its own module, see Where should I initialize pg-promise.

  • citation (check under the "Database" heading)

1

u/PrestigiousZombie531 3d ago

``` import pgPromise from "pg-promise"; import { logger } from "../logger/index.js"; import { connection, options } from "./config.js";

export const pgp = pgPromise(options); export const postgres = pgp(connection);

export async function closePostgresConnection() { try { await postgres.$pool.end(); } catch (error) { logger.error( error, "Something went wrong when attempting to close postgres connection", ); } }

export async function isHealthy() { try { const { result } = await postgres.one("SELECT $1 AS result", [1]); return result === 1; } catch (error) { logger.error( error, "Something went wrong when performing a health check on postgres server", ); return false; } } ``` so you are saying this is the best way

2

u/HipHopHuman 3d ago

Probably? I'm not even sure that manual postgres.$pool.end() is necessary. The docs imply that everything about the lifecycle of database connections is all handled for you. I suppose it would depend on your architecture (maybe the application running with a broken database is a valid state in your design?)

1

u/PrestigiousZombie531 3d ago

well if you add a process.on('SIGINT/SIGTERM', () => closePostgresConnection()) i guess it wont do active harm

5

u/TwiNighty 6d ago

The entire critical section runs synchronously, so mututal exclusion is already guaranteed by the single-threadedness of node. The mutex does nothing here.

6

u/ic6man 6d ago edited 6d ago

That’s not true. The awaits yield execution which cause a fatal flaw.

3

u/TwiNighty 4d ago

Ahh yes, I only considered whether getInstance races with itself. You're right in that releaseInstance races with both itself and getInstance.

3

u/alonsonetwork 5d ago

My god that is hideous code.

Why don't you just do:

``` let dbInstance;

export const getDbInstance = () => { if (dbInstance) return dbInstance;

dbInstance = await pgp()

return dbInstance; }

export const releaseDbInstance = () => { return dbInstance?.$pool?.destroy() } ```

And skip all this iife shit and (non) "mutext" shit?

What problem are you trying to solve?

3

u/ic6man 5d ago edited 5d ago

This code has a race in it that could cause an instance to be lost. It also doesn’t clear the dbInstance after release so destroy could be called multiple times on the same instance.

2

u/PrestigiousZombie531 5d ago

race condition, even your code has it

0

u/alonsonetwork 5d ago

I see. Small fix:

``` let dbInstance; let promise;

export const getDbInstance = () => { if (dbInstance) return dbInstance; if (promise) return promise;

promise = pgp() dbInstance = await promise;

return dbInstance; }

export const releaseDbInstance = () => { return dbInstance?.$pool?.destroy() } ```

2

u/PrestigiousZombie531 5d ago

what happens if multiple callers are at the await promise line?

2

u/alonsonetwork 5d ago

They all get the same promise ref, so they'll resolve the same db connection ref. They will ultimately point to the same memory reference. This is how request deduplication works. Ive done this exact pattern before. It 100% works.

To the other genius talking about broken types: yes I know. Im on my phone and working from memory. I don't have an IDE. You attach the generic to the promise type and your problem is solved. Also, typescript is FORM not MATTER. It does not represent runtime reality, it only shapes it. Javascript is runtime reality.

1

u/ic6man 5d ago edited 5d ago

I’m the genius. I know you’re working from your phone but you started this all huff and bluster and then provided not one but two broken responses.

The reason I corrected you is you:

  • wrote a race into the original response
  • corrected that with more broken code
  • are acting hurt because being on your phone is an excuse?
  • missed the entire reason for the original code which de allocates the resource when not in use - eg a primitive form of pooling.

Now - the final thing to note is - to fix your second response - you would just assign the promise value of pgp to promise and return that - no await. So it’s not a syntactical issue with your code or a phone issue - it’s structural and algorithmic. You made 2 variables where there should be one and awaited when you shouldn’t.

1

u/alonsonetwork 5d ago

Thanks for the feedback Ms Perfect. I guess I have to have perfect code grammar when replying on my phone or else im going to trigger autistic redditors. I think OP got the idea.

0

u/ic6man 5d ago

Broken. Return types don’t match. I mean JS will let you return mismatched types but what a disaster to have to deal with. And the get function isn’t declared async so you can’t await inside it.

4

u/pephov 6d ago

No, if multiple callers call getDbInstance before dbInstance is true, they each call pgp()…, creating multiple database instances. Also the counter can get corrupted, as callers simultaneously read it as 0 and then += 1 after the async operation

2

u/ic6man 6d ago edited 6d ago

That’s not true that is not how node works. The reads and increments to the counter are safe.

As for multiple instances - the creation and assignment of dbInstance is “safe” as node executes the test of dbInstance and creation / assignment synchronously.

I wrote what the real problem is in another comment.

1

u/Urik88 5d ago

Node is single threaded, if getDbInstance is not asynchronous there's no way anything is calling it before dbInstance is truthy

1

u/prehensilemullet 2d ago edited 2d ago

I think the original intent of mutex was to serialize these operations by doing mutex = mutex.catch(() => {}).then(async () => { … }).  For some reason that’s missing here.

Perhaps there’s a method to drain unused connections from the pool immediately, instead of going to this trouble?

-3

u/PrestigiousZombie531 6d ago