Skip to content
This repository was archived by the owner on Nov 4, 2021. It is now read-only.

Process events in batches - #39

Merged
mariusandra merged 70 commits into
masterfrom
process-events
Dec 9, 2020
Merged

Process events in batches#39
mariusandra merged 70 commits into
masterfrom
process-events

Conversation

@mariusandra

@mariusandra mariusandra commented Dec 7, 2020

Copy link
Copy Markdown
Collaborator

Still WIP

Ready for review (comments below)

@mariusandra

Copy link
Copy Markdown
Collaborator Author

This is ready for a look!

Inside the PR:

  • Add support for a processEventBatch(batch: PluginEvent[], meta: PluginMeta) function inside plugins that can operate on a batch of events. For example to send 100 events at once to S3 instead of making 100 HTTP requests.
  • Either write processEvent or processEventBatch in your plugin. The other gets stubbed automatically.
  • Batches are split by team - all events in one batch are for the same team
  • Remove support for Posthog.capture inside processEvent and processEventBatch. Segment also doesn't support this in their destination functions. The problem is that since $identity events don't contain a token field, we can't initialize the posthog instance like we can for other events. We can add this back later by asking for the API key from postgres. However I'd just add it then to the scheduled/webhook plugins and not the processEvent side of thing to avoid infinite loops.
  • Update worker benchmark tests to see if batching makes a difference. (Spoiler: it does... will reply in the next comment)

@mariusandra
mariusandra requested a review from Twixes December 8, 2020 14:30
@mariusandra

Copy link
Copy Markdown
Collaborator Author

I added one more fix since batching seemed to slow down sync events. Now in the stubbed processEventBatch function we only await Promise.all([...]) if we need to, greatly speeding things up!

Tested on a M1 Air. I got up to 100k events/sec with batches of 100. 🚀

// Before last fix
      ┌─────────┬────────────────┬───────────┬────────┬───────────┬──────────┬───────────┬───────────┬───────────┐
      │ (index) │    testName    │ coreCount │ events │ batchSize │ 1 thread │ 2 threads │ 4 threads │ 8 threads │
      ├─────────┼────────────────┼───────────┼────────┼───────────┼──────────┼───────────┼───────────┼───────────┤
      │    0    │    'simple'    │     8     │ 10000  │     1     │  39453   │   70847   │   81106   │   76728   │
      │    1    │    'simple'    │     8     │ 10000  │    10     │  24070   │   40898   │   54639   │   59051   │
      │    2    │    'simple'    │     8     │ 10000  │    100    │  30800   │   52167   │   69978   │   73826   │
      │    3    │   'for200k'    │     8     │ 10000  │     1     │  12650   │   22944   │   37368   │   43391   │
      │    4    │   'for200k'    │     8     │ 10000  │    10     │   9495   │   17370   │   27272   │   35226   │
      │    5    │   'for200k'    │     8     │ 10000  │    100    │  10329   │   19656   │   33916   │   37400   │
      │    6    │ 'timeout100ms' │     8     │ 10000  │     1     │   1010   │   1987    │   3352    │   5101    │
      │    7    │ 'timeout100ms' │     8     │ 10000  │    10     │   8839   │   16482   │   27245   │   31118   │
      │    8    │ 'timeout100ms' │     8     │ 10000  │    100    │  16397   │   29250   │   38102   │   38000   │
      └─────────┴────────────────┴───────────┴────────┴───────────┴──────────┴───────────┴───────────┴───────────┘

// After last fix
      ┌─────────┬────────────────┬───────────┬────────┬───────────┬──────────┬───────────┬───────────┬───────────┐
      │ (index) │    testName    │ coreCount │ events │ batchSize │ 1 thread │ 2 threads │ 4 threads │ 8 threads │
      ├─────────┼────────────────┼───────────┼────────┼───────────┼──────────┼───────────┼───────────┼───────────┤
      │    0    │    'simple'    │     8     │ 10000  │     1     │  44184   │   70350   │   79122   │   81577   │
      │    1    │    'simple'    │     8     │ 10000  │    10     │  31499   │   51685   │   75783   │   82023   │
      │    2    │    'simple'    │     8     │ 10000  │    100    │  46066   │   75966   │   99487   │  115690   │
      │    3    │   'for200k'    │     8     │ 10000  │     1     │  12684   │   23340   │   37095   │   42061   │
      │    4    │   'for200k'    │     8     │ 10000  │    10     │  10462   │   18992   │   31391   │   35820   │
      │    5    │   'for200k'    │     8     │ 10000  │    100    │  11667   │   22335   │   39604   │   43295   │
      │    6    │ 'timeout100ms' │     8     │ 10000  │     1     │   1011   │   1981    │   3417    │   5115    │
      │    7    │ 'timeout100ms' │     8     │ 10000  │    10     │   8801   │   16270   │   26732   │   31813   │
      │    8    │ 'timeout100ms' │     8     │ 10000  │    100    │  16475   │   28314   │   33264   │   38892   │
      └─────────┴────────────────┴───────────┴────────┴───────────┴──────────┴───────────┴───────────┴───────────┘

