Forum: Ruby how do I impose a method call delay?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Jason S. (Guest)
on 2006-06-07 21:35
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?
unknown (Guest)
on 2006-06-07 22:51
(Received via mailing list)
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
Chris (Guest)
on 2006-06-07 22:51
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
Chris (Guest)
on 2006-06-07 22:56
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!
Jason S. (Guest)
on 2006-06-07 22:57
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!
Lloyd Z. (Guest)
on 2006-06-08 00:41
(Received via mailing list)
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.
Jason S. (Guest)
on 2006-06-08 00:46
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...
Lloyd Z. (Guest)
on 2006-06-08 01:39
(Received via mailing list)
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.
Jason S. (Guest)
on 2006-06-08 01:43
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!
Lloyd Z. (Guest)
on 2006-06-08 02:07
(Received via mailing list)
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.
This topic is locked and can not be replied to.