ixti/sidekiq-throttled

Throttling with Sidekiq Pro's super_fetch

Closed this issue ยท 40 comments

Blokh commented

Greetings, we've been using Sidekiq-Throttle gem for a while and it seems to work perfectly.
Lately we've implemented sidekiq pros's super_fetch.

For some reason, after this change sidekiq-throttling seems to be broken.

Do you know by any chance to fix this issue?
Could you provide me some guidance of how to implement sidekiq-throttling in a way that would not affect the fetch strategy?

Thanks in advance,
Daniel

We are using super_fetch icm sidekiq_throttled and it seems to work as expected.
Perhaps the initializer order matters? This how I init relevant parts of the server:

initializer 'sidekiq-server' do |app|
  Sidekiq.configure_server do |config|
    config.super_fetch!
  end
  Sidekiq::Throttled.setup!
end

We are on sidekiq 6.2.1.

From our observation, you can either use sidekiq_throttled or super_fetch, but not both, unfortunately.

To prevent any problems, add the .setup! call to the bottom of your init file.

^ If you follow this instruction from the readme or essentially do what @holstvoogd suggested, sidekiq_throttled is going to work. However, it'll override Sidekiq's custom fetch from Sidekiq::Pro::SuperFetch to Sidekiq::Throttled::Fetch.

However, it'll override Sidekiq's custom fetch from Sidekiq::Pro::SuperFetch to Sidekiq::Throttled::Fetch.

We've since learned this too, but at first glance it looks like it should be possible to 'wrap' the original fetcher, albeit the normal or super one, with the throttling. I'm hoping to look into that at some point and make a PR, but that might take a while...

ixti commented

I'll be happy to accept the PR. I was thinking to make fetch more composable to make it possible to easily hook into any other one.

@ixti I see you've setup a new repo for sidekick-throttled? Shall I make a PR there?
It might also be a good idea to put a little notice on this repo of the move? I hadn't noticed until I thought I was done ๐Ÿ˜…

I think I have a pretty decent solution that wraps the orignal fetcher, works with both SuperFetch or BasicFetch. It does require a few patches to BasicFetch, depending on the version of sidekiq, to make everything compatible.

Setup is quite straight forward, it just picks up the current fetcher by default. I think that leads to less suprises then having to pass it to #setup! manually, especially as under normal circumstances Sidekiq.options[:fetch] is nil init. I wouldn't want to break the default flow.

I've also made it possible to disable pausing/resuming queues & the enhanched queues tab; as this features is also provided by sk pro and having both active seems like trouble waiting to happen. Although it should still work if you really want to pause a queue twice ;)

I am not sure what to do for testing with Pro though.
I have confirmed it seems to work properly with sidekiq pro in a project, but can't really figure out a way to run spec agains pro. I can add apraisals for it, but you'd have to have a compatible version of sidekiq-pro installed already. And I have not been able to get SuperFetch to properly start in that context.

I could make some tests that use a mock and not actually hit sidekiq, but is that testing anything not covered by the existing tests? Main thing is that the SuperFetch 'api' needs to be stable, can't really test that with out pulling it in..

ixti commented

@holstvoogd I've lost any control over this repo, so I can't change README as well. :D
Please open PR in the new repo โ€“ I'll be happy to accept the changes!

Re configurability. Thanks for doing that. I was considering extracting the pausing feature away from this gem, as it's unrelated. As well as Communicator class, I think I made a mistake by introducing it in a first place. :D

On testing, I think we can mark tests as "require sidekiq-pro" and have extra appraisal for those. Thus, it will be possible to run test-suite against sidekiq-pro locally.

On testing, I think we can mark tests as "require sidekiq-pro" and have extra appraisal for those.

I'll see if I can get that to work ๐Ÿ‘

What is the status of the fix and is there any estimates when this PR may merged and released?

@ixti or @holstvoogd any progress with this?

ixti commented

I'm finishing preparation of v1.0.0, which should make this possible. The refactored fetcher class should become compatible with super-fetch out of the box (but I will need volounteers to check that). :D

ixti commented

Just released v1.0.0.alpha that should make sidekiq-throttled compatible with super fetch. But as I don't know hte details of sidekiq-pro, I can't test it myself.

@ixti What do you think of this? Am I making a mistake by configuring super_fetch! explicitly like so along with Sidekiq::Throttled.setup!
Here's a sample config that I have sidekiq.rb

