fileq 0.1.3
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.
- data/README +15 -0
- data/lib/fileq.rb +455 -0
- data/lib/job.rb +181 -0
- data/lib/lockfile.rb +88 -0
- data/test/fileq_createdir_tests.rb +83 -0
- data/test/fileq_find_tests.rb +74 -0
- data/test/fileq_first_tests.rb +33 -0
- data/test/fileq_job_tests.rb +218 -0
- data/test/fileq_queue_tests.rb +365 -0
- data/test/fileq_test.rb +12 -0
- data/test/lockfile_test.rb +4 -0
- data/test/lockfile_tests.rb +132 -0
- data/test/test_helper.rb +8 -0
- metadata +67 -0
data/README
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
FileQ
|
2
|
+
==============
|
3
|
+
|
4
|
+
A class that manages a simple queue on a typical
|
5
|
+
Unix based filesystem. Insert data or items into
|
6
|
+
the Queue, pull the items out in the same order.
|
7
|
+
The system is designed to be resilient and safe
|
8
|
+
for multi-process use. Eventually, it will be used
|
9
|
+
as a component in a larger project.
|
10
|
+
|
11
|
+
|
12
|
+
AUTHOR: Dru Nelson - "dru" * 2 + '@gmail.com'
|
13
|
+
|
14
|
+
License: BSD Like
|
15
|
+
|
data/lib/fileq.rb
ADDED
@@ -0,0 +1,455 @@
|
|
1
|
+
# FileQ
|
2
|
+
#
|
3
|
+
# This is a class which manages an on-disk/persistent, ordered, queue.
|
4
|
+
# You can insert data or files into the queue.
|
5
|
+
#
|
6
|
+
# In Process A (for example a mongrel)
|
7
|
+
# fq = FileQ.initialize('/path/to/que')
|
8
|
+
# fq.insert_file('/path/to/file')
|
9
|
+
# <watching process or fork/exec worker process>
|
10
|
+
#
|
11
|
+
# In Process B (worker process)
|
12
|
+
# fq = FileQ.initialize('/path/to/que')
|
13
|
+
# job = fq.pull_job(job_id if provided, or nil for oldest job)
|
14
|
+
# <memory/cpu intense work>
|
15
|
+
# job.mark_as_done
|
16
|
+
#
|
17
|
+
|
18
|
+
require 'yaml'
|
19
|
+
require 'fcntl'
|
20
|
+
require 'job'
|
21
|
+
require 'lockfile'
|
22
|
+
|
23
|
+
module Xxeo
|
24
|
+
|
25
|
+
ST_QUEUED = :ST_QUEUED
|
26
|
+
ST_RUN = :ST_RUN
|
27
|
+
ST_DONE = :ST_DONE
|
28
|
+
ST_PAUSED = :ST_PAUSED
|
29
|
+
ST_ERROR = :ST_ERROR
|
30
|
+
ST_UNKNOWN = :ST_UNKNOWN
|
31
|
+
|
32
|
+
class FileQ
|
33
|
+
|
34
|
+
@@config_path = nil
|
35
|
+
@@config = nil
|
36
|
+
@@dir = nil
|
37
|
+
@@fname_rgx = /(\d+.\d\d\d):(\d+):(\d+)\.fq/
|
38
|
+
|
39
|
+
def initialize(name, options = {})
|
40
|
+
options[:env] ||= 'development'
|
41
|
+
@err = ''
|
42
|
+
|
43
|
+
if not @dir
|
44
|
+
if options[:dir]
|
45
|
+
@dir = options[:dir]
|
46
|
+
elsif
|
47
|
+
@@config_path = options[:config_path] || ('./config/fileq.yml')
|
48
|
+
@@config = YAML.load_file(@@config_path)
|
49
|
+
# USe
|
50
|
+
path = eval('"' + @@config[name][options[:env]]['pathname'] + '"')
|
51
|
+
|
52
|
+
# TODO
|
53
|
+
# If it is an expression, evaluate the env var
|
54
|
+
@dir = path
|
55
|
+
else
|
56
|
+
return nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@lock = nil
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_error
|
65
|
+
return @err
|
66
|
+
end
|
67
|
+
|
68
|
+
# Names will be in format of
|
69
|
+
# YYYYMMDD.HHMM.SS
|
70
|
+
|
71
|
+
def generate_name
|
72
|
+
z = Time.now.getutc
|
73
|
+
name = z.strftime("%Y%m%d.%H%M.%S.") + sprintf("%03d", (z.tv_usec / 1000))
|
74
|
+
return name
|
75
|
+
# Process.pid kddkd
|
76
|
+
end
|
77
|
+
|
78
|
+
def log(msg)
|
79
|
+
@err = msg
|
80
|
+
File.open(@dir + '/_log', "a") do
|
81
|
+
|f|
|
82
|
+
f.write(Time.now.to_s + " == " + msg + "\n")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def read_log
|
87
|
+
data = ''
|
88
|
+
File.open(@dir + '/_log', "r") do
|
89
|
+
|f|
|
90
|
+
data = f.read
|
91
|
+
end
|
92
|
+
return data
|
93
|
+
end
|
94
|
+
|
95
|
+
def insert_file(fname, opts = {})
|
96
|
+
unless FileTest.writable?(fname)
|
97
|
+
log("Supplied file: #{fname} is not writable. Failed ot insert. (see log)")
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
opts[:XX_type] = 'file'
|
101
|
+
opts[:XX_data] = fname
|
102
|
+
insert(opts)
|
103
|
+
end
|
104
|
+
|
105
|
+
def insert_data(data, opts = {})
|
106
|
+
opts[:XX_type] = 'data'
|
107
|
+
opts[:XX_data] = data
|
108
|
+
insert(opts)
|
109
|
+
end
|
110
|
+
|
111
|
+
def length
|
112
|
+
Dir.glob(@dir + '/que/*').length
|
113
|
+
end
|
114
|
+
|
115
|
+
def all_lengths
|
116
|
+
a = %w(_tmp que run pause done _err)
|
117
|
+
h = Hash.new(0)
|
118
|
+
a.map {
|
119
|
+
|e|
|
120
|
+
a = Dir.glob(@dir + "/#{e}/*")
|
121
|
+
l = a.length
|
122
|
+
h[e.to_sym] = l if l > 0
|
123
|
+
}
|
124
|
+
return h
|
125
|
+
end
|
126
|
+
|
127
|
+
def internal_job_exists?(q_name, job_name)
|
128
|
+
throw ArgumentError unless ['que', 'run', 'done', 'pause', '_err'].include?(q_name)
|
129
|
+
throw ArgumentError unless job_name
|
130
|
+
result = nil
|
131
|
+
path = @dir + '/' + q_name + '/' + job_name
|
132
|
+
return FileTest.directory?(path)
|
133
|
+
end
|
134
|
+
|
135
|
+
def status(job_name = nil)
|
136
|
+
throw ArgumentError unless job_name
|
137
|
+
lock
|
138
|
+
result = ST_UNKNOWN
|
139
|
+
[['que', ST_QUEUED], ['run', ST_RUN], ['done', ST_DONE], ['pause', ST_PAUSED], ['_err', ST_ERROR]].map {
|
140
|
+
| dir |
|
141
|
+
path = @dir + '/' + dir[0] + '/' + job_name
|
142
|
+
if FileTest.directory? path
|
143
|
+
result = dir[1]
|
144
|
+
break
|
145
|
+
end
|
146
|
+
}
|
147
|
+
unlock
|
148
|
+
return result
|
149
|
+
end
|
150
|
+
|
151
|
+
def meta_for_job(job_name)
|
152
|
+
lock
|
153
|
+
data = nil
|
154
|
+
[['que', ST_QUEUED], ['run', ST_RUN], ['done', ST_DONE], ['pause', ST_PAUSED], ['_err', ST_ERROR]].map {
|
155
|
+
| dir |
|
156
|
+
path = @dir + '/' + dir[0] + '/' + job_name
|
157
|
+
if FileTest.directory? path
|
158
|
+
data = YAML.load_file(path + '/meta.yml')
|
159
|
+
break
|
160
|
+
end
|
161
|
+
}
|
162
|
+
unlock
|
163
|
+
return data
|
164
|
+
end
|
165
|
+
|
166
|
+
def status_mesg_for_job(job_name)
|
167
|
+
lock
|
168
|
+
data = ''
|
169
|
+
[['que', ST_QUEUED], ['run', ST_RUN], ['done', ST_DONE], ['pause', ST_PAUSED], ['_err', ST_ERROR]].map {
|
170
|
+
| dir |
|
171
|
+
path = @dir + '/' + dir[0] + '/' + job_name
|
172
|
+
if FileTest.directory? path
|
173
|
+
if FileTest.readable? path + '/status'
|
174
|
+
File.open(path + '/status') { |f| data = f.read }
|
175
|
+
end
|
176
|
+
break
|
177
|
+
end
|
178
|
+
}
|
179
|
+
unlock
|
180
|
+
return data
|
181
|
+
end
|
182
|
+
|
183
|
+
def internal_find_job(job_name = nil)
|
184
|
+
result = [nil, nil]
|
185
|
+
if job_name
|
186
|
+
[['que', ST_QUEUED], ['run', ST_RUN], ['done', ST_DONE], ['pause', ST_PAUSED], ['_err', ST_ERROR]].map {
|
187
|
+
| dir |
|
188
|
+
path = @dir + '/' + dir[0] + '/' + job_name
|
189
|
+
if FileTest.directory? path
|
190
|
+
result = [Job.create(self, job_name, path, dir[1]), dir[1]]
|
191
|
+
break
|
192
|
+
end
|
193
|
+
}
|
194
|
+
else
|
195
|
+
# Find the oldest job in the queue
|
196
|
+
jobs = Dir.glob(que_path + '*')
|
197
|
+
min = jobs.min
|
198
|
+
if min
|
199
|
+
name = File.basename(min)
|
200
|
+
result = [Job.create(self, name, que_path + name, ST_QUEUED), ST_QUEUED]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
return result
|
204
|
+
end
|
205
|
+
|
206
|
+
def find_job(job_name = nil)
|
207
|
+
lock
|
208
|
+
job,status = internal_find_job(job_name)
|
209
|
+
unlock
|
210
|
+
return job
|
211
|
+
end
|
212
|
+
|
213
|
+
# Currently we can only move items from the que to the run queu
|
214
|
+
# TODO: allow paused or error jobs to be reset, (maybe even done jobs)
|
215
|
+
# definitely log history in those jobs
|
216
|
+
def pull_job(job_name = nil)
|
217
|
+
lock
|
218
|
+
job,status = internal_find_job(job_name)
|
219
|
+
if job && status == ST_QUEUED
|
220
|
+
# Move to run, notice use of job.name rather than job_name
|
221
|
+
# .. if we are pulling a new job, it could be nil
|
222
|
+
FileUtils.mv(@dir + '/que/' + job.name, @dir + '/run/' + job.name)
|
223
|
+
job.set_as_active
|
224
|
+
elsif job
|
225
|
+
# We cannot pull a job that isn't queued
|
226
|
+
log("cannot pull job that isn't queued: " + job_name)
|
227
|
+
job = nil
|
228
|
+
end
|
229
|
+
unlock
|
230
|
+
return job
|
231
|
+
end
|
232
|
+
|
233
|
+
def mark_job_done(job = nil)
|
234
|
+
throw ArgumentError unless job
|
235
|
+
throw ArgumentError unless job.own?
|
236
|
+
lock
|
237
|
+
if internal_job_exists?('run', job.name)
|
238
|
+
# Move to run
|
239
|
+
job.disown
|
240
|
+
FileUtils.mv(@dir + '/run/' + job.name, @dir + '/done/' + job.name)
|
241
|
+
job.set_status(@dir + '/done/' + job.name, ST_DONE)
|
242
|
+
else
|
243
|
+
log('attemped to mark invalid job as done: ' + job.name)
|
244
|
+
job = nil
|
245
|
+
end
|
246
|
+
unlock
|
247
|
+
return job
|
248
|
+
end
|
249
|
+
|
250
|
+
def mark_job_error(job = nil)
|
251
|
+
throw ArgumentError unless job
|
252
|
+
throw ArgumentError unless job.own?
|
253
|
+
lock
|
254
|
+
if internal_job_exists?('run', job.name)
|
255
|
+
# Move to run
|
256
|
+
job.disown
|
257
|
+
FileUtils.mv(@dir + '/run/' + job.name, @dir + '/_err/' + job.name)
|
258
|
+
job.set_status(@dir + '/_err/' + job.name, ST_ERROR)
|
259
|
+
else
|
260
|
+
log('attemped to mark invalid job as error: ' + job.name)
|
261
|
+
job = nil
|
262
|
+
end
|
263
|
+
unlock
|
264
|
+
return job
|
265
|
+
end
|
266
|
+
|
267
|
+
# The assumption here is that this is for testing or that
|
268
|
+
# this process couldn't handle the job and it has been
|
269
|
+
# changed so another script could handle the job. Otherwise,
|
270
|
+
# an infinite loop is created with this method
|
271
|
+
def reinsert_job(job = nil)
|
272
|
+
throw ArgumentError unless job
|
273
|
+
throw ArgumentError unless job.own?
|
274
|
+
lock
|
275
|
+
# TODO: handle other job types later
|
276
|
+
if internal_job_exists?('run', job.name)
|
277
|
+
# Move to que
|
278
|
+
job.disown
|
279
|
+
FileUtils.mv(@dir + '/run/' + job.name, @dir + '/que/' + job.name)
|
280
|
+
job.set_status(@dir + '/que/' + job.name, ST_QUEUED)
|
281
|
+
else
|
282
|
+
log('attemped to reinsert job that could not be found: ' + job.name)
|
283
|
+
job = nil
|
284
|
+
end
|
285
|
+
unlock
|
286
|
+
return job
|
287
|
+
end
|
288
|
+
|
289
|
+
def files_for_store
|
290
|
+
d = [
|
291
|
+
['', 'd'],
|
292
|
+
['_lock', 'w'],
|
293
|
+
['_log', 'w'],
|
294
|
+
['_tmp', 'd'],
|
295
|
+
['que', 'd'],
|
296
|
+
['run', 'd'],
|
297
|
+
['pause', 'd'],
|
298
|
+
['done', 'd'],
|
299
|
+
['_err', 'd'],
|
300
|
+
]
|
301
|
+
return d
|
302
|
+
end
|
303
|
+
|
304
|
+
def que_path
|
305
|
+
return @dir + '/que/'
|
306
|
+
end
|
307
|
+
|
308
|
+
def run_que_path
|
309
|
+
return @dir + '/run/'
|
310
|
+
end
|
311
|
+
|
312
|
+
def done_que_path
|
313
|
+
return @dir + '/done/'
|
314
|
+
end
|
315
|
+
|
316
|
+
def error_que_path
|
317
|
+
return @dir + '/_err/'
|
318
|
+
end
|
319
|
+
|
320
|
+
def pause_que_path
|
321
|
+
return @dir + '/pause/'
|
322
|
+
end
|
323
|
+
|
324
|
+
def create_queue_dirs
|
325
|
+
files_for_store.map {
|
326
|
+
| e |
|
327
|
+
path = @dir + '/' + e[0]
|
328
|
+
next if File.exists? path
|
329
|
+
FileUtils.mkdir(path) if e[1] == 'd'
|
330
|
+
FileUtils.touch(path) if e[1] == 'w'
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
def verify_store
|
335
|
+
|
336
|
+
files_for_store.map {
|
337
|
+
| e |
|
338
|
+
if e[1] == 'w'
|
339
|
+
unless FileTest.exists?( @dir + '/' + e[0])
|
340
|
+
log "bad queue dir: file '#{e[0]}' does not exist"
|
341
|
+
return false
|
342
|
+
end
|
343
|
+
unless FileTest.writable?( @dir + '/' + e[0])
|
344
|
+
log "bad queue dir: file '#{e[0]}' not writable"
|
345
|
+
return false
|
346
|
+
end
|
347
|
+
elsif e[1] == 'd'
|
348
|
+
unless FileTest.directory?( @dir + '/' + e[0])
|
349
|
+
log "bad queue dir: '#{e[0]}' not a directory"
|
350
|
+
return false
|
351
|
+
end
|
352
|
+
else
|
353
|
+
log "Bad code in verify store"
|
354
|
+
return false
|
355
|
+
end
|
356
|
+
}
|
357
|
+
|
358
|
+
@err = ''
|
359
|
+
return true
|
360
|
+
|
361
|
+
end
|
362
|
+
|
363
|
+
|
364
|
+
private
|
365
|
+
|
366
|
+
def lock
|
367
|
+
@lock = LockFile.new(@dir + '/_lock') if not @lock
|
368
|
+
|
369
|
+
@lock.lock
|
370
|
+
end
|
371
|
+
|
372
|
+
def unlock
|
373
|
+
@lock.unlock
|
374
|
+
end
|
375
|
+
|
376
|
+
def insert(opts = {})
|
377
|
+
unless opts[:XX_type] && opts[:XX_data]
|
378
|
+
log("Invalid call to insert. Missing arguments (see log)")
|
379
|
+
return nil
|
380
|
+
end
|
381
|
+
|
382
|
+
name = tmpName = ''
|
383
|
+
|
384
|
+
lock
|
385
|
+
|
386
|
+
begin
|
387
|
+
# Flaw - what if somebody screwed up and their
|
388
|
+
# are files there from the future
|
389
|
+
# TODO: handle that case
|
390
|
+
# We should always succeed if there is room!
|
391
|
+
1000.times do
|
392
|
+
|i|
|
393
|
+
n = generate_name
|
394
|
+
|
395
|
+
tmpName = @dir + '/_tmp/' + n
|
396
|
+
|
397
|
+
if i > 5
|
398
|
+
log("Queue insertion problem: too many collisions")
|
399
|
+
elsif i > 990
|
400
|
+
log("Queue insertion problem: too many collisions")
|
401
|
+
unlock
|
402
|
+
return nil
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
if FileTest.exists?(tmpName)
|
407
|
+
sleep 0.001
|
408
|
+
next
|
409
|
+
end
|
410
|
+
|
411
|
+
# We have our name
|
412
|
+
FileUtils.mkdir(tmpName)
|
413
|
+
# Do addtional TODO tests for writability
|
414
|
+
# and lack of pre-existing files
|
415
|
+
|
416
|
+
unlock
|
417
|
+
|
418
|
+
name = n
|
419
|
+
|
420
|
+
break
|
421
|
+
end
|
422
|
+
|
423
|
+
rescue
|
424
|
+
log($!)
|
425
|
+
unlock
|
426
|
+
return false
|
427
|
+
end
|
428
|
+
|
429
|
+
opts[:XX_status] = 'inserted'
|
430
|
+
|
431
|
+
if opts[:XX_type] == 'file'
|
432
|
+
fname = opts[:XX_data]
|
433
|
+
opts[:XX_original_file_name] = fname
|
434
|
+
# Move file to new dir
|
435
|
+
FileUtils.mv(fname, @dir + '/_tmp/' + name + '/data')
|
436
|
+
else
|
437
|
+
File.open(@dir + '/_tmp/' + name + '/data', "w") { |f| f.write(opts[:XX_data]) }
|
438
|
+
end
|
439
|
+
|
440
|
+
# Write meta file
|
441
|
+
File.open(@dir + '/_tmp/' + name + '/meta.yml', "w") do
|
442
|
+
| out |
|
443
|
+
YAML.dump(opts, out)
|
444
|
+
end
|
445
|
+
|
446
|
+
# Move job to '/que' directory
|
447
|
+
FileUtils.mv(@dir + '/_tmp/' + name, @dir + '/que/' + name)
|
448
|
+
|
449
|
+
job = Job.create(self, name, @dir + '/que/' + name, ST_QUEUED)
|
450
|
+
return job
|
451
|
+
end
|
452
|
+
|
453
|
+
end
|
454
|
+
|
455
|
+
end
|