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 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