Skip to content

Commit 15f2420

Browse files
authored
perf: rewrite using lua scripting when is possible (#15)
1 parent 2474876 commit 15f2420

File tree

4 files changed

+77
-14
lines changed

4 files changed

+77
-14
lines changed

src/stats.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,44 @@
11
'use strict'
22

3+
const { promisify } = require('util')
4+
const stream = require('stream')
5+
6+
const { Transform } = stream
7+
8+
const pipeline = promisify(stream.pipeline)
9+
310
const formatYYYMMDDDate = (now = new Date()) => {
411
const year = now.getFullYear()
512
const month = String(now.getMonth() + 1).padStart(2, '0')
613
const day = String(now.getDate()).padStart(2, '0')
714
return `${year}-${month}-${day}`
815
}
916

17+
/**
18+
* 90 days in milliseconds
19+
*/
20+
const TTL = 90 * 24 * 60 * 60 * 1000
21+
22+
/**
23+
* Lua script to increment a key and set an expiration time if it doesn't have one.
24+
* This script is necessary to perform the operation atomically.
25+
*/
26+
const LUA_INCREMENT_AND_EXPIRE = `
27+
local key = KEYS[1]
28+
local ttl = ARGV[1]
29+
local quantity = ARGV[2]
30+
local current = redis.call('incrby', key, quantity)
31+
if tonumber(redis.call('ttl', key)) == -1 then
32+
redis.call('expire', key, ttl)
33+
end
34+
return current
35+
`
36+
1037
module.exports = ({ redis, prefix }) => {
1138
const prefixKey = key => `${prefix}stats:${key}`
1239

13-
const increment = async keyValue => {
14-
redis.incr(`${prefixKey(keyValue)}:${formatYYYMMDDDate()}`)
15-
}
40+
const increment = (keyValue, quantity = 1) =>
41+
redis.eval(LUA_INCREMENT_AND_EXPIRE, 1, `${prefixKey(keyValue)}:${formatYYYMMDDDate()}`, TTL, quantity)
1642

1743
/**
1844
* Get stats for a given key.
@@ -30,15 +56,24 @@ module.exports = ({ redis, prefix }) => {
3056
* // ]
3157
*/
3258
const stats = async keyValue => {
33-
const keys = await redis.keys(prefixKey(`${keyValue}*`))
3459
const stats = []
60+
const stream = redis.scanStream({ match: `${prefixKey(keyValue)}*` })
61+
const dataHandler = new Transform({
62+
objectMode: true,
63+
transform: async (keys, _, next) => {
64+
if (keys.length) {
65+
const values = await redis.mget.apply(redis, keys)
66+
const statsPart = keys.map((key, i) => ({
67+
date: key.replace(`${prefixKey(keyValue)}:`, ''),
68+
count: Number(values[i])
69+
}))
70+
stats.push.apply(stats, statsPart)
71+
}
72+
next()
73+
}
74+
})
3575

36-
for (const key of keys) {
37-
const date = key.replace(`${prefixKey(keyValue)}:`, '')
38-
const count = Number(await redis.get(key))
39-
stats.push({ date, count })
40-
}
41-
76+
await pipeline(stream, dataHandler)
4277
return stats.sort((a, b) => a.date.localeCompare(b.date))
4378
}
4479

src/usage.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ module.exports = ({ plans, keys, redis, stats, prefix, serialize, deserialize })
2929

3030
let usage = deserialize(await redis.get(prefixKey(keyValue)))
3131

32-
// TODO: move into lua script
3332
if (usage === null) {
3433
usage = {
3534
count: quantity,
@@ -45,7 +44,9 @@ module.exports = ({ plans, keys, redis, stats, prefix, serialize, deserialize })
4544
}
4645

4746
const pending =
48-
quantity > 0 && Promise.all([redis.set(prefixKey(keyValue), serialize(usage)), stats.increment(keyValue)])
47+
quantity > 0
48+
? Promise.all([redis.set(prefixKey(keyValue), serialize(usage)), stats.increment(keyValue, quantity)])
49+
: Promise.resolve([])
4950

5051
return {
5152
limit: plan.limit,

test/stats.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ testCleanup({
2424
])
2525
})
2626

27-
test('.stats', async t => {
27+
test.serial('.increment # by one', async t => {
2828
const plan = await openkey.plans.create({
2929
id: randomUUID(),
3030
limit: 3,
@@ -59,3 +59,31 @@ test('.stats', async t => {
5959
{ date: openkey.stats.formatYYYMMDDDate(addDays(Date.now(), 2)), count: 5 }
6060
])
6161
})
62+
63+
test.serial('.increment # by more than one', async t => {
64+
const plan = await openkey.plans.create({
65+
id: randomUUID(),
66+
limit: 3,
67+
period: '100ms'
68+
})
69+
70+
const key = await openkey.keys.create({ plan: plan.id })
71+
const data = await openkey.usage.increment(key.value)
72+
await data.pending
73+
74+
Date.setNow(addDays(Date.now(), 1))
75+
await openkey.usage.increment(key.value, 10)
76+
await data.pending
77+
78+
Date.setNow(addDays(Date.now(), 1))
79+
await openkey.usage.increment(key.value, 5)
80+
await data.pending
81+
82+
Date.setNow()
83+
84+
t.deepEqual(await openkey.stats(key.value), [
85+
{ date: openkey.stats.formatYYYMMDDDate(), count: 1 },
86+
{ date: openkey.stats.formatYYYMMDDDate(addDays(Date.now(), 1)), count: 10 },
87+
{ date: openkey.stats.formatYYYMMDDDate(addDays(Date.now(), 2)), count: 5 }
88+
])
89+
})

test/usage.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ test('.increment', async t => {
3131

3232
const key = await openkey.keys.create({ plan: plan.id })
3333
let data = await openkey.usage(key.value)
34-
t.is(data.pending, false)
3534
t.is(data.remaining, 3)
3635
data = await openkey.usage.increment(key.value)
3736
await data.pending

0 commit comments

Comments
 (0)