Comment thread src/plugins.ts Outdated
Comment thread src/plugins.ts Outdated

for (const pluginConfig of pluginsToRun.reverse()) {
if (pluginConfig.vm?.methods?.processEventBatch && returnedEvents.length > 0) {
const { processEventBatch } = pluginConfig.vm.methods

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.

Nit: could be a bit more concise with less lookups if we moved this line one level up (to right below the for (const pluginConfig... line) with optional chaining and in the if just checked for processEventBatch.

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.

cleaned it up a bit

Comment thread src/vm.ts
Comment on lines +68 to +83
// we have processEvent, but not processEventBatch
if (!__getExported('processEventBatch') && __getExported('processEvent')) {
exports.processEventBatch = async function processEventBatch (batch, meta) {
const processEvent = __getExported('processEvent');
let waitFor = false
const processedEvents = batch.map(event => {
const e = processEvent(event, meta)
if (e && typeof e.then !== 'undefined') {
waitFor = true
}
return e
})
const response = waitFor ? (await Promise.all(processedEvents)) : processedEvents;
return response.filter(r => r)
}
// we have processEventBatch, but not processEvent

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.

Where do we handle the case when there are both processEvent and processEventBatch?

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 user handled it for us in that case. I think it's fine for the user to give both functions (e.g. making custom batching that only works with max 10 events), yet if they want to give just one of the two, the system should still work. Also if they give zero, the system should work... and then just do the plugin differently.

Comment thread tests/worker.test.ts Outdated
Comment thread tests/worker.test.ts
@mariusandra mariusandra mentioned this pull request Dec 9, 2020
Comment thread src/plugins.ts Outdated
Comment thread tests/worker.test.ts Outdated
@mariusandra
mariusandra merged commit e2adfb4 into master Dec 9, 2020
@mariusandra
mariusandra deleted the process-events branch December 9, 2020 10:09
fuziontech pushed a commit to PostHog/posthog that referenced this pull request Oct 12, 2021
* silence some test logs

* admit defeat in the battle with `esModuleInterop`

* add basic typescript piscina workers

* es module fix

* remove rollup and compile directly with typescript

* fix fetch import error

* make piscina work in jest, dist and dev... run plugins through it

* simplify tests

* store plugin status on the `server`

* config types

* not all keys needed

* add worker concurrency setting

* clean up code

* fix test worker code

* add a basic benchmark test

* refactor test

* refactor test utils

* support more serialized binary formats

* mock jest behind enemy lines

* less verbose logs in tests

* add test to make sure adding more CPU cores makes plugins faster!

* clone objects

* add 12

* fix linter's worries

* remove extra word

* improve display

* remove the exact cpu count from the list

* add postgres and redis services to github to make tests pass

* add ENV to config

* add ENV to config

* process events test wip

* add db:init script

* add db:init script to CI

* add db:init script

* add db:init script to CI

* No prepublish on yarn install

* processEvents in vm

* run plugins on batches of events

* tests for batching performance

* async processEvent & events

* lint

* performance test batches

* not a dev run

* prettier for ts

* prettier

* remove support for PostHog.capture inside `processEvent` for now. This will go back inside the scheduled/webhook plugins

* less round in tests, increase timeout

* refactor

* slight cleanup

* rename `processEvents` to `processEventBatch`

* only wait in processEventBatch if needed

* Order file extensions more readably in lint-staged

* Prettier

* remove time rounding

* less lookups

* describe test devmode

* add "light"

* Remove redundant Math.round

* Run prettier

* Fix performance measurement

* Prettier but correctly

Co-authored-by: Michael Matloka <dev@twixes.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants