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

Strict priority polling w/ backmerge #288

Merged
merged 5 commits into from
Dec 21, 2016

Conversation

atyndall
Copy link
Contributor

@phstc This is the strict priority polling implementation from #263 with master backmerged, it didn't make it when you closed #263 in favour of #284.

@phstc
Copy link
Collaborator

phstc commented Dec 14, 2016

@mariokostelac would you mind reviewing it?

@mariokostelac
Copy link
Contributor

@atyndall could you rebase on master first please? We merged my change in the master.

@atyndall
Copy link
Contributor Author

@mariokostelac Should be done now

@@ -19,11 +19,49 @@ def ==(other)
end

alias_method :eql?, :==

def to_s
options.empty? ? name : super
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was to tidy up log messages such as these:

Before change;

2016-12-15T23:38:08Z 88280 TID-oxeiaih3s DEBUG: Looking for new messages in '#<struct Shoryuken::Polling::QueueConfiguration name="development_atyndall_affinity_0", options={}>'
2016-12-15T23:38:08Z 88280 TID-oxeiaih3s DEBUG: Fetcher for '#<struct Shoryuken::Polling::QueueConfiguration name="development_atyndall_affinity_0", options={}>' completed in 268.11899999999997 ms

After change;

2016-12-15T23:39:56Z 88353 TID-out2291p8 DEBUG: Looking for new messages in 'development_atyndall_affinity_0'
2016-12-15T23:39:56Z 88353 TID-out2291p8 DEBUG: Fetcher for 'development_atyndall_affinity_0' completed in 217.197 ms

Seeing as QueueConfigurations are considered equal if have no options and their strings are equal, it made sense to me to make their string representation just the names of the queues to tidy up logging. This could also be handled by changing inspect, I'm open to either way.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make a case where options does exist a little bit nicer? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made some changes

.each_with_object({}) { |queue, h| h[queue] = [true, nil] }

# Most recently used queue
@current_queue = nil
Copy link
Contributor

Choose a reason for hiding this comment

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

From what I understand, that it actually last_used_queue. Can we name it like that? It will make more sense for the loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rework has eliminated variable altogether

.map(&:first)

# Pause status of the queues
@queue_status = @queue_order
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can compress queue_status in just one field, something like "activation time". When Time.now < activation_time, queue is not active yet. When Time.now >= activation_time, queue is active.


# Priority ordering of the queues
@queue_order = @queue_priorities
.to_a
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to simplify this code or add a comment of the datastructure coming out :).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍


# Loop through the queue order from the current queue until we find a
# queue that is next in line and is not paused
while true
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use a for loop? This external bookkeeping of variable i looks ugly.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not drop-in but we have similar code in our custom stuff that works like this:

queues = @queue_order.dup

while true
  queue = queues.first
  # process queue
  queues.rotate!
end

So rather than maintaining i or @current_queue you dup the ordered queues at some point and then Array#rotate! them as needed to move to the next queue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gshutler Excellent! I didn't know about Array#rotate!, I'll rework the code around it.


i += 1
return queue if active
return nil if i >= @queue_order.length # Prevents infinite looping
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, using For loop gets rid of this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

logger.debug "Paused '#{queue}'"
end

def unpause_queues
Copy link
Contributor

Choose a reason for hiding this comment

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

Once we compress the state, we do not have to unpause queues because we know their order, we just have to check if activation_time > Time.now or not :).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, I'll implement this approach.

expect(subject.next_queue).to eq(queue2)
end

it 'cycles when declared asc' do
Copy link
Contributor

Choose a reason for hiding this comment

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

why looks the same as previous one, while priorities are different?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The priorities aren't different, the priorities are the number. They are declaration order independent.

Therefore

[shoryuken, 2]
[uppercut,  1]

and

[uppercut,  1]
[shoryuken, 2]

Have the same meaning, because they have the same priority numbers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, did not see that queues are reversed!

@mariokostelac
Copy link
Contributor

@atyndall could you please describe the algorithm you want to implement? That's the only way we can be sure we're on the same page :).


# Priority ordering of the queues
@queue_order = @queue_priorities
.to_a
Copy link
Contributor

Choose a reason for hiding this comment

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

@queue_order = queues.group_by(&:itself).sort_by { |q, qs| -qs.count }.map(&:first)

May have to do the longhand of Object#itself if you need support for Ruby < 2.2.0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice, will use instead

@atyndall
Copy link
Contributor Author

I have made significant changes to the implementation taking into account the feedback, thanks!

@mariokostelac Sure, at a high level each queue is assigned a unique priority, describing how critical its work is.

When shoryuken goes to select a new job, it chooses the highest priority unpaused queue to check first.

If that queue contains no work, it will proceed to check the next highest priority unpaused queue, etc.

When new work is received or when a queue unpauses, the checking order is reset to check the highest priority queue once more.

@mariokostelac
Copy link
Contributor

@atyndall so that's priority queue, right? As long as there are jobs in a queue with the highest priority, poll that queue. Once it has no jobs, pause it for delay period and continue with the rest of queues?

If yes, we can just use https://ruby-doc.org/stdlib-2.3.1/libdoc/set/rdoc/SortedSet.html.

@atyndall
Copy link
Contributor Author

@mariokostelac Yeah that's correct.
I don't really understand what changes you're envisioning with the use of a SortedSet

Copy link
Contributor

@mariokostelac mariokostelac left a comment

Choose a reason for hiding this comment

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

I think we have to figure out how to release that. This change is actually moving from "one polling strategy" model to "choose your polling strategy" model.

