Skip to content

Commit

Permalink
feat(cache): add block
Browse files Browse the repository at this point in the history
  • Loading branch information
KABBOUCHI committed Nov 10, 2023
1 parent fd2a61c commit cd3e8d4
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 22 deletions.
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,23 +251,40 @@ const users = await Cache.remember("users", seconds, async () => {
await Cache.store("memory").get("users:1"); // safe way to switch driver for a moment

// experimental
const isLocked = await Cache.lock("key", 10).get(async () => {
const isLocked = await Cache.lock("key").get(async () => {
console.log("do something");
});

// or
const isAcquired = await Cache.getLock("key", 10, async () => {
const isAcquired = await Cache.getLock("key", async () => {
console.log("do something");
});

// or
const lock = Cache.lock("key", 10);
const lock = Cache.lock("key");

if (await lock.get()) {
console.log("do something");

await lock.release();
}

// or
const lock = Cache.lock("key");

try {
await lock.block(5);
// Lock acquired after waiting a maximum of 5 seconds...
} catch {
// Unable to acquire lock...
} finally {
$lock.release();
}

// or
await Cache.block("key", 5, async () => {
// Lock acquired after waiting a maximum of 5 seconds...
});
```

### toJsonRpcProvider
Expand Down
12 changes: 4 additions & 8 deletions src/cache/drivers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,16 @@ export const memoryCacheDriver: ICacheDriver = {

await Promise.resolve()
},
lock (key, seconds) {
return new MemoryLock(key, seconds)
lock (key, seconds, owner) {
return new MemoryLock(key, seconds, owner)
}
}

const cacheLock = new Map()

class MemoryLock implements ICacheLock {
// eslint-disable-next-line no-useless-constructor
constructor (private key: string, private seconds: number) { }
constructor (private key: string, private seconds: number, private owner: string) { }

acquire () {
const currentTime = Date.now()
Expand All @@ -60,10 +60,6 @@ class MemoryLock implements ICacheLock {
}

release () {
const lockExpiry = cacheLock.get(this.key)

if (lockExpiry && lockExpiry > Date.now()) {
cacheLock.delete(this.key)
}
cacheLock.delete(this.key)
}
}
35 changes: 29 additions & 6 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface ICacheDriver {
set(key: string, value: any, seconds?: number): Promise<void>
forget(key: string): Promise<void>
flush(): Promise<void>
lock?: (key: string, seconds: number) => ICacheLock
lock?: (key: string, seconds: number, owner: string) => ICacheLock
}

export class Cache {
Expand Down Expand Up @@ -100,15 +100,15 @@ export class Cache {
return value
}

static lock (key: string, seconds: number) {
static lock (key: string, options: { seconds?: number, owner?: string, sleepMilliseconds?: number } = {}) {
if (!this.driver.lock) {
throw new Error(`Driver ${this.defaultDriver} does not support locking`)
}

const lock = this.driver.lock(key, seconds)
const lock = this.driver.lock(key, options.seconds ?? 86400, options.owner ?? Date.now().toString())

return {
release: lock.release,
release: lock.release.bind(lock),
get: async (cb?: () => Promise<void> | void): Promise<boolean> => {
if (!cb) {
return await lock.acquire()
Expand All @@ -123,11 +123,34 @@ export class Cache {
}

return false
},
block: async (timeout: number, cb?: () => Promise<void> | void): Promise<void> => {
const starting = Date.now()

while (!await lock.acquire()) {
await new Promise(resolve => setTimeout(resolve, options.sleepMilliseconds ?? 300))

if (Date.now() - timeout * 1000 >= starting) {
throw new Error(`Lock ${key} is not acquired within ${timeout} seconds`)
}
}

if (cb) {
try {
await cb()
} finally {
await lock.release()
}
}
}
}
}

static async getLock (key: string, seconds: number, cb: () => Promise<void> | void) {
return await this.lock(key, seconds).get(cb)
static async getLock (key: string, cb: () => Promise<void> | void, options: { seconds?: number, owner?: string, sleepMilliseconds?: number } = {}) {
return await this.lock(key, options).get(cb)
}

static async block (key: string, timeout: number, cb: () => Promise<void> | void, options: { seconds?: number, owner?: string, sleepMilliseconds?: number } = {}) {
return await this.lock(key, options).block(timeout, cb)
}
}
64 changes: 59 additions & 5 deletions test/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('cache', () => {
})

test('atomic lock', async () => {
const lock = Cache.lock('lock', 10)
const lock = Cache.lock('lock')

if (await lock.get()) {
await wait(1000)
Expand All @@ -105,7 +105,7 @@ describe('cache', () => {
})

test('atomic lock - cb', async () => {
const acquired = await Cache.lock('lock-cb', 10).get(async () => {
const acquired = await Cache.lock('lock-cb').get(async () => {
await wait(1000)
})

Expand All @@ -114,10 +114,10 @@ describe('cache', () => {

test('atomic lock - multiple', async () => {
const [acquired, notAcquired] = await Promise.all([
Cache.lock('lock-multiple', 10).get(async () => {
Cache.lock('lock-multiple').get(async () => {
await wait(1000)
}),
Cache.lock('lock-multiple', 10).get(async () => {
Cache.lock('lock-multiple').get(async () => {
await wait(1000)
})
])
Expand All @@ -127,10 +127,64 @@ describe('cache', () => {
})

test('atomic lock - getLock', async () => {
const acquired = await Cache.getLock('get-lock', 10, async () => {
const acquired = await Cache.getLock('get-lock', async () => {
await wait(1000)
})

expect(acquired).toBe(true)
})

test('atomic lock - block', async () => {
const lock1 = Cache.lock('block')

await lock1.get()

setTimeout(async () => {
await lock1.release()
}, 1000)

const lock2 = Cache.lock('block')

await lock2.block(5)

expect(true).toBe(true)
})

test('atomic lock - block cb', async () => {
const lock1 = Cache.lock('block')

await lock1.get()

setTimeout(async () => {
await lock1.release()
}, 1000)

const lock2 = Cache.lock('block')

let lock2Acquired = false

await lock2.block(5, () => {
lock2Acquired = true
})

expect(lock2Acquired).toBe(true)
})

test('atomic block', async () => {
const lock1 = Cache.lock('block')

await lock1.get()

setTimeout(async () => {
await lock1.release()
}, 1000)

let lock2Acquired = false

await Cache.block('block', 5, () => {
lock2Acquired = true
})

expect(lock2Acquired).toBe(true)
})
})

0 comments on commit cd3e8d4

Please sign in to comment.