require 'sidekiq/throttled'

Sidekiq.configure_server do |config|
  config.super_fetch!
end

Sidekiq::Throttled.setup!

Sidekiq::Throttled::Registry.add(....)

And here's the error I am getting when deploying

48 | on(:heartbeat) do
49 |   f.register_myself
50 | end

NoMethodError: undefined method `register_myself' for #<Sidekiq::Throttled::BasicFetch:0x00007fd087736a90...
/gems/sidekiq-pro-7.1.2/lib/sidekiq-pro.rb:49:in `block (3 levels) in super_fetch!'
/gems/sidekiq-7.1.1/lib/sidekiq/component.rb:60:in `block in fire_event'
/gems/sidekiq-7.1.1/lib/sidekiq/component.rb:59:in `each'
/gems/sidekiq-7.1.1/lib/sidekiq/component.rb:59:in `fire_event'
/gems/sidekiq-7.1.1/lib/sidekiq/launcher.rb:181:in `โค'
/gems/sidekiq-7.1.1/lib/sidekiq/launcher.rb:98:in `beat'
ixti commented

Hmm. I don't really know what is going inside of the register_myself. Without knowing what that method should do, the way I see, would be to provide a mixin for original basic fetch class that would simply overload some methods that I know to exist in the original class

I don't really know what is going inside of the register_myself.

For each capsules, the register_myself will register the capsules as super_processes

This seems to work with Sidekiq 6 Pro. With the v1.0.0.alpha version pause/unpause works and concurrency is also handled correctly.

The capsules where introduced in Sidekiq 7.

ixti commented

For each capsules, the register_myself will register the capsules as super_processes

Hmm. So, is should be something like?:

def register_myself
  @config.capsules.each_value do |capsule|
    capsule.super_process(self)
  end
end

FWIW I think the best way forward will be to simply provide something like:

Sidekiq::Throttled::Patches::BasicFetch.patch!(Sidekiq::BasicFetch)

That would prepend needed fixes to retrieve_work. Will add such one this week.

ixti commented

I'm gonna prepare a fix over the next couple of days, but I will need somebody to test it :D

ixti commented

I've changed approach of inheriting from Sidekiq::BasicFetch to directly patching its #retrieve_work that should make throttling work on both Sidekiq and Sidekiq::Pro seamingly.

ixti commented

Please give 1.0.0.alpha.1 a try.

For 1.0.0.alpha.1, I can enable the super_fetch without sidekiq erroring out, however, the throttling that I had in place with 1.0.0.alpha doesn't seem to work. Workers make calls as fast as possible without respecting the concurrency limit.

Update: If i disable super_fetch! the throttling works fine with 1.0.0.alpha.1. Once super_fetch! is enabled with 1.0.0.alpha.1, the throttling won't work (There would be no 'throttled' key in redis)

ixti commented

@Parth-Rewind Can you check which fetch class is being used once super_fetch is required and what are the ancestors of the class?

@Parth-Rewind Can you check which fetch class is being used once super_fetch is required and what are the ancestors of the class?

With 1.0.0.alpha.1, the fetch class will be set to :fetch_class => Sidekiq::Pro::SuperFetch < Object

Doing

Sidekiq::BasicFetch.ancestors

Gives me this

[
  Sidekiq::Throttled::Patches::BasicFetch,
  Sidekiq::BasicFetch,
  Sidekiq::Component,
  ActiveSupport::Dependencies::RequireDependency,
  ActiveSupport::ToJsonWithActiveSupportEncoder,
  Object, ActiveSupport::Tryable,
  JSON::Ext::Generator::GeneratorMethods::Object,
  PP::ObjectMixin,
  Debase::PrivateMultiProcess,
  Debase::MultiProcess,
  Kernel,
  BasicObject
]

My config file looks like so:

require 'sidekiq/throttled'

Sidekiq.configure_server do |config|
  config.super_fetch!
end

Sidekiq::Throttled.setup!

Sidekiq::Throttled::Registry.add(....)
ixti commented

Oh. That explains. Okay will prepare a fix shortly.

Oh. That explains. Okay will prepare a fix shortly.

Thank You!!

When you have time, could you please tell me what was causing this? I am curious. Is that the fetch_class is not being set to Sidekiq::Throttled::Patches::BasicFetch

ixti commented

The fetch class after config.super_fetch! is set to Sidekiq::Pro::SuperFetch. Which does not inherit the behaviour of Sidekiq::BasicFetch. I'm gonna change how fetch class is being patched, to correctly patch super fetch.

Thank you very much @ixti so far for actively working on fixing this issue.
Just wanted to know, when is it planned to move these fixes in alpha to a stable release branch?

ixti commented

After trying to get my head around Sidekiq-Pro, I realize I need somebody with sidekiq-pro to answer a couple of questions. Otherwise, I'm acting in a dark place with my eyes shut, trying to guess. And I have a hunch that my guess will lead to some breakage without knowing the expected API contract:

  • How to notify super-fetch that job was pushed back?
  • How does retrieve_work gets list of queues to poll?
    • Is it calling queues_cmd?
    • If so, is the expected response is just queues list?

I can try to answer your questions about Sidekiq-Pro @ixti! We really appreciate all the hard work you're putting into this

  • How to notify super-fetch that job was pushed back?

Sidekiq::Pro::SuperFetch defines Sidekiq::Pro::SuperFetch::UnitOfWork which is similar to Sidekiq::BasicFetch::UnitOfWork and both have a requeue method. The SuperFetch version uses Lua to requeue the job. The current thread's unit of work is made available to Sidekiq::Batch as Thread.current["sfuow"]

  • How does retrieve_work gets list of queues to poll?
    • Is it calling queues_cmd?

In Sidekiq-Pro 7.1.3,

  • Sidekiq::Pro::BasicFetch
    • Is prepended onto Sidekiq::BasicFetch
    • Overrides queues_cmd to check if any queues are paused (so they can be skipped) before calling super
  • Sidekiq::Pro::SuperFetch
    • Completely independent from either BasicFetch implementation
    • Does not implement queues_cmd and instead has its own implementation of retrieve_work, but it relies on a public method called queues which just gets the queues from the capsule

Is there any progress with this?

Can you link the pull request with this issue? It easier to give feedback or contribute if can see the current implementation.

FYI, I've been running a custom fork of the sidekiq-pro-support branch of this gem in production for a while now, with support for Sidekiq Pro 5.5, 7.0 and 7.1: https://github.com/bookingexperts/sidekiq-throttled/tree/sidekiq-pro-support. Seems to be running fine so far, no guarantees though. This fork also brings back the old method of maintaining an ExpirableList with throttled queues to prevent thrashing Redis when most jobs in a queue are throttled.

Thank you @gstokkink, I will look into it. Do you have any plans to upstream the patch to original gem as well or it's complicated, because the appraisals and specs need the pro gem access?

That's one reason why it probably won't be upstreamed, yeah. Also, we are currently looking into replacing Sidekiq Pro with Sidekiq Ent entirely in the not-too-distant future, which would mean we no longer need sidekiq-throttled.

Sorry everyone for being slow and unresponsive. Had some issues, but seems like I'm getting back on track time-wise, so will work this week on merging/fixing the annoying bugs

Glad to hear things are improving for you @ixti I'm happy to assist wherever/however helpful, including testing with Sidekiq Pro and super_fetch

@mnovelo I'm gonna focus on releasing 1.0.0 with a partial support of Sidekiq-Pro - namely only paused queues will be covered for now, as I need to find a way to correctly hook into super_fetch. Thus, if you can provide some insights on how to get the list of paused queues - that will help significantly!

If we're just wanting to support paused queues in Sidekiq-Pro, you can prepend Sidekiq::Pro::BasicFetch instead of Sidekiq::BasicFetch. Their APIs are not much different and you can use queues_cmd in both to get the list of non-paused queues

ixti commented

I've added some changes to make Sidekiq::Pro::Basic fetch play nicely with Sidekiq::Throttled. Will release 1.0.0 shortly today, after merging queues exhausting PR.

Sidekiq-Pro super fetch compatibility will not be part of 1.0.0 release. Will try to add it sometime later.

ixti commented

@gstokkink I'll be more than happy to add you as collaborator if you want to maintain full sidekiq-pro support. Regarding access to Sidekiq-Pro and appraisal - I think this can be managed by having some env-based conditionals in Appraisals.

ixti commented

Finally, after all this time Sidekiq-Pro super fetch support was landed in v1.3.0

Thanks everyone for your patience and collaboration!