-
-
Notifications
You must be signed in to change notification settings - Fork 206
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
Why poll when using LISTEN/NOTIFY? #90
Comments
Thanks for the kind words π LISTEN/NOTIFY works for jobs scheduled immediately ( Just while I'm thinking of it. Currently the polling implementation is integrated with the Scheduler object, whereas it could also be integrated with the Notifier object. A process may have multiple schedulers but only a single notifier. For some configurations having the Notifier implement polling could be more efficient, but I'm not sure. |
If the GoodJob process wasn't running when a job was enqueued, would the polling be responsible for picking that job up when the GoodJob process starts also? |
Yes π Polling is necessary for proper functioning, and LISTEN/NOTIFY is nice-to-have. Though if you did want to disable polling, that's possible with |
Ah hah, clever stuff. If you had multiple processes monitoring the queue, then they would all get the NOTIFY but only one would be able to grab the advisory lock and the rest would continue listening and polling? |
Interesting! What I'd imagine is that enqueuing new jobs, immediate or scheduled, would post a simple "changed" message via NOTIFY. Then there would be a supervisor process which LISTENs, see the "changed" message, and checks to see if there's new immediate work βΒ and kick off workers βΒ or if the next scheduled time has changed, and then go back to sleep until the next scheduled job or LISTEN message. |
Having the Scheduler itself track when the next scheduled job is expected would be interesting. If you want to dive into the code you could make a proof of concept. And additional execution accuracy does introduce a (potential) problem of this, which is correct:
...meaning that any available Scheduler objects will all query the database at once trying to pick up that next job. Polling does (potentially) introduce a bit more randomness. But the challenge here is that different applications will have different kinds of workloads. |
I'll have a go. π Avoiding a thundering herd could be achieved by adding jitter, within the poll interval? |
This is the situation (and problem) that we have. In some circumstances we have a lot of small Heroku DelayedJob worker processes monitoring the queue (50+ or them) in order to parallelise the generation of 100's of thousands of fragments of XML. Each one takes a fraction of a second to generate, so scaling to 50 dynos lets us process them all in say 4 hours instead of many days. Polling does not work well in this situation, and nor does locking records by updating them! So moving to a system of slow polling and relying more on listen/notify would be great. Does it sound like GoodJob is a candidate for that? (Sorry, aware that this has gone a little off-topic). |
@slimdave tough to say, tbh. Scaled/production replacements are a lot more difficult than growing with a particular solution π It does though make me think that it would be helpful to unpack a couple of different scenarios, which went into my thinking for the GoodJob Scheduler (a scheduler has a single threadpool; a process may have multiple schedulers; there are potentially multiple processes running).
Depending on the application/workload each of these states may matter more or less. |
My question is the opposite -- why use LISTEN/NOTIFY, making more complex code and tying to postgres, if you have to poll anyway? Especially if you are polling every second by default -- I don't think any bg job system expects guaranteed latency of less than a second in a job getting picked up, it's a bg job system, some delay is expected. Would it be simpler to only poll if you need to poll anyway? Or maybe an option to disable listen/notify to use on non-postgres systems? Although that would still leave the complexity of the listen/notify code in the codebase to maintain. Well either way, that's for the future anyway, get it stable and mature on postgres first. |
@jrochkind those are great questions. GoodJob also uses Advisory Locks which are Postgres only too. I agree with you broadly. It's possible to make GoodJob be database agnostic, but right now my focus is on serving the needs of Postgres users and making GoodJob compelling, which is something I'm learning more about every day. |
This is released in |
Polling is not necessary when you use LISTEN/NOTIFY; it would provide you all the schedules of the jobs; so there is no need to poll. I've created skiplock to demonstrate this point. Note that you can gain quite a bit of performance if you don't need to poll. |
@vtt That's great! Thanks for sharing skiplock. I'm curious how you addressed some design challenges I've run into when thinking about how to entirely remove polling:
I'm curious about the performance benefits you're focused on too. Thanks again for hopping in here. |
Have a look at Skiplock::Job.dispatch function; it's quite short and might be easier to understand |
@bensheldon, On my machine using, your |
Realistically, how many have a job load that can be executed at more than 9K jobs/sec anyway? But skiplock looks awesome! It already supports forking multiple worker processes out of the box?? Is skiplock just a demo proof of concept at this point, not really ready for prime time? Very curious for the contents of currently empty "Retry" and "Notifications" docs systems. (Also it doesn't support multiple queues at present, i guess?) |
I just published the Multiple workers are supported ; as for queues, I'm still debating the design; it will be implemented once it's finalized. FYI, I created |
@vtt thanks! I'll review the queue-shootout PR. And also try to get back in the context of why I was sweating those design challenges. One behavioral difference I noticed is that skiplock wraps job execution in a transaction. Avoiding that was a goal of GoodJob (I want to support long-running jobs), which is why GoodJob uses session-level advisory locks. I'm not surprised that skiplock's SKIP LOCKED is faster than GoodJob's CTE-based advisory-locking query. There are other strategies like Sidekiq's super fetch to avoid a wrapping transaction, but with increased complexity. |
@bensheldon, why would you want to avoid the transaction? The downside is that each thread uses a dedicated database connection during dispatching; but the upside is guaranteed reliability and performance; if you have 5 workers each with 5 threads; then it will maximum use 25 connections in the pool; these connections cannot be used by Rails application; but in return, you get the safety and reliabily provided by PostgreSQL Note that once the job dispatches are completed, the used connections are checked back into pool. |
From your comment, I've just caught a design flaw in |
@vtt In my experience, database connection limits can become a bottleneck. For example, Heroku Hobby databases only allow 20 connections. I've also seen a lot of interest in PgBouncer for GoodJob (#52) so I think database connection management is on people's minds when choosing a backend... and PgBouncer compatibility is definitely on my own mind when thinking about how to evolve GoodJob. |
I would assume that if the server limits the database connections then it would limit the CPU/memory resources as well; so for this case, you would use 1 additional worker at most for background jobs (using only 5 connections) or better off using async mode by setting workers = 0 |
Every workload is different. I hit a lot of (slow) 3rd party APIs in one project and it's possible to stack up quite a bit of concurrency that's not CPU/Memory limited. I don't think there is a perfect implementation tbh, just different tradeoffs and targeted use-cases. |
Typical setup in Rails is going to hold the DB connection until the job is done anyway though. ActiveRecord connection handling is weird and messy. But if the domain-specific job code does any AR activities, that will check out a connection which will ordinarily remain checked out until end of the job In fact it's up the job wrapper to make sure it gets checked back in at all -- generally with a Rails executor/reloader. Sidekiq uses the Rails reloader. I'm not sure what good_job does -- if you are doing nothing, I think you are going to be leaking connections from any job that uses AR, which is not good. (correction maybe not, maybe ActiveJob takes care of it for you as in discussion at #97) I understand it's still worthwhile to not add to the problem -- after all, not every job uses AR at all, and there are ways to try to work around it with AR if you really want to. But for reasons of AR design, it's not easy to avoid holding onto a connection for duration of job execution if a job is using AR. |
@jrochkind you're right that GoodJob currently does implicitly hold onto the AR connection... but my brain is already on to #52 and solving for PgBouncer and using shared db-connections. GoodJob shares the same AR connection with the job, so 1-thread only uses 1 connection, whereas if I'm understanding it, skiplock uses potentially 2 (one holding a transaction for the job record, potentially a second for AR). |
Apologies if this thread becomes now a skiplock one, but this is following along some of the things on my mind with GoodJob. @vtt I noticed this change vtt/skiplock@73a1d0e you introduced a I'm gonna step away from this Issue, but happy to see you working in this space! |
@bensheldon , this was previously the case; but the recent change no longer requires this restriction; now the connection can be released back into the pool to be used by AR because the However, this would incur performance cost if the application handles a lot of background jobs. If there are 10000 jobs to be run, then it would requires 10000 checkins and 10000 checkouts for marking the jobs in progress and release the connections back to the pool. The benefit is the connection is available to be used by AR. Instead, if the working thread holds on to the dedicated connection, then it only check out once, finishes 10000 jobs, then release back the connection. The downside is that during the running of 10000 jobs, the connection is reserved and not available for AR use. Perhaps, I'd introduce a configurable option so the user can select which mode to use: performance or connection resource |
@bensheldon I apogolize for hijacking your thread; but have you address the problem with adivsory locks approach in The other job workers/threads reconnect to the freshly restarted database with no locks in memory and found the job "available". It dispatches another thread/worker to work on the job and now the job is being executed twice.... |
Regarding the orphaned job issue with the introduction of |
@vtt no worries about where this thread goes; I just wanted to recognize it if someone else arrives (including myself at some future time). A problem I see with vtt/skiplock@e41a80d is that someone starts a 2nd skiplock process (e.g. spins up another worker container), it will unlock the jobs on the first skiplock process. GoodJob currently uses a session-level Advisory Lock, which has the benefit of not requiring a transaction, and being implicitly unlocked if worker disconnects. But I'm also considering a SKIP LOCKED approach instead of Advisory Locks to better support PgBouncer. The strategy I'm considering on GoodJob (if I do move to a SKIP LOCKED approach) involves having a worker-heartbeat table and if a worker does not check-in in a certain amount of time, any jobs locked by that worker (also tracked on the job) are unlocked. |
One ancillary benefit of a worker heartbeat table is it could perhaps also support an admin interface which shows how many workers are active, and perhaps what they are doing. Resque admin gives you that information; I think sidekiq does too but not certain. |
@bensheldon please correct me if I get this wrong, but the session-level advisory lock is held in PostgreSQL memory (not on Rails side); and if the PG server crashes then restarts, all previous session-level advisory locks are cleared; so if you had a running worker processing a long job (eg. calculating some hypothetical bank payments), and the restart of database server is not known to the worker as it's working the job code. How do you prevent other workers/threads from fetching the same unfinished job that is now no longer locked by the session-level advisory lock due to restart of DB server... Wouldn't you then have two workers running the same job (ie: doing it more than once)? As for someone deliberating starting a 2nd worker process manually; this is sabotage; they may as well go wipe the data from the database. Even so, you can add a few lines in the startup script to prevent more than one instance of the master process running at the same time (eg. by checking the PID file to see if process is still running then exit). |
@vtt oh! the DB crashing is a really good situation I haven't considered. I'd assume that's pretty unexpected, but I'm not quite sure if AR would crash out or not in that instance. Regarding multiple workers, that's something GoodJob explicitly focused on to support containerized environments and horizontal scaling. Also, because writing on the Internet is hard, I am really glad you're in this space, and I'm not trying to poke holes in skiplock. Mostly just hoping you can contribute some ideas to GoodJob (this Issue itself is a lot of "why is this harder than it looks?" even before you arrived) and talking through this all is helpful for me π |
@bensheldon PG restarting is not rare at all; it happens every now and then even on top paid services like AWS RDMBS; it happens because of scheduled maintenance, upgrades or even just a pure crash. From Rails and AR side, it will report exceptions (lost connection to database) and keep trying to reconnect and continue on as usual. This usually lasts for 10-30 seconds (typical restart of PostgreSQL sever process); so you have to design In this situation, the only way to guarantee atomic execution of a job is to mark the Job record as "in progress "; so that no other process or thread can touch it. This can lead to orphaned jobs if the worker thread/process crashes or gets killed; but then these orphaned jobs should be treated just like any failed job (since it wasn't completed successfully) for retry on next master process restart. If a worker has died abnormally by crashing or killed then exceptions be generated and reported; and a pending restart of the master process is needed to resume normal operation; so the orphaned jobs will be retried then. |
If you are supporting horizontal scaling (ie: running multiple master processes on multiple machines), then you'd likely need to mark the jobs in progress tied to the host (eg. hostname or IP address). Instead of using a boolean for |
If this isn't an issue, maybe we could close or move to discussion. |
Absolutely loving good_job βΒ good job! π
I'm curious why it polls, though, when it supports LISTEN/NOTIFY?
The text was updated successfully, but these errors were encountered: