qless 0.9.3 → 0.10.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 (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