qu-scheduler 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/MIT-LICENSE +2 -1
  2. data/README.md +191 -0
  3. data/lib/qu-scheduler.rb +1 -4
  4. data/lib/qu-scheduler/tasks.rb +35 -0
  5. data/lib/qu/extensions/scheduler.rb +102 -0
  6. data/lib/qu/extensions/scheduler/redis.rb +199 -0
  7. data/lib/qu/scheduler.rb +227 -0
  8. data/lib/qu/scheduler/version.rb +2 -2
  9. data/test/delayed_queue_test.rb +277 -0
  10. data/test/redis-test.conf +115 -0
  11. data/test/scheduler_args_test.rb +156 -0
  12. data/test/scheduler_hooks_test.rb +51 -0
  13. data/test/scheduler_test.rb +181 -0
  14. data/test/test_helper.rb +91 -7
  15. metadata +64 -76
  16. data/README.rdoc +0 -7
  17. data/lib/tasks/qu-scheduler_tasks.rake +0 -4
  18. data/test/dummy/Rakefile +0 -7
  19. data/test/dummy/app/assets/javascripts/application.js +0 -9
  20. data/test/dummy/app/assets/stylesheets/application.css +0 -7
  21. data/test/dummy/app/controllers/application_controller.rb +0 -3
  22. data/test/dummy/app/helpers/application_helper.rb +0 -2
  23. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  24. data/test/dummy/config.ru +0 -4
  25. data/test/dummy/config/application.rb +0 -45
  26. data/test/dummy/config/boot.rb +0 -10
  27. data/test/dummy/config/database.yml +0 -25
  28. data/test/dummy/config/environment.rb +0 -5
  29. data/test/dummy/config/environments/development.rb +0 -30
  30. data/test/dummy/config/environments/production.rb +0 -60
  31. data/test/dummy/config/environments/test.rb +0 -39
  32. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  33. data/test/dummy/config/initializers/inflections.rb +0 -10
  34. data/test/dummy/config/initializers/mime_types.rb +0 -5
  35. data/test/dummy/config/initializers/secret_token.rb +0 -7
  36. data/test/dummy/config/initializers/session_store.rb +0 -8
  37. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  38. data/test/dummy/config/locales/en.yml +0 -5
  39. data/test/dummy/config/routes.rb +0 -58
  40. data/test/dummy/db/test.sqlite3 +0 -0
  41. data/test/dummy/log/test.log +0 -0
  42. data/test/dummy/public/404.html +0 -26
  43. data/test/dummy/public/422.html +0 -26
  44. data/test/dummy/public/500.html +0 -26
  45. data/test/dummy/public/favicon.ico +0 -0
  46. data/test/dummy/script/rails +0 -6
