qless 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +70 -25
  3. data/Rakefile +125 -9
  4. data/exe/install_phantomjs +21 -0
  5. data/lib/qless.rb +115 -76
  6. data/lib/qless/config.rb +11 -9
  7. data/lib/qless/failure_formatter.rb +43 -0
  8. data/lib/qless/job.rb +201 -102
  9. data/lib/qless/job_reservers/ordered.rb +7 -1
  10. data/lib/qless/job_reservers/round_robin.rb +16 -6
  11. data/lib/qless/job_reservers/shuffled_round_robin.rb +9 -2
  12. data/lib/qless/lua/qless-lib.lua +2463 -0
  13. data/lib/qless/lua/qless.lua +2012 -0
  14. data/lib/qless/lua_script.rb +63 -12
  15. data/lib/qless/middleware/memory_usage_monitor.rb +62 -0
  16. data/lib/qless/middleware/metriks.rb +45 -0
  17. data/lib/qless/middleware/redis_reconnect.rb +6 -3
  18. data/lib/qless/middleware/requeue_exceptions.rb +94 -0
  19. data/lib/qless/middleware/retry_exceptions.rb +38 -9
  20. data/lib/qless/middleware/sentry.rb +3 -7
  21. data/lib/qless/middleware/timeout.rb +64 -0
  22. data/lib/qless/queue.rb +90 -55
  23. data/lib/qless/server.rb +177 -130
  24. data/lib/qless/server/views/_job.erb +33 -15
  25. data/lib/qless/server/views/completed.erb +11 -0
  26. data/lib/qless/server/views/layout.erb +70 -11
  27. data/lib/qless/server/views/overview.erb +93 -53
  28. data/lib/qless/server/views/queue.erb +9 -8
  29. data/lib/qless/server/views/queues.erb +18 -1
  30. data/lib/qless/subscriber.rb +37 -22
  31. data/lib/qless/tasks.rb +5 -10
  32. data/lib/qless/test_helpers/worker_helpers.rb +55 -0
  33. data/lib/qless/version.rb +3 -1
  34. data/lib/qless/worker.rb +4 -413
  35. data/lib/qless/worker/base.rb +247 -0
  36. data/lib/qless/worker/forking.rb +245 -0
  37. data/lib/qless/worker/serial.rb +41 -0
  38. metadata +135 -52
  39. data/lib/qless/qless-core/cancel.lua +0 -101
  40. data/lib/qless/qless-core/complete.lua +0 -233
  41. data/lib/qless/qless-core/config.lua +0 -56
  42. data/lib/qless/qless-core/depends.lua +0 -65
  43. data/lib/qless/qless-core/deregister_workers.lua +0 -12
  44. data/lib/qless/qless-core/fail.lua +0 -117
  45. data/lib/qless/qless-core/failed.lua +0 -83
  46. data/lib/qless/qless-core/get.lua +0 -37
  47. data/lib/qless/qless-core/heartbeat.lua +0 -51
  48. data/lib/qless/qless-core/jobs.lua +0 -41
  49. data/lib/qless/qless-core/pause.lua +0 -18
  50. data/lib/qless/qless-core/peek.lua +0 -165
  51. data/lib/qless/qless-core/pop.lua +0 -314
  52. data/lib/qless/qless-core/priority.lua +0 -32
  53. data/lib/qless/qless-core/put.lua +0 -169
  54. data/lib/qless/qless-core/qless-lib.lua +0 -2354
  55. data/lib/qless/qless-core/qless.lua +0 -1862
  56. data/lib/qless/qless-core/queues.lua +0 -58
  57. data/lib/qless/qless-core/recur.lua +0 -190
  58. data/lib/qless/qless-core/retry.lua +0 -73
  59. data/lib/qless/qless-core/stats.lua +0 -92
  60. data/lib/qless/qless-core/tag.lua +0 -100
  61. data/lib/qless/qless-core/track.lua +0 -79
  62. data/lib/qless/qless-core/unfail.lua +0 -54
  63. data/lib/qless/qless-core/unpause.lua +0 -12
  64. data/lib/qless/qless-core/workers.lua +0 -69
  65. data/lib/qless/wait_until.rb +0 -19