I suggest we do something similar to:

  • we do not share options between polling strategies (no sharing delay option, it's confusing)
  • we cut a new version where we deprecate delay parameter (it's confusing anyway). The new version also has a way to choose polling strategy and parameters are scoped per polling strategy (while weighted round robin still supporting delay for some time).

WDYT @phstc?

@queue_order = @initial_order.dup

# Pause status of the queues, default to past time (unpaused)
@paused_until = queues
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

pause(queue)
else
# Reset the queue order to the initial ordering
@queue_order = @initial_order.dup
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we're using this reset_queue_order several times, can we isolate that into a method? If we do so, we do not even need a comment. Let the method name talk! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will add such a method


# `rotate!` through the queue list until we find an unpaused queue
begin
next_queue = @queue_order.first
Copy link
Contributor

Choose a reason for hiding this comment

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

What is stored in here? It seems that .map(&:first) in initializer stores just the q in here, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's getting refactored out now


allow(subject).to receive(:delay).and_return(10)

now = Time.now
Copy link
Contributor

Choose a reason for hiding this comment

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

Good practice 👍

now = Time.now

# Return nil if all queues are paused to prevent infinite loop
return nil if @paused_until.values.all? { |t| t > now }
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, I know I am picky here but this code seems a little bit complicated to me :). Since our number of queues has to be low (100 at most), we do not have to think about speed too much and we can implement a straightforward solution - iterate over queues from index last_index (like you did before) and just check if a queue is paused. The first one that is not - has to be returned. If it ever gets slow (I really doubt that :)), we can optimize this class, but I'd rather have something that is very simple and everybody can understand, fast!

I think it's far simpler than maintaining that initial_order, having a check from line 174, making sure that we call dup every time we want to get a copy etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

The whole next_active_queue would be very simple, something like:

def next_active_queue
   size = @queues.length
   for 0..limit do
      queue = @queues[@next_queue_index]
      @next_queue_index = (@next_queue_index + 1) % size
      return queue unless queue_paused?(queue)
   end
   return nil
end

We would be able to have very simple, reusable queue_paused? method.
Also, reset_next_queue is simple as @next_queue_index = 0. We have to call it in initializer, too.

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I'll implement that approach.

end

def active_queues
@paused_until
Copy link
Contributor

Choose a reason for hiding this comment

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

Also, once we have queue_paused?, we can use something simple as

@queues.reject{ |q| queue_paused?(q) }

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's far easier to understand

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add the method

Copy link
Contributor Author

@atyndall atyndall left a comment

Choose a reason for hiding this comment

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

Made additional changes to simplify methods further, and move them into smaller submethods.

now = Time.now

# Return nil if all queues are paused to prevent infinite loop
return nil if @paused_until.values.all? { |t| t > now }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I'll implement that approach.

end

def active_queues
@paused_until
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add the method

pause(queue)
else
# Reset the queue order to the initial ordering
@queue_order = @initial_order.dup
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will add such a method

@@ -19,11 +19,49 @@ def ==(other)
end

alias_method :eql?, :==

def to_s
options.empty? ? name : super
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made some changes


# `rotate!` through the queue list until we find an unpaused queue
begin
next_queue = @queue_order.first
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's getting refactored out now

@phstc
Copy link
Collaborator

phstc commented Dec 19, 2016

@mariokostelac I think most of people they don't care about which polling strategy they need to use, the only want to make it work. The shoryuken.yml by default (in the samples/README) should be something like this

concurrency: 25  # The number of allocated threads to process messages. Default 25
queues:
  - [high_priority, 6]
  - [default, 2]
  - [low_priority, 1]

I would remove the delay from the sample, I agree it's confusing. Most people, only want to install, configure the queues and run it. Maybe we could also omit concurrency from the samples/README, not sure if it's needed. Most people only need the defaults, these extra options can be documented in the wiki. Including the polling strategy, I'm still seeing it as "edge case".

With that, I wouldn't do any major release, I would just let "choose your polling strategy" something special for now, documented in the wiki. Keeping shoryuken as simple and straightforward as possible, I don't mind about keeping the delay, default strategy will be probably the most used one.

WDYT?

Is this change introducing any breaking changes?

@atyndall
Copy link
Contributor Author

@phstc AFAIK there are no breaking changes, merging this in should not have any impact on anyone who doesn't use it.

For your information, this is what our queueing config looks like atm;

polling_strategy: !ruby/class 'Shoryuken::Polling::StrictPriority'
timeout: 1140   # Seconds grace to shutdown
concurrency: 1  # The number of allocated threads to process messages. Default 25
delay: 60       # The delay in seconds to pause a queue when it's empty. Default 0
queues:
  - [high_priority,       3]
  - [medium_priority,     2]
  - [low_priority,        1]

@phstc
Copy link
Collaborator

phstc commented Dec 19, 2016

@atyndall nice! polling_strategy, timeout and delay are still optional, right?

@atyndall
Copy link
Contributor Author

@phstc Yep

@mariokostelac
Copy link
Contributor

@atyndall @phstc I am fine with having a default polling strategy being set and all associated parameters, but I am not really happy with sharing these parameters between polling strategies. They could have slightly or completely different meanings. That's why I'd like to namespace these parameters in new version :). Also, delay is not descriptive at all.

@phstc phstc merged commit b48342e into ruby-shoryuken:master Dec 21, 2016
@phstc
Copy link
Collaborator

phstc commented Dec 21, 2016

That's why I'd like to namespace these parameters in new version :). Also, delay is not descriptive at all.

@mariokostelac I agree, I just want to make the basic shoryuken.yml as trivial as possible. Most of people don't care about these extra settings, as long as we can have a good set of defaults, all is fine 🍻

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.

4 participants