data/MIT-LICENSE CHANGED
@@ -1,4 +1,5 @@
1
- Copyright 2011 YOURNAME
1
+ Copyright 2011 Morton Jonuschat
2
+ Some parts copyright 2010 Ben VandenBos
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
data/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # Qu::Scheduler
2
+
3
+ ## Description
4
+
5
+ qu-scheduler is an extension to [Qu](http://github.com/bkeepers/qu)
6
+ that adds support for queueing jobs in the future.
7
+
8
+ Currently qu-scheduler only works with qu-redis and requires Redis 2.0 or newer.
9
+
10
+ Job scheduling is supported in two different ways: Recurring (scheduled) and
11
+ Delayed.
12
+
13
+ Scheduled jobs are like cron jobs, recurring on a regular basis. Delayed
14
+ jobs are Qu jobs that you want to run at some point in the future.
15
+ The syntax is pretty explanatory:
16
+
17
+ Qu.enqueue_in(5.days, SendFollowupEmail) # run a job in 5 days
18
+ # or
19
+ Qu.enqueue_at(5.days.from_now, SomeJob) # run SomeJob at a specific time
20
+
21
+ ## Installation
22
+
23
+ # Rails 3.x: add it to your Gemfile
24
+ gem 'qu-scheduler'
25
+
26
+ There are just a single thing `qu-scheduler` needs to know about in order
27
+ to do it's jobs: the schedule. The easiest way to configure these things
28
+ is via the rake task. By default, `qu-scheduler` depends on the "qu:setup"
29
+ rake task. Since you probably already have this task, lets just put our
30
+ configuration there. `qu-scheduler` pretty much inherits everything else
31
+ from Qu.
32
+
33
+ # Qu tasks
34
+ require 'qu/tasks'
35
+ require 'qu-scheduler/tasks'
36
+
37
+ namespace :qu do
38
+ task :setup do
39
+ require 'qu'
40
+ require 'qu-scheduler'
41
+
42
+ # If you want to be able to dynamically change the schedule,
43
+ # uncomment this line. A dynamic schedule can be updated via the
44
+ # Qu::Scheduler.set_schedule (and remove_schedule) methods.
45
+ # When dynamic is set to true, the scheduler process looks for
46
+ # schedule changes and applies them on the fly.
47
+ # Note: This feature is still under development
48
+ # Qu::Scheduler.dynamic = true
49
+
50
+ # The schedule doesn't need to be stored in a YAML, it just needs to
51
+ # be a hash. YAML is usually the easiest.
52
+ Qu.schedule = YAML.load_file(Rails.root.join('config', 'your_resque_schedule.yml'))
53
+
54
+ # If your don't depend on your application environment you need to
55
+ # require your jobs here. Qu determines the queue exclusively from the
56
+ # class, so we need to have access to them.
57
+ require 'jobs'
58
+ end
59
+ end
60
+
61
+ The scheduler process is just a rake task which is responsible for both
62
+ queueing jobs from the schedule and polling the delayed queue for jobs
63
+ ready to be pushed on to the work queues. For obvious reasons, this process
64
+ never exits.
65
+
66
+ $ bundle exec rake qu:scheduler
67
+
68
+ NOTE: You DO NOT want to run more than one instance of the scheduler. Doing
69
+ so will result in the same job being queued multiple times. You only need one
70
+ instance of the scheduler running per application, regardless of number of servers.
71
+
72
+ If the scheduler process goes down for whatever reason, the delayed items
73
+ that should have fired during the outage will fire once the scheduler process
74
+ is started back up again (even if it is on a new machine). Missed scheduled
75
+ jobs, however, will not fire upon recovery of the scheduler process.
76
+
77
+ ## Delayed jobs
78
+
79
+ Delayed jobs are one-off jobs that you want to be put into a queue at some point
80
+ in the future. The classic example is sending email:
81
+
82
+ Qu.enqueue_in(5.days, SendFollowUpEmail, current_user.id)
83
+
84
+ This will store the job for 5 days in the Qu delayed queue at which time
85
+ the scheduler process will pull it from the delayed queue and put it in the
86
+ appropriate work queue for the given job. It will then be processed as soon as
87
+ a worker is available (just like any other Qu job).
88
+
89
+ NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
90
+ time is in the past, the job moves from the delayed queue to the actual work
91
+ queue and will be completed as workers as free to process it.
92
+
93
+ Also supported is `Qu.enqueue_at` which takes a timestamp to queue the job.
94
+
95
+ The delayed queue is stored in redis and is persisted in the same way the
96
+ standard Qu jobs are persisted (redis writing to disk). Delayed jobs differ
97
+ from scheduled jobs in that if your scheduler process is down or workers are
98
+ down when a particular job is supposed to be processed, they will simply "catch up"
99
+ once they are started again. Jobs are guaranteed to run (provided they make it
100
+ into the delayed queue) after their given queue_at time has passed.
101
+
102
+ Your jobs can specify one or more `before_schedule` and `after_schedule` hooks,
103
+ to be run before or after scheduling. If *any* of your `before_schedule` hooks
104
+ returns `false`, the job will *not* be scheduled and your `after_schedule` hooks
105
+ will *not* be run.
106
+
107
+ One other thing to note is that insertion into the delayed queue is O(log(n))
108
+ since the jobs are stored in a redis sorted set (zset). I can't imagine this
109
+ being an issue for someone since redis is stupidly fast even at log(n), but full
110
+ disclosure is always best.
111
+
112
+ ### Removing Delayed jobs
113
+
114
+ If you have the need to cancel a delayed job, you can do it like this:
115
+
116
+ # after you've enqueued a job like:
117
+ Qu.enqueue_at(5.days.from_now, SendFollowUpEmail, current_user.id)
118
+ # remove the job with exactly the same parameters:
119
+ Qu.remove_delayed(SendFollowUpEmail, current_user.id)
120
+
121
+ ## Scheduled Jobs (Recurring Jobs)
122
+
123
+ Scheduled (or recurring) jobs are logically no different than a standard cron
124
+ job. They are jobs that run based on a fixed schedule which is set at
125
+ startup.
126
+
127
+ The schedule is a list of job classes with arguments and a schedule frequency
128
+ (in crontab syntax). The schedule is just a hash, but is most likely stored in
129
+ a YAML like this:
130
+
131
+ queue_documents_for_indexing:
132
+ cron: "0 0 * * *"
133
+ # you can use rufus-scheduler "every" syntax in place of cron if you prefer
134
+ # every: 1hr
135
+ klass: QueueDocuments
136
+ args:
137
+ description: "This job queues all content for indexing in solr"
138
+
139
+ clear_leaderboards_contributors:
140
+ cron: "30 6 * * 1"
141
+ klass: ClearLeaderboards
142
+ args: contributors
143
+ description: "This job resets the weekly leaderboard for contributions"
144
+
145
+ NOTE: Six parameter cron's are also supported (as they supported by
146
+ rufus-scheduler which powers the resque-scheduler process). This allows you
147
+ to schedule jobs per second (ie: "30 * * * * *" would fire a job every 30
148
+ seconds past the minute).
149
+
150
+ A big shout out to [rufus-scheduler](http://github.com/jmettraux/rufus-scheduler)
151
+ for handling the heavy lifting of the actual scheduling engine.
152
+
153
+ ## Running in the background
154
+
155
+ (Only supported with ruby >= 1.9). There are scenarios where it's helpful for
156
+ the resque worker to run itself in the background (usually in combination with
157
+ PIDFILE). Use the BACKGROUND option so that rake will return as soon as the
158
+ worker is started.
159
+
160
+ $ PIDFILE=./qu-scheduler.pid BACKGROUND=yes bundle exec rake qu:scheduler
161
+
162
+ ## Additional information
163
+
164
+ by Ben VandenBos to the Qu queuing library.
165
+
166
+ ## Note on Patches / Pull Requests
167
+
168
+ * Fork the project.
169
+ * Make your feature addition or bug fix.
170
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
171
+ * Commit, do not mess with rakefile, version, or history.
172
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
173
+ * Send me a pull request. Bonus points for topic branches.
174
+
175
+ ## Credits
176
+
177
+ This work is a port of [resque-scheduler](https://github.com/bvandenbos/resque-scheduler) by Ben VandenBos.
178
+ Modified to work with the Qu queueing library by Morton Jonuschat.
179
+
180
+ ## Maintainers
181
+
182
+ * [Morton Jonuschat](https://github.com/yabawock)
183
+
184
+ ## License
185
+
186
+ MIT License
187
+
188
+ ## Copyright
189
+
190
+ Copyright 2011 Morton Jonuschat
191
+ Some parts copyright 2010 Ben VandenBos
data/lib/qu-scheduler.rb CHANGED
@@ -1,4 +1 @@
1
- module Qu
2
- module Scheduler
3
- end
4
- end
1
+ require 'qu/scheduler'
@@ -0,0 +1,35 @@
1
+ require 'qu/tasks'
2
+
3
+ namespace :qu do
4
+ desc "Start Qu Scheduler"
5
+ task :scheduler => :scheduler_setup do
6
+ require 'qu'
7
+ require 'qu-scheduler'
8
+
9
+ if ENV['BACKGROUND']
10
+ unless Process.respond_to?('daemon')
11
+ abort "Environment variable BACKGROUND is set, which requires ruby >= 1.9"
12
+ end
13
+ Process.daemon(true)
14
+ end
15
+
16
+ File.open(ENV['PIDFILE'], 'w') { |f| f << Process.pid.to_s } if ENV['PIDFILE']
17
+
18
+ Qu::Scheduler.dynamic = true if ENV['DYNAMIC_SCHEDULE']
19
+ Qu::Scheduler.run
20
+ end
21
+
22
+ task :scheduler_setup do
23
+ if ENV['INITIALIZER_PATH']
24
+ load ENV['INITIALIZER_PATH'].to_s.strip
25
+ else
26
+ Rake::Task['qu:setup'].invoke
27
+ end
28
+ end
29
+
30
+ # No-op task in case it doesn't already exist
31
+ task :setup
32
+ end
33
+
34
+ # Convenience tasks compatibility
35
+ task 'resque:scheduler' => 'qu:scheduler'
@@ -0,0 +1,102 @@
1
+ module Qu
2
+ module Extensions
3
+ module Scheduler
4
+ autoload :Redis, 'qu/extensions/scheduler/redis'
5
+
6
+ #
7
+ # Accepts a new schedule configuration of the form:
8
+ #
9
+ # {'some_name' => {"cron" => "5/* * * *",
10
+ # "class" => "DoSomeWork",
11
+ # "args" => "work on this string",
12
+ # "description" => "this thing works it"s butter off"},
13
+ # ...}
14
+ #
15
+ # 'some_name' can be anything and is used only to describe and reference
16
+ # the scheduled job
17
+ #
18
+ # :cron can be any cron scheduling string :job can be any resque job class
19
+ #
20
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
21
+ # for valid syntax. If :cron is present it will take precedence over :every.
22
+ #
23
+ # :class must be a resque worker class
24
+ #
25
+ # :args can be any yaml which will be converted to a ruby literal and
26
+ # passed in a params. (optional)
27
+ #
28
+ # :rails_envs is the list of envs where the job gets loaded. Envs are
29
+ # comma separated (optional)
30
+ #
31
+ # :description is just that, a description of the job (optional). If
32
+ # params is an array, each element in the array is passed as a separate
33
+ # param, otherwise params is passed in as the only parameter to perform.
34
+ def schedule=(schedule_hash)
35
+ if Qu::Scheduler.dynamic
36
+ schedule_hash.each do |name, job_spec|
37
+ backend.set_schedule(name, job_spec)
38
+ end
39
+ end
40
+ @schedule = schedule_hash
41
+ end
42
+
43
+ # Returns the schedule hash
44
+ def schedule
45
+ @schedule ||= {}
46
+ end
47
+
48
+ # reloads the schedule from redis
49
+ def reload_schedule!
50
+ @schedule = backend.get_schedules
51
+ end
52
+
53
+ # This method is nearly identical to +enqueue+ only it also
54
+ # takes a timestamp which will be used to schedule the job
55
+ # for queueing. Until timestamp is in the past, the job will
56
+ # sit in the schedule list.
57
+ def enqueue_at(timestamp, klass, *args)
58
+ before_hooks = before_schedule_hooks(klass).collect do |hook|
59
+ klass.send(hook,*args)
60
+ end
61
+ return false if before_hooks.any? { |result| result == false }
62
+
63
+ backend.delayed_push(timestamp, Payload.new(:klass => klass, :args => args))
64
+
65
+ after_schedule_hooks(klass).collect do |hook|
66
+ klass.send(hook,*args)
67
+ end
68
+ end
69
+
70
+ # Identical to enqueue_at but takes number_of_seconds_from_now
71
+ # instead of a timestamp.
72
+ def enqueue_in(number_of_seconds_from_now, klass, *args)
73
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
74
+ end
75
+
76
+ # Returns an array of delayed items for the given timestamp
77
+ def delayed_timestamp_peek(timestamp, start, count)
78
+ if 1 == count
79
+ r = backend.list_range "delayed:#{timestamp.to_i}", start, count
80
+ r.nil? ? [] : [r]
81
+ else
82
+ backend.list_range "delayed:#{timestamp.to_i}", start, count
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def before_schedule_hooks(klass)
89
+ klass.methods.grep(/^before_schedule/).sort
90
+ end
91
+
92
+ def after_schedule_hooks(klass)
93
+ klass.methods.grep(/^after_schedule/).sort
94
+ end
95
+
96
+ end
97
+ end
98
+ end
99
+ Qu.send :extend, Qu::Extensions::Scheduler
100
+ if defined? Qu::Backend::Redis
101
+ Qu::Backend::Redis.send :include, Qu::Extensions::Scheduler::Redis
102
+ end
@@ -0,0 +1,199 @@
1
+ module Qu
2
+ module Extensions
3
+ module Scheduler
4
+ module Redis
5
+ # Does the dirty work of fetching a range of items from a Redis list
6
+ # and converting them into Ruby objects.
7
+ def list_range(key, start = 0, count = 1)
8
+ if count == 1
9
+ decode redis.lindex(key, start)
10
+ else
11
+ Array(redis.lrange(key, start, start+count-1)).map do |item|
12
+ decode item
13
+ end
14
+ end
15
+ end
16
+
17
+ # gets the schedule as it exists in redis
18
+ def get_schedules
19
+ if redis.exists(:schedules)
20
+ redis.hgetall(:schedules).tap do |h|
21
+ h.each do |name, config|
22
+ h[name] = decode(config)
23
+ end
24
+ end
25
+ else
26
+ nil
27
+ end
28
+ end
29
+
30
+ # Create or update a schedule with the provided name and configuration.
31
+ #
32
+ # Note: values for class and custom_job_class need to be strings,
33
+ # not constants.
34
+ #
35
+ # Qu.set_schedule('some_job', { :class => 'SomeJob',
36
+ # :every => '15mins',
37
+ # :queue => 'high',
38
+ # :args => '/tmp/poop' })
39
+ def set_schedule(name, config)
40
+ existing_config = get_schedule(name)
41
+ unless existing_config && existing_config == config
42
+ redis.hset(:schedules, name, encode(config))
43
+ redis.sadd(:schedules_changed, name)
44
+ end
45
+ config
46
+ end
47
+
48
+ # retrieve the schedule configuration for the given name
49
+ def get_schedule(name)
50
+ decode(redis.hget(:schedules, name))
51
+ end
52
+
53
+ # remove a given schedule by name
54
+ def remove_schedule(name)
55
+ redis.hdel(:schedules, name)
56
+ redis.sadd(:schedules_changed, name)
57
+ end
58
+
59
+ def update_schedule
60
+ if redis.scard(:schedules_changed) > 0
61
+ Qu::Scheduler.set_process_title "updating schedule"
62
+ Qu.reload_schedule!
63
+ while schedule_name = redis.spop(:schedules_changed)
64
+ if Qu.schedule.keys.include?(schedule_name)
65
+ Qu::Scheduler.unschedule_job(schedule_name)
66
+ Qu::Scheduler.load_schedule_job(schedule_name, Qu.schedule[schedule_name])
67
+ else
68
+ Qu::Scheduler.unschedule_job(schedule_name)
69
+ end
70
+ end
71
+ Qu::Scheduler.set_process_title "running"
72
+ end
73
+ end
74
+
75
+ # Pulls the schedule from Qu.schedule and loads it into the
76
+ # rufus scheduler instance
77
+ def load_schedule!
78
+ Qu::Scheduler.set_process_title "loading schedule"
79
+
80
+ # Need to load the schedule from redis for the first time if dynamic
81
+ Qu.reload_schedule! if Qu::Scheduler.dynamic
82
+
83
+ Qu.logger.warning("Schedule empty! Set Qu.schedule") if Qu.schedule.empty?
84
+
85
+ @@scheduled_jobs = {}
86
+
87
+ Qu.schedule.each do |name, config|
88
+ Qu::Scheduler.load_schedule_job(name, config)
89
+ end
90
+ redis.del(:schedules_changed)
91
+ Qu::Scheduler.set_process_title "running"
92
+ end
93
+
94
+ # Used internally to stuff the item into the schedule sorted list.
95
+ # +timestamp+ can be either in seconds or a datetime object
96
+ # Insertion if O(log(n)).
97
+ # Returns true if it's the first job to be scheduled at that time, else false
98
+ def delayed_push(timestamp, payload)
99
+ # First add this item to the list for this timestamp
100
+ redis.rpush("delayed:#{timestamp.to_i}", encode('klass' => payload.klass.to_s, 'args' => payload.args))
101
+
102
+ # Now, add this timestamp to the zsets. The score and the value are
103
+ # the same since we'll be querying by timestamp, and we don't have
104
+ # anything else to store.
105
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
106
+ end
107
+
108
+ # Returns an array of timestamps based on start and count
109
+ def delayed_queue_peek(start, count)
110
+ Array(redis.zrange(:delayed_queue_schedule, start, start+count)).collect{|x| x.to_i}
111
+ end
112
+
113
+ # Returns the size of the delayed queue schedule
114
+ def delayed_queue_schedule_size
115
+ redis.zcard :delayed_queue_schedule
116
+ end
117
+
118
+ # Returns the number of jobs for a given timestamp in the delayed queue schedule
119
+ def delayed_timestamp_size(timestamp)
120
+ redis.llen("delayed:#{timestamp.to_i}").to_i
121
+ end
122
+
123
+ # Returns the next delayed queue timestamp
124
+ # (don't call directly)
125
+ def next_delayed_timestamp(at_time=nil)
126
+ items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
127
+ timestamp = items.nil? ? nil : Array(items).first
128
+ timestamp.to_i unless timestamp.nil?
129
+ end
130
+
131
+ # Returns the next item to be processed for a given timestamp, nil if
132
+ # done. (don't call directly)
133
+ # +timestamp+ can either be in seconds or a datetime
134
+ def next_item_for_timestamp(timestamp)
135
+ key = "delayed:#{timestamp.to_i}"
136
+
137
+ item = decode redis.lpop(key)
138
+
139
+ # If the list is empty, remove it.
140
+ clean_up_timestamp(key, timestamp)
141
+ item
142
+ end
143
+
144
+ # Clears all jobs created with enqueue_at or enqueue_in
145
+ def reset_delayed_queue
146
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
147
+ redis.del "delayed:#{item}"
148
+ end
149
+
150
+ redis.del :delayed_queue_schedule
151
+ end
152
+
153
+ # Given an encoded item, remove it from the delayed_queue
154
+ #
155
+ # This method is potentially very expensive since it needs to scan
156
+ # through the delayed queue for every timestamp.
157
+ def remove_delayed(klass, *args)
158
+ destroyed = 0
159
+ search = encode('klass' => klass.to_s, 'args' => args)
160
+ Array(redis.keys("delayed:*")).each do |key|
161
+ destroyed += redis.lrem key, 0, search
162
+ end
163
+ destroyed
164
+ end
165
+
166
+ # Given a timestamp and job (klass + args) it removes all instances and
167
+ # returns the count of jobs removed.
168
+ #
169
+ # O(N) where N is the number of jobs scheduled to fire at the given
170
+ # timestamp
171
+ def remove_delayed_job_from_timestamp(timestamp, klass, *args)
172
+ key = "delayed:#{timestamp.to_i}"
173
+ count = redis.lrem key, 0, encode('klass' => klass.to_s, 'args' => args)
174
+ clean_up_timestamp(key, timestamp)
175
+ count
176
+ end
177
+
178
+ def count_all_scheduled_jobs
179
+ total_jobs = 0
180
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
181
+ total_jobs += redis.llen("delayed:#{timestamp}").to_i
182
+ end
183
+ total_jobs
184
+ end
185
+
186
+ private
187
+
188
+ def clean_up_timestamp(key, timestamp)
189
+ # If the list is empty, remove it.
190
+ if 0 == redis.llen(key).to_i
191
+ redis.del key
192
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+