Skip to content

Implement throttler for non-core db functions to improve postgres performance - #318

Merged
JonoPrest merged 15 commits into
mainfrom
jp/implement-debounce
Nov 6, 2024
Merged

Implement throttler for non-core db functions to improve postgres performance#318
JonoPrest merged 15 commits into
mainfrom
jp/implement-debounce

Conversation

@JonoPrest

Copy link
Copy Markdown
Collaborator
  1. Adds debounce module to the envio package
  2. Moves Pino bindings and Utils to envio package
  3. Adds rescript-schema to envio package
  4. Uses debounce instead of LazyWriter for Benchmark.res
  5. Uses debounce instead of async task queue for update chain metadata
  6. Applies debounce to cleanup db functions
  7. Adds tests for debounce module

@JonoPrest
JonoPrest requested a review from DZakh November 4, 2024 12:46

@DZakh DZakh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I didn't take at the Debouncer code yet. Just a few early comments, I'll continue in 30 minutes

"dependencies": {
"rescript": "11.1.3"
"rescript": "11.1.3",
"rescript-schema": "8.2.0"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should keep the package.json.tmpl dependencies in sync. Need to add rescript-schema there as well

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ah good spot 👌🏼

let t = (logger, exn, message) =>
logger->Pino.errorExn(message->Pino.createPinoMessageWithError(exn))

%%private(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's have a .resi file instead

@JonoPrest JonoPrest Nov 5, 2024

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Can you elaborate on why you think so?

IMO this is better for the given case unless we want opaque types (I'd prefer visible types to make it easier to test). This way you don't need to maintain 2 files and keep signatures/types matching.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok, I thought we want it to be opaque

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I like opaque types from an API perspective, but it makes it less testable and more encapsulated. I'm keen to try not use too much encapsulation unless it solves footguns. At which point you can just have an inner module of exposed getter functions for testing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I prefer the black-box approach for unit testing, when we test only the public API the way how the user would interact with it. This is more reliable since we test exactly how the code is used. Also, when some internal implementation changes, it doesn't require us to update the tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fair, but it's still more boilerplate just to hide simple data etc. I'm wondering how much the black-box ceremony helps us vs just creates overhead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I got used to writing the code this way, so I think this is not a big difference. But might be annoying sometimes

Comment on lines +19 to +20
let t = (logger, exn, message) =>
logger->Pino.errorExn(message->Pino.createPinoMessageWithError(exn))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this used?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nope, that was accidentally left in 👍🏼

@DZakh DZakh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good. But this is more like a throttler than a debouncer. Debounce restarts the timer every time a new function is scheduled, while the throttle keeps the original timer

Assert.deepEqual(
actionsCalled,
[1, 2],
~message="Scheduler should have been called straight after the initial interval",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is Throttler then, not a Debouncer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Kind of but if you look at the first test it also has debouncing properties. Open to renaming but this is the behaviour I was looking for.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Maybe Debottler? 🙃

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Jk, I think we can call it Throttler, it makes more sense 👍🏼

if timeSinceLastRun >= debouncer.intervalMillis {
debouncer.isRunning = true
debouncer.scheduled = None
debouncer.lastRunTimeMillis = Js.Date.now()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we maybe set it after the fn call?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The way I see it, it's not a cooldown period so much as an interval, it can work either way it just makes sense in my head to have a time interval from when the last function was called, not necessarily finished.

Say a cleanup function gets called on the postgres and takes really long awaiting a transaction. By the time it's finished we can immediately execute the next scheduled function if it's passed the interval.

Comment on lines +45 to +52
await Js.Promise2.make((~resolve, ~reject as _) => {
let _ = Js.Global.setTimeout(() => {
debouncer.isAwaitingInterval = false
resolve()
}, Belt.Int.fromFloat(timeOutInterval))
})

await debouncer->startInternal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd say we don't need a promise here. You can call debouncer->startInternal directly from Js.Global.setTimeout

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

True! It's just then that the resolve of startInternal is not indicative of it actually finishing.

Comment on lines +11 to +17
let logger = Logging.createChild(
~params={
"context": "Debouncer for chain metadata writes",
"intervalMillis": intervalMillis,
},
)
Debouncer.make(~intervalMillis, ~logger)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this is convenient here, but isn't it more correct to pass the logger from where we call schedule, instead of when we create the Debouncer

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It can work either way, then we just store the logger here in this state and pass where it get's called. I guess it's just a preference of where we store the logger. I think it's better than constructing a new logger on every call unless we want to pass in different context values, which might be nice as well 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Actually after looking back at the code, it is best this way.

startInternal will recurse through scheduled calls, so if that takes the logger as a param then it can only hand down the same logger even if it's a different scheduled call.

If you actually want a logger to be tied to where it gets scheduled then the "scheduled" state should also have a logger. I think we can do that but it's not needed yet.

@JonoPrest
JonoPrest requested a review from DZakh November 5, 2024 16:20
@JonoPrest
JonoPrest force-pushed the jp/implement-debounce branch from 96282e3 to 401dfa6 Compare November 5, 2024 16:58

@DZakh DZakh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

@JonoPrest
JonoPrest merged commit b4b2894 into main Nov 6, 2024
@JonoPrest
JonoPrest deleted the jp/implement-debounce branch November 6, 2024 08:04
@JonoPrest JonoPrest changed the title Implement Debounce for Cleanup & UI db functions Implement throttler for non-core db functions to improve postgres performance Nov 6, 2024
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.

2 participants