@@ -1,58 +0,0 @@
1
- -- Queues(0, now, [queue])
2
- -- -----------------------
3
- --
4
- -- Return all the queues we know about, with how many jobs are scheduled, waiting,
5
- -- and running in that queue. If a queue name is provided, then only the appropriate
6
- -- response hash should be returned. The response is JSON:
7
- --
8
- -- [
9
- -- {
10
- -- 'name': 'testing',
11
- -- 'stalled': 2,
12
- -- 'waiting': 5,
13
- -- 'running': 5,
14
- -- 'scheduled': 10,
15
- -- 'depends': 5,
16
- -- 'recurring': 0
17
- -- }, {
18
- -- ...
19
- -- }
20
- -- ]
21
-
22
- if #KEYS > 0 then
23
- error('Queues(): Got '.. #KEYS .. ' expected 0 KEYS arguments')
24
- end
25
-
26
- local now = assert(tonumber(ARGV[1]), 'Queues(): Arg "now" missing or not a number: ' .. (ARGV[1] or 'nil'))
27
- local queue = ARGV[2]
28
-
29
- local response = {}
30
- local queuenames = redis.call('zrange', 'ql:queues', 0, -1)
31
-
32
- if queue then
33
- local stalled = redis.call('zcount', 'ql:q:' .. queue .. '-locks', 0, now)
34
- response = {
35
- name = queue,
36
- waiting = redis.call('zcard', 'ql:q:' .. queue .. '-work'),
37
- stalled = stalled,
38
- running = redis.call('zcard', 'ql:q:' .. queue .. '-locks') - stalled,
39
- scheduled = redis.call('zcard', 'ql:q:' .. queue .. '-scheduled'),
40
- depends = redis.call('zcard', 'ql:q:' .. queue .. '-depends'),
41
- recurring = redis.call('zcard', 'ql:q:' .. queue .. '-recur')
42
- }
43
- else
44
- for index, qname in ipairs(queuenames) do
45
- local stalled = redis.call('zcount', 'ql:q:' .. qname .. '-locks', 0, now)
46
- table.insert(response, {
47
- name = qname,
48
- waiting = redis.call('zcard', 'ql:q:' .. qname .. '-work'),
49
- stalled = stalled,
50
- running = redis.call('zcard', 'ql:q:' .. qname .. '-locks') - stalled,
51
- scheduled = redis.call('zcard', 'ql:q:' .. qname .. '-scheduled'),
52
- depends = redis.call('zcard', 'ql:q:' .. qname .. '-depends'),
53
- recurring = redis.call('zcard', 'ql:q:' .. qname .. '-recur')
54
- })
55
- end
56
- end
57
-
58
- return cjson.encode(response)
@@ -1,190 +0,0 @@
1
- -- Recur(0, 'on', queue, jid, klass, data, now, 'interval', second, offset, [priority p], [tags t], [retries r])
2
- -- Recur(0, 'off', jid)
3
- -- Recur(0, 'get', jid)
4
- -- Recur(0, 'update', jid, ['priority', priority], ['interval', interval], ['retries', retries], ['data', data], ['klass', klass], ['queue', queue])
5
- -- Recur(0, 'tag', jid, tag, [tag, [...]])
6
- -- Recur(0, 'untag', jid, tag, [tag, [...]])
7
- -- -------------------------------------------------------------------------------------------------------
8
- -- This script takes the name of a queue, and then the info
9
- -- info about the work item, and makes sure that jobs matching
10
- -- its criteria are regularly made available.
11
-
12
- if #KEYS ~= 0 then
13
- error('Recur(): Got ' .. #KEYS .. ', expected 0 KEYS arguments')
14
- end
15
-
16
- local command = assert(ARGV[1] , 'Recur(): Missing first argument')
17
-
18
- if command == 'on' then
19
- local queue = assert(ARGV[2] , 'Recur(): Arg "queue" missing')
20
- local jid = assert(ARGV[3] , 'Recur(): Arg "jid" missing')
21
- local klass = assert(ARGV[4] , 'Recur(): Arg "klass" missing')
22
- local data = assert(cjson.decode(ARGV[5]) , 'Recur(): Arg "data" missing or not JSON: ' .. tostring(ARGV[5]))
23
- local now = assert(tonumber(ARGV[6]) , 'Recur(): Arg "now" missing or not a number: ' .. tostring(ARGV[6]))
24
- local spec = assert(ARGV[7] , 'Recur(): Arg "schedule type" missing')
25
- if spec == 'interval' then
26
- local interval = assert(tonumber(ARGV[8]) , 'Recur(): Arg "interval" must be a number: ' .. tostring(ARGV[8]))
27
- local offset = assert(tonumber(ARGV[9]) , 'Recur(): Arg "offset" must be a number: ' .. tostring(ARGV[9]))
28
- if interval <= 0 then
29
- error('Recur(): Arg "interval" must be greater than or equal to 0')
30
- end
31
- -- Read in all the optional parameters
32
- local options = {}
33
- for i = 10, #ARGV, 2 do options[ARGV[i]] = ARGV[i + 1] end
34
- options.tags = assert(cjson.decode(options.tags or {}), 'Recur(): Arg "tags" must be JSON-encoded array of string. Got: ' .. tostring(options.tags))
35
- options.priority = assert(tonumber(options.priority or 0) , 'Recur(): Arg "priority" must be a number. Got: ' .. tostring(options.priority))
36
- options.retries = assert(tonumber(options.retries or 0) , 'Recur(): Arg "retries" must be a number. Got: ' .. tostring(options.retries))
37
-
38
- local count, old_queue = unpack(redis.call('hmget', 'ql:r:' .. jid, 'count', 'queue'))
39
- count = count or 0
40
-
41
- -- If it has previously been in another queue, then we should remove
42
- -- some information about it
43
- if old_queue then
44
- redis.call('zrem', 'ql:q:' .. old_queue .. '-recur', jid)
45
- end
46
-
47
- -- Do some insertions
48
- redis.call('hmset', 'ql:r:' .. jid,
49
- 'jid' , jid,
50
- 'klass' , klass,
51
- 'data' , cjson.encode(data),
52
- 'priority', options.priority,
53
- 'tags' , cjson.encode(options.tags or {}),
54
- 'state' , 'recur',
55
- 'queue' , queue,
56
- 'type' , 'interval',
57
- -- How many jobs we've spawned from this
58
- 'count' , count,
59
- 'interval', interval,
60
- 'retries' , options.retries)
61
- -- Now, we should schedule the next run of the job
62
- redis.call('zadd', 'ql:q:' .. queue .. '-recur', now + offset, jid)
63
-
64
- -- Lastly, we're going to make sure that this item is in the
65
- -- set of known queues. We should keep this sorted by the
66
- -- order in which we saw each of these queues
67
- if redis.call('zscore', 'ql:queues', queue) == false then
68
- redis.call('zadd', 'ql:queues', now, queue)
69
- end
70
-
71
- return jid
72
- else
73
- error('Recur(): schedule type "' .. tostring(spec) .. '" unknown')
74
- end
75
- elseif command == 'off' then
76
- local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
77
- -- First, find out what queue it was attached to
78
- local queue = redis.call('hget', 'ql:r:' .. jid, 'queue')
79
- if queue then
80
- -- Now, delete it from the queue it was attached to, and delete the thing itself
81
- redis.call('zrem', 'ql:q:' .. queue .. '-recur', jid)
82
- redis.call('del', 'ql:r:' .. jid)
83
- return true
84
- else
85
- return true
86
- end
87
- elseif command == 'get' then
88
- local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
89
- local job = redis.call(
90
- 'hmget', 'ql:r:' .. jid, 'jid', 'klass', 'state', 'queue',
91
- 'priority', 'interval', 'retries', 'count', 'data', 'tags')
92
-
93
- if not job[1] then
94
- return false
95
- end
96
-
97
- return cjson.encode({
98
- jid = job[1],
99
- klass = job[2],
100
- state = job[3],
101
- queue = job[4],
102
- priority = tonumber(job[5]),
103
- interval = tonumber(job[6]),
104
- retries = tonumber(job[7]),
105
- count = tonumber(job[8]),
106
- data = cjson.decode(job[9]),
107
- tags = cjson.decode(job[10])
108
- })
109
- elseif command == 'update' then
110
- local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
111
- local options = {}
112
-
113
- -- Make sure that the job exists
114
- if redis.call('exists', 'ql:r:' .. jid) ~= 0 then
115
- for i = 3, #ARGV, 2 do
116
- local key = ARGV[i]
117
- local value = ARGV[i+1]
118
- if key == 'priority' or key == 'interval' or key == 'retries' then
119
- value = assert(tonumber(value), 'Recur(): Arg "' .. key .. '" must be a number: ' .. tostring(value))
120
- -- If the command is 'interval', then we need to update the time
121
- -- when it should next be scheduled
122
- if key == 'interval' then
123
- local queue, interval = unpack(redis.call('hmget', 'ql:r:' .. jid, 'queue', 'interval'))
124
- redis.call('zincrby', 'ql:q:' .. queue .. '-recur', value - tonumber(interval), jid)
125
- end
126
- redis.call('hset', 'ql:r:' .. jid, key, value)
127
- elseif key == 'data' then
128
- value = assert(cjson.decode(value), 'Recur(): Arg "data" is not JSON-encoded: ' .. tostring(value))
129
- redis.call('hset', 'ql:r:' .. jid, 'data', cjson.encode(value))
130
- elseif key == 'klass' then
131
- redis.call('hset', 'ql:r:' .. jid, 'klass', value)
132
- elseif key == 'queue' then
133
- local queue = redis.call('hget', 'ql:r:' .. jid, 'queue')
134
- local score = redis.call('zscore', 'ql:q:' .. queue .. '-recur', jid)
135
- redis.call('zrem', 'ql:q:' .. queue .. '-recur', jid)
136
- redis.call('zadd', 'ql:q:' .. value .. '-recur', score, jid)
137
- redis.call('hset', 'ql:r:' .. jid, 'queue', value)
138
- else
139
- error('Recur(): Unrecognized option "' .. key .. '"')
140
- end
141
- end
142
- return true
143
- else
144
- return false
145
- end
146
- elseif command == 'tag' then
147
- local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
148
- local tags = redis.call('hget', 'ql:r:' .. jid, 'tags')
149
- -- If the job has been canceled / deleted, then return false
150
- if tags then
151
- -- Decode the json blob, convert to dictionary
152
- tags = cjson.decode(tags)
153
- local _tags = {}
154
- for i,v in ipairs(tags) do _tags[v] = true end
155
-
156
- -- Otherwise, add the job to the sorted set with that tags
157
- for i=3,#ARGV do if _tags[ARGV[i]] == nil then table.insert(tags, ARGV[i]) end end
158
-
159
- tags = cjson.encode(tags)
160
- redis.call('hset', 'ql:r:' .. jid, 'tags', tags)
161
- return tags
162
- else
163
- return false
164
- end
165
- elseif command == 'untag' then
166
- local jid = assert(ARGV[2], 'Recur(): Arg "jid" missing')
167
- -- Get the existing tags
168
- local tags = redis.call('hget', 'ql:r:' .. jid, 'tags')
169
- -- If the job has been canceled / deleted, then return false
170
- if tags then
171
- -- Decode the json blob, convert to dictionary
172
- tags = cjson.decode(tags)
173
- local _tags = {}
174
- -- Make a hash
175
- for i,v in ipairs(tags) do _tags[v] = true end
176
- -- Delete these from the hash
177
- for i = 3,#ARGV do _tags[ARGV[i]] = nil end
178
- -- Back into a list
179
- local results = {}
180
- for i, tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
181
- -- json encode them, set, and return
182
- tags = cjson.encode(results)
183
- redis.call('hset', 'ql:r:' .. jid, 'tags', tags)
184
- return tags
185
- else
186
- return false
187
- end
188
- else
189
- error('Recur(): First argument must be one of [on, off, get, update, tag, untag]. Got ' .. tostring(ARGV[1]))
190
- end
@@ -1,73 +0,0 @@
1
- -- retry(0, jid, queue, worker, now, [delay])
2
- -- ------------------------------------------
3
- -- This script accepts jid, queue, worker and delay for
4
- -- retrying a job. This is similar in functionality to
5
- -- `put`, except that this counts against the retries
6
- -- a job has for a stage.
7
- --
8
- -- If the worker is not the worker with a lock on the job,
9
- -- then it returns false. If the job is not actually running,
10
- -- then it returns false. Otherwise, it returns the number
11
- -- of retries remaining. If the allowed retries have been
12
- -- exhausted, then it is automatically failed, and a negative
13
- -- number is returned.
14
-
15
- if #KEYS ~= 0 then
16
- error('Retry(): Got ' .. #KEYS .. ', expected 0')
17
- end
18
-
19
- local jid = assert(ARGV[1] , 'Retry(): Arg "jid" missing')
20
- local queue = assert(ARGV[2] , 'Retry(): Arg "queue" missing')
21
- local worker = assert(ARGV[3] , 'Retry(): Arg "worker" missing')
22
- local now = assert(tonumber(ARGV[4]) , 'Retry(): Arg "now" missing')
23
- local delay = assert(tonumber(ARGV[5] or 0), 'Retry(): Arg "delay" not a number: ' .. tostring(ARGV[5]))
24
-
25
- -- Let's see what the old priority, history and tags were
26
- local oldqueue, state, retries, oldworker, priority = unpack(redis.call('hmget', 'ql:j:' .. jid, 'queue', 'state', 'retries', 'worker', 'priority'))
27
-
28
- -- If this isn't the worker that owns
29
- if oldworker ~= worker or (state ~= 'running') then
30
- return false
31
- end
32
-
33
- -- Remove it from the locks key of the old queue
34
- redis.call('zrem', 'ql:q:' .. oldqueue .. '-locks', jid)
35
-
36
- local remaining = redis.call('hincrby', 'ql:j:' .. jid, 'remaining', -1)
37
-
38
- -- Remove this job from the worker that was previously working it
39
- redis.call('zrem', 'ql:w:' .. worker .. ':jobs', jid)
40
-
41
- if remaining < 0 then
42
- -- Now remove the instance from the schedule, and work queues for the queue it's in
43
- local group = 'failed-retries-' .. queue
44
- -- First things first, we should get the history
45
- local history = redis.call('hget', 'ql:j:' .. jid, 'history')
46
- -- Now, take the element of the history for which our provided worker is the worker, and update 'failed'
47
- history = cjson.decode(history or '[]')
48
- history[#history]['failed'] = now
49
-
50
- redis.call('hmset', 'ql:j:' .. jid, 'state', 'failed', 'worker', '',
51
- 'expires', '', 'history', cjson.encode(history), 'failure', cjson.encode({
52
- ['group'] = group,
53
- ['message'] = 'Job exhausted retries in queue "' .. queue .. '"',
54
- ['when'] = now,
55
- ['worker'] = worker
56
- }))
57
-
58
- -- Add this type of failure to the list of failures
59
- redis.call('sadd', 'ql:failures', group)
60
- -- And add this particular instance to the failed types
61
- redis.call('lpush', 'ql:f:' .. group, jid)
62
- else
63
- -- Put it in the queue again with a delay. Like put()
64
- if delay > 0 then
65
- redis.call('zadd', 'ql:q:' .. queue .. '-scheduled', now + delay, jid)
66
- redis.call('hset', 'ql:j:' .. jid, 'state', 'scheduled')
67
- else
68
- redis.call('zadd', 'ql:q:' .. queue .. '-work', priority - (now / 10000000000), jid)
69
- redis.call('hset', 'ql:j:' .. jid, 'state', 'waiting')
70
- end
71
- end
72
-
73
- return remaining
@@ -1,92 +0,0 @@
1
- -- Stats(0, queue, date)
2
- -- ---------------------
3
- -- Return the current statistics for a given queue on a given date. The results
4
- -- are returned are a JSON blob:
5
- --
6
- --
7
- -- {
8
- -- # These are unimplemented as of yet
9
- -- 'failed': 3,
10
- -- 'retries': 5,
11
- -- 'wait' : {
12
- -- 'total' : ...,
13
- -- 'mean' : ...,
14
- -- 'variance' : ...,
15
- -- 'histogram': [
16
- -- ...
17
- -- ]
18
- -- }, 'run': {
19
- -- 'total' : ...,
20
- -- 'mean' : ...,
21
- -- 'variance' : ...,
22
- -- 'histogram': [
23
- -- ...
24
- -- ]
25
- -- }
26
- -- }
27
- --
28
- -- The histogram's data points are at the second resolution for the first minute,
29
- -- the minute resolution for the first hour, the 15-minute resolution for the first
30
- -- day, the hour resolution for the first 3 days, and then at the day resolution
31
- -- from there on out. The `histogram` key is a list of those values.
32
- --
33
- -- Args:
34
- -- 1) queue
35
- -- 2) time
36
-
37
- if #KEYS > 0 then error('Stats(): No Keys should be provided') end
38
-
39
- local queue = assert(ARGV[1] , 'Stats(): Arg "queue" missing')
40
- local time = assert(tonumber(ARGV[2]), 'Stats(): Arg "time" missing or not a number: ' .. (ARGV[2] or 'nil'))
41
-
42
- -- The bin is midnight of the provided day
43
- -- 24 * 60 * 60 = 86400
44
- local bin = time - (time % 86400)
45
-
46
- -- This a table of all the keys we want to use in order to produce a histogram
47
- local histokeys = {
48
- 's0','s1','s2','s3','s4','s5','s6','s7','s8','s9','s10','s11','s12','s13','s14','s15','s16','s17','s18','s19','s20','s21','s22','s23','s24','s25','s26','s27','s28','s29','s30','s31','s32','s33','s34','s35','s36','s37','s38','s39','s40','s41','s42','s43','s44','s45','s46','s47','s48','s49','s50','s51','s52','s53','s54','s55','s56','s57','s58','s59',
49
- 'm1','m2','m3','m4','m5','m6','m7','m8','m9','m10','m11','m12','m13','m14','m15','m16','m17','m18','m19','m20','m21','m22','m23','m24','m25','m26','m27','m28','m29','m30','m31','m32','m33','m34','m35','m36','m37','m38','m39','m40','m41','m42','m43','m44','m45','m46','m47','m48','m49','m50','m51','m52','m53','m54','m55','m56','m57','m58','m59',
50
- 'h1','h2','h3','h4','h5','h6','h7','h8','h9','h10','h11','h12','h13','h14','h15','h16','h17','h18','h19','h20','h21','h22','h23',
51
- 'd1','d2','d3','d4','d5','d6'
52
- }
53
-
54
- local mkstats = function(name, bin, queue)
55
- -- The results we'll be sending back
56
- local results = {}
57
-
58
- local count, mean, vk = unpack(redis.call('hmget', 'ql:s:' .. name .. ':' .. bin .. ':' .. queue, 'total', 'mean', 'vk'))
59
-
60
- count = tonumber(count) or 0
61
- mean = tonumber(mean) or 0
62
- vk = tonumber(vk)
63
-
64
- results.count = count or 0
65
- results.mean = mean or 0
66
- results.histogram = {}
67
-
68
- if not count then
69
- results.std = 0
70
- else
71
- if count > 1 then
72
- results.std = math.sqrt(vk / (count - 1))
73
- else
74
- results.std = 0
75
- end
76
- end
77
-
78
- local histogram = redis.call('hmget', 'ql:s:' .. name .. ':' .. bin .. ':' .. queue, unpack(histokeys))
79
- for i=1,#histokeys do
80
- table.insert(results.histogram, tonumber(histogram[i]) or 0)
81
- end
82
- return results
83
- end
84
-
85
- local retries, failed, failures = unpack(redis.call('hmget', 'ql:s:stats:' .. bin .. ':' .. queue, 'retries', 'failed', 'failures'))
86
- return cjson.encode({
87
- retries = tonumber(retries or 0),
88
- failed = tonumber(failed or 0),
89
- failures = tonumber(failures or 0),
90
- wait = mkstats('wait', bin, queue),
91
- run = mkstats('run' , bin, queue)
92
- })
@@ -1,100 +0,0 @@
1
- -- tag(0, ('add' | 'remove'), jid, now, tag, [tag, ...])
2
- -- tag(0, 'get', tag, [offset, [count]])
3
- -- tag(0, 'top', [offset, [count]])
4
- -- ------------------------------------------------------------------------------------------------------------------
5
- -- Accepts a jid, 'add' or 'remove', and then a list of tags
6
- -- to either add or remove from the job. Alternatively, 'get',
7
- -- a tag to get jobs associated with that tag, and offset and
8
- -- count
9
- --
10
- -- If 'add' or 'remove', the response is a list of the jobs
11
- -- current tags, or False if the job doesn't exist. If 'get',
12
- -- the response is of the form:
13
- --
14
- -- {
15
- -- total: ...,
16
- -- jobs: [
17
- -- jid,
18
- -- ...
19
- -- ]
20
- -- }
21
- --
22
- -- If 'top' is supplied, it returns the most commonly-used tags
23
- -- in a paginated fashion.
24
-
25
- if #KEYS ~= 0 then
26
- error('Tag(): Got ' .. #KEYS .. ', expected 0')
27
- end
28
-
29
- local command = assert(ARGV[1], 'Tag(): Missing first arg "add", "remove" or "get"')
30
-
31
- if command == 'add' then
32
- local jid = assert(ARGV[2] , 'Tag(): Arg "jid" missing')
33
- local now = assert(tonumber(ARGV[3]), 'Tag(): Arg "now" is not a number')
34
- local tags = redis.call('hget', 'ql:j:' .. jid, 'tags')
35
- -- If the job has been canceled / deleted, then return false
36
- if tags then
37
- -- Decode the json blob, convert to dictionary
38
- tags = cjson.decode(tags)
39
- local _tags = {}
40
- for i,v in ipairs(tags) do _tags[v] = true end
41
-
42
- -- Otherwise, add the job to the sorted set with that tags
43
- for i=4,#ARGV do
44
- local tag = ARGV[i]
45
- if _tags[tag] == nil then
46
- table.insert(tags, tag)
47
- end
48
- redis.call('zadd', 'ql:t:' .. tag, now, jid)
49
- redis.call('zincrby', 'ql:tags', 1, tag)
50
- end
51
-
52
- tags = cjson.encode(tags)
53
- redis.call('hset', 'ql:j:' .. jid, 'tags', tags)
54
- return tags
55
- else
56
- return false
57
- end
58
- elseif command == 'remove' then
59
- local jid = assert(ARGV[2] , 'Tag(): Arg "jid" missing')
60
- local now = assert(tonumber(ARGV[3]), 'Tag(): Arg "now" is not a number')
61
- local tags = redis.call('hget', 'ql:j:' .. jid, 'tags')
62
- -- If the job has been canceled / deleted, then return false
63
- if tags then
64
- -- Decode the json blob, convert to dictionary
65
- tags = cjson.decode(tags)
66
- local _tags = {}
67
- for i,v in ipairs(tags) do _tags[v] = true end
68
-
69
- -- Otherwise, add the job to the sorted set with that tags
70
- for i=4,#ARGV do
71
- local tag = ARGV[i]
72
- _tags[tag] = nil
73
- redis.call('zrem', 'ql:t:' .. tag, jid)
74
- redis.call('zincrby', 'ql:tags', -1, tag)
75
- end
76
-
77
- local results = {}
78
- for i,tag in ipairs(tags) do if _tags[tag] then table.insert(results, tag) end end
79
-
80
- tags = cjson.encode(results)
81
- redis.call('hset', 'ql:j:' .. jid, 'tags', tags)
82
- return tags
83
- else
84
- return false
85
- end
86
- elseif command == 'get' then
87
- local tag = assert(ARGV[2] , 'Tag(): Arg "tag" missing')
88
- local offset = assert(tonumber(ARGV[3] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(ARGV[3]))
89
- local count = assert(tonumber(ARGV[4] or 25), 'Tag(): Arg "count" not a number: ' .. tostring(ARGV[4]))
90
- return cjson.encode({
91
- total = redis.call('zcard', 'ql:t:' .. tag),
92
- jobs = redis.call('zrange', 'ql:t:' .. tag, offset, count)
93
- })
94
- elseif command == 'top' then
95
- local offset = assert(tonumber(ARGV[2] or 0) , 'Tag(): Arg "offset" not a number: ' .. tostring(ARGV[2]))
96
- local count = assert(tonumber(ARGV[3] or 25), 'Tag(): Arg "count" not a number: ' .. tostring(ARGV[3]))
97
- return cjson.encode(redis.call('zrevrangebyscore', 'ql:tags', '+inf', 2, 'limit', offset, count))
98
- else
99
- error('Tag(): First argument must be "add", "remove" or "get"')
100
- end