How do I impose a method call delay?


#1

I’ve got a simple Ruby class that has one method which uses open-uri to
pass a request to a remote server and retrieve an xml document
containing some server statistics. Nothing too exciting and it’s
working fine. The administrator has asked that we limit our calls to
once every 30 seconds to avoid hammering the server. I’d like to bake
this forced delay right into my class so that if another application is
using it and tries to make 2 calls within 30 seconds, the second call is
delayed until the 30 second time period is up. Any ideas on how I could
approach this?


#2

On Thu, 8 Jun 2006, Jason S. wrote:

I’ve got a simple Ruby class that has one method which uses open-uri to pass
a request to a remote server and retrieve an xml document containing some
server statistics. Nothing too exciting and it’s working fine. The
administrator has asked that we limit our calls to once every 30 seconds to
avoid hammering the server. I’d like to bake this forced delay right into
my class so that if another application is using it and tries to make 2
calls within 30 seconds, the second call is delayed until the 30 second time
period is up. Any ideas on how I could approach this?

here’s an idea for a thread-safe impl:

harp:~ > cat a.rb
#! /usr/bin/env ruby
require ‘sync’

module Delay
module ClassMethods
def delay m, t
m, t = m.to_s, t.to_f
dm = “delayed#{ m }_”
module_eval <<-code
alias_method “#{ dm }”, “#{ m }”
def #{ m }(*a, &b)
delay_init
delay("#{ m }"){ #{ dm }(*a, &b) }
end
code
delay_time[m] = t
end
def delay_last() @delay_last ||= {} end
def delay_time() @delay_time ||= Hash.new{|h,k| h[k] = 0.0}
end
end

module InstanceMethods
def delay_last() self.class.delay_last end
def delay_time() self.class.delay_time end
def delay_init() extend Sync_m unless Sync_m === self end
def delay m
m = m.to_s
synchronize do
last, now = delay_last[m], Time.now
if last
already = now.to_f - last.to_f
t = delay_time[m] - already
sleep t if t > 0
end
delay_last[m] = Time.now
yield
end
end
end

def self.included other
other.extend ClassMethods
other.module_eval{ include InstanceMethods }
end
end

class C
include Delay

def foo
STDOUT.sync = true
puts “#{ Thread.current.object_id } : #{ Time.now }”
end

delay ‘foo’, 2
end

c = C.new

t1 = Thread.new { 2.times{ c.foo } }
t2 = Thread.new { 2.times{ c.foo } }

t1.join
t2.join

harp:~ > ruby a.rb
-609340922 : Wed Jun 07 12:48:22 MDT 2006
-609341842 : Wed Jun 07 12:48:24 MDT 2006
-609340922 : Wed Jun 07 12:48:26 MDT 2006
-609341842 : Wed Jun 07 12:48:28 MDT 2006

regards.

-a


#3

Jason S. wrote:

I’ve got a simple Ruby class that has one method which uses open-uri to
pass a request to a remote server and retrieve an xml document
containing some server statistics. Nothing too exciting and it’s
working fine. The administrator has asked that we limit our calls to
once every 30 seconds to avoid hammering the server. I’d like to bake
this forced delay right into my class so that if another application is
using it and tries to make 2 calls within 30 seconds, the second call is
delayed until the 30 second time period is up. Any ideas on how I could
approach this?

class politeGet
def initialize(delay=30)
@delay = delay
@last_get = nil
end

def getDoc(uri)
tdiff = Time.now.to_i - @last_get.to_i #diff in seconds
sleep(tdiff+1) if (tdiff) < @delay #force desired delay with fudge
as sleep() may return early
open(uri)
@last_get = Time.now.to_i
end
end

not tested or even executed but should be close 9^)

Cheers
Chris


#4

Thanks to both of you…I think Chris’ idea is probably sufficient for
my needs, but I’m going to try to wrap my head around the other solution
too!


#5

Chris wrote:

open(uri)
@last_get = Time.now.to_i

end
end

Need to swap the last two statements in getDoc so it returns the doc and
not @last_get…doh!


#6

Chris removed_email_address@domain.invalid writes:

open(uri)
@last_get = Time.now.to_i

end
end

not tested or even executed but should be close 9^)

Well, this does force a delay, but I’m pretty sure that it won’t solve
the problem described by Jason S…

What this solution does is remember the last time that the getDoc method
was called for a given instance of the politeGet class. However, Mr.
Salis explained that the delay has to apply even when another
application has to use the class. In this case, there will be a
completely new instance of politeGet (running in a completely new
process, to boot), and therefore, the delay mechanism described here
will not work, because each instance has its own last_get variable.

Even if you made the last_get variable into a class variable (by
preceding it with two ‘@’ signs instead of one), the delay for one
application will still be independent of the delay for another, because
different applications run within different ruby interpreters, and their
class variables are therefore separate.

And finally, even if you could somehow set this up to run within a
single application, the way this is written will still not cause the
desired throttling. The following example illustrates. In it, I assume
the exact code as written above, except for the fact that last_get is
set up as a class variable (i.e., it begins with “@@”). Note that I’m
ignoring locking and other threading and concurrency issues for the
purpose of making this example easier to understand:

