Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refc!: use new store, cleanup old files #7

Merged
merged 27 commits into from
Sep 17, 2023
Merged

refc!: use new store, cleanup old files #7

merged 27 commits into from
Sep 17, 2023

Conversation

gamemaker1
Copy link
Member

@gamemaker1 gamemaker1 commented Aug 18, 2023

package.json Outdated Show resolved Hide resolved
@gamemaker1 gamemaker1 marked this pull request as ready for review August 18, 2023 17:05
@nfriedly
Copy link
Member

Thank you for doing this! I've started to look through it, but I'm not going to be able to finish reviewing tonight. Hopefully I can find some time this weekend.

Copy link
Member

@nfriedly nfriedly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this! I have a few thoughts below, the biggest ones are

  1. I think we have to bring cjs back
  2. incr/decr flow
  3. accept locations & options to pass to the memcached client

Then there's a bunch of smaller nitpicky things

.github/workflows/ci.yaml Outdated Show resolved Hide resolved
.github/workflows/ci.yaml Outdated Show resolved Hide resolved
.github/workflows/ci.yaml Outdated Show resolved Hide resolved
license.md Outdated Show resolved Hide resolved
readme.md Show resolved Hide resolved
source/store.ts Outdated Show resolved Hide resolved
src/memcached-store.ts Show resolved Hide resolved
source/store.ts Outdated Show resolved Hide resolved
source/store.ts Outdated
Comment on lines 129 to 179
let record = await new Promise<ClientRecord>((resolve, reject) => {
this.client.get(key, (error, data) => {
if (error) {
reject(error)
return
}

resolve(data as ClientRecord)
})
})

if (record === undefined && delta < 0) {
// If the record does not exist, and we are supposed to decrement the key,
// we don't need to do anything, so we return.
return
}

if (record === undefined) {
// If a record does not exist, and must be incremented, then add a record
// for that key.
record = await new Promise<ClientRecord>((resolve, reject) => {
const payload: ClientRecord = { hits: 1, time: Date.now() }

this.client.add(key, payload, this.expiration, (error) => {
if (error) {
reject(error)
return
}

resolve(payload)
})
})
} else {
// If it does exist, simply change the hit count by `delta`.
record = await new Promise<ClientRecord>((resolve, reject) => {
const payload: ClientRecord = { ...record, hits: record.hits + delta }
const expiration = Math.floor(
// = window - (time elapsed since start of window) [in seconds]
this.expiration - (Date.now() - payload.time) / 1000,
)

this.client.replace(key, payload, expiration, (error) => {
if (error) {
reject(error)
return
}

resolve(payload)
})
})
}
Copy link
Member

@nfriedly nfriedly Aug 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the old flow of calling incr/decr. Calling get then create/update leaves it you open to race conditions where a bunch of requests come in at once, and all get the initial value then all overwrite eachother's additions.

It's also fewer requests in the common case of a returning visitor.

IMO, removing this function and just letting increment and decrement each have their own logic probably makes more sense here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that, but then we won't be able to return an accurate resetTime to the middleware.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should use the cas command instead of replace to avoid a race condition?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's a good point. I should familiarize myself with memcahed's commands.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about incr followed by either creating the key if it's not found, or mg (meta get), with the t to return the expiration time in seconds if the incr worked.

It's still two commands, but there's no risk of collision (after the initial creation) this way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, snap, the Meta Arithmetic command, ma is exactly what we want.

ma mykey N60 v t will:

  • create mykey if it doesn't exist with a expiration time of 60 seconds (N60)
  • increment it (MI, which is the default so I left it out) by 1 (D1, also default)
  • return the new value (v) and the expiration time (t)

That's a single memcached request per hit, regardless of weather or not the key already existed.

Similarly, ma mykey N60 MD v t will decrement (MD) it instead of incrementing.

I'm going to see if I can find a memcached client that supports meta commands, and I might send some PRs if I can't find one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just filed a ticket on memcached-client requesting support for meta commands. I'm kind of afraid that I just signed myself up for a ton of work, but I think it will make this a better library in the long run.

electrode-io/memcache#22

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, snap, the Meta Arithmetic command, ma is exactly what we want.

It is!! Great discovery!

electrode-io/memcache#22

Thanks for opening the issue, I'll do my best to help implement it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reply on the issue.. should we try pinging the maintainers?

Otherwise we could try using the private .command method on the library we are using right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, might as well try that

readme.md Outdated Show resolved Hide resolved
readme.md Outdated Show resolved Hide resolved
@gamemaker1
Copy link
Member Author

Thanks for the review comments, I'll address them by tomorrow!

@gamemaker1
Copy link
Member Author

gamemaker1 commented Aug 22, 2023

Made the suggested changes:

  • updated copyright notice in the license and readme files
  • used the lts version names in the workflow file
  • added the location and config options.
  • restored cjs support
  • renamed store.ts -> memcached-store.ts.

Copy link
Member

@nfriedly nfriedly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this got sidelined for a bit. I think I posted a good plan to move forward below.

config/jest.json Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
@gamemaker1
Copy link
Member Author

This should be ready to go!

@nfriedly
Copy link
Member

Awesome, I'll take another look sometime soon

Copy link
Member

@nfriedly nfriedly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

I left a ton of comments, but the expiresAt one is the only thing that looks like a real bug. The others are pretty minor.

readme.md Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved
readme.md Outdated
Comment on lines 41 to 43
> [pure `esm` package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).
> This means you cannot `require` it in `cjs` projects anymore. Please see the
> linked article for more information.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should delete this text also.

readme.md Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
source/memcached-store.ts Outdated Show resolved Hide resolved
@gamemaker1
Copy link
Member Author

I think I addressed all the review comments, let me know if I should change anything else.

Copy link
Member

@nfriedly nfriedly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

@gamemaker1 gamemaker1 merged commit 155a401 into main Sep 17, 2023
22 checks passed
@gamemaker1 gamemaker1 deleted the new-store branch September 17, 2023 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use new Store interface from express-rate-limit v6
2 participants