Locking a specific value
anton-johansson opened this issue · comments
Hi! I just found this project and I'm looking for something just like this. In my case, it's related to the creation of database connections. I only want to create the database connection once, even if the server is called multiple times during a very short period.
In normal cases, I could just create the promise for creating the database connection at startup, and use that promise, as it would always resolve to the same database connection. The problem is that my application is multitenant, and there are a lot of different databases (one per installation/company/customer). I can't know beforehand which databases exists. They can come and go.
I can use Mutex
to lock the entire database connection creation - which will work for me and solve my problems - however, it would be very nice if database connections for different databases could be created in parallell. So I was thinking of something like this:
const mutex = new Mutex();
const connections: Record<number, DatabaseConnection> = {};
const createConnection = async (databaseId: number) => {
const connection = connections[databaseId];
if (connection) {
return connection;
}
return mutex.runExclusive(databaseId, async () => {
const connection = connections[databaseId];
if (connection) {
return connection;
}
const databaseInfo = getDatabaseInfo(databaseId);
const connection = Database.connect(databaseInfo);
connections[databaseId] = connection;
return connection;
});
}
The idea here is that the mutex would only lock per databaseId
. Have anyone given any thought regarding this? Is there an obvious solution that I've missed?
Thanks in advance!
I guess I could have a Mutex
for each databaseId
, and use a separate Mutex
when actually creating the Mutex
instances for each databaseId
. This could work, as I'm assuming that the creation of a Mutex
is a cheap operation.
Hi @anton-johansson !
Sorry for the late reply. The way you would solve your problem with async-mutex
is creating a separate Mutex
instance for every database connection you create. The only catch with this approach is that you will need to take care of creating and tearing down Mutex
instances as you go. If your DB connection are represented by objects you can use a WeakMap<DBHandle, Mutex>
to avoid leaking mutexes when the DB handle is not used anymore, but if you are using primitive values like IDs, you'll have to do the bookkeeping yourself.
I have though about solving this at the library level in the past by
- Adding an event to the Mutex / Semaphore that fires once there are no more consumers waiting in the queue
- Adding a decorator that manages a collection of mutexes for different values. When you try to acquire a lock for a specific value it would create the mutex on demand and automatically remove it once the last consumer is finished.
I have not found time or need to add it, but give me a shout if the initial approach is too cumbersome, this might provide me with the motivation to add this 😏
Hi! You can try something like that:
import { Mutex } from 'async-mutex';
type AbstractConnection = {
info: DatabaseInfo;
};
type DatabaseInfo = {};
interface AbstractDB {
connect: (info: DatabaseInfo) => AbstractConnection
}
class AbstractDatabase implements AbstractDB {
constructor() {}
connect(info: DatabaseInfo) { return { info } as AbstractConnection }
}
class Connections {
pool: Map<number, Promise<AbstractConnection>>;
constructor(pool = new Map()) {
this.pool = pool;
}
async createConnection(databaseId: number) {
if (this.pool.has(databaseId)) return this.pool.get(databaseId);
const mutex = new Mutex();
this.pool.set(databaseId, mutex.runExclusive(() => this.getConnection(databaseId)));
return this.pool.get(databaseId);
}
async getConnection(databaseId: number) {
const info = this.getDatabaseInfo(databaseId);
return new AbstractDatabase().connect(info);
}
getDatabaseInfo(databaseId: number): DatabaseInfo {
return { databaseId } as DatabaseInfo;
}
}
Hey people!
I actually ended up with the similar package async-lock
, which already had what I was after.
@kmescharikov, there are two potential problems with the code:
- It seems like you're creating a new mutex everytime, so I'm not sure this would prevent anything. If you call
createConnection(1)
(initially) twice at the same time, it would surely enterthis.getConnection(1)
twice? - You need a second check for an existing pool within the synchronized block. So first you check outside the synchronized block (for performance), and then you check again inside the synchronized block incase multiple callers were blocking there at the same time.
Anyway, I could've solved it with async-mutex
with multiple Mutex
instances, but async-lock
made it a little easier, so I stuck with that.
Thanks anyway!