10:00:00 - User A invokes getDoc for the first time.
getDoc sets last_get to 10:00:00 and returns
10:00:05 - User B invokes getDoc.
getDoc waits because 30 seconds have not yet
passed since the last call completed
10:00:15 - User C invokes getDoc.
getDoc waits because 30 seconds have not yet
passed since the last call completed
10:00:25 - User D invokes getDoc.
getDoc waits because 30 seconds have not yet
passed since the last call completed
10:00:30 - User B’s call wakes up and services the
request, and last_get is set to 10:00:30
10:00:30 - User C’s call wakes up and services the
request, and last_get is set to 10:00:30
10:00:30 - User D’s call wakes up and services the
request, and last_get is set to 10:00:30

(this assumes that the uri requests each take significantly less than a
second to fulfill)

Notice that users B, C, and D all have their requests serviced within
one second, which violates the requirement that only one request takes
place within any given 30 second period.

The solution offered by Ara T. Howard will work here, as long as all the
invocations of getDoc are performed within the same application (the
same instance of the Ruby interpreter). For multiple applications,
however, something even more complicated would be needed, because each
Ruby interpreter contains its own Delay module. In this case, the Delay
module would have to use some kind of centrally accessible resource
(disk file, data base, etc.) to keep track of the next available time
slot.

Unfortunately, I don’t have time to write that code at the moment,
but it should be easy to add to Ara T. Howard’s solution.


#7

My apologies, Lloyd…after re-reading my initial post, the language was
a little vague. Chris actually did get it right. I need the class to
be useable by a number of other classes, but each individual instance of
the class should impose its own delay. Although, after reading your
solution, it may be better to implement it your way in the future (time
permitting). I was looking at it as if each consuming application was a
“client” and each “client” should have the delay imposed, but I may want
to rewrite my class and think of IT as the “client” and impose the delay
globally for all consuming apps. Something to think about for me…


#8

Jason S. removed_email_address@domain.invalid writes:

My apologies, Lloyd…after re-reading my initial post, the language was
a little vague. Chris actually did get it right. I need the class to
be useable by a number of other classes, but each individual instance of
the class should impose its own delay. Although, after reading your
solution, it may be better to implement it your way in the future (time
permitting). I was looking at it as if each consuming application was a
“client” and each “client” should have the delay imposed, but I may want
to rewrite my class and think of IT as the “client” and impose the delay
globally for all consuming apps. Something to think about for me…

Well, thanks for clearing that up. However, Chris’s procedure, as
written (i.e., without threading), will also not give you the results
you are hoping for. The following is a sample chronology (note that
each user has his/her own instance of last_get):

10:00:00 - User A invokes getDoc.
User A’s last_get is set to 10:00:00 and getDoc returns.
10:00:01 - User B invokes getDoc.
User B’s last_get is set to 10:00:01 and getDoc returns.
10:00:02 - User C invokes getDoc.
User C’s last_get is set to 10:00:02 and getDoc returns.
10:00:10 - User A calls getDoc.
the process sleeps until 10:00:30 and then getDoc returns
after setting User A’s last_get to 10:00:30.
*** NOTE: Without threading, the entire app will be hung
in the ‘sleep’ call within this getDoc method.
Users B or C cannot do anything, because they
are all part of the same non-threaded app.
10:00:30 - User B invokes getDoc. It cannot make this call any
earlier, because the entire app is sleeping during
the call above.
The process sleeps until 10:00:31 (because the last
call that User B made was at 10:00:01), and then
it returns after setting User B’s last_get to 10:00:31
*** NOTE: Again, the entire app is waiting on the
‘sleep’ call, and users A and C cannot do
anything.
10:00:31 - User C invokes getDoc. It cannot make this call any
earlier, because the entire app is sleeping during
the calls above.
The process sleeps until 10:00:32 (because the last
call that User C made was at 10:00:02), and then
it returns after setting User C’s last_get to 10:00:32
*** NOTE: Again, the entire app is waiting on the
‘sleep’ call, and users A and B cannot do
anything.

Notice that three URI access were made within three seconds, one group
of three between 10:00:00 and 10:00:02, and the next group of three
between 10:00:30 and 10:00:32. If you had 10 users, you could have as
many as 10 requests, one right after the other at around 10:00:00, and
then 10 more requests, one right after the other at around 10:00:30.
This works out to 10 requests every 30 seconds.

If you give each user its own thread, then you will get a chronology
closer to what you want. However, with multiple threads, it’s easily
possible for requests to go to the web server at a much faster rate than
once every 30 seconds. For example, for 10 users each having their own
thread, this methodology results in web server get up to one hit every 3
seconds.

Maybe this is OK. But if not, you’ll have to rethink this approach.


#9

Jason S. removed_email_address@domain.invalid writes:

More food for thought. I’m going to collect some more information
around the desired usage and make a call on design. thanks again to
everyone for ideas!

Well, recall that Ara T. Howard’s methodology will indeed ensure that no
more than one call every 30 seconds will get made to the web server, as
long as all of the calls are made within a single instance of the Ruby
interpreter. Since you already said that this is the way your
application will run, then Mr. Howard’s procedure already solves your
problem.


#10

More food for thought. I’m going to collect some more information
around the desired usage and make a call on design. thanks again to
everyone for ideas!