localjob 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: becc6c53c0de0692e654c0e4fd8b220b13baa424
4
+ data.tar.gz: 770a92796d0fedf97c3d66728e039b3d966e7f92
5
+ SHA512:
6
+ metadata.gz: f56f0b1ae32cda8650cd0e6375278ad34bf16c00db257a180d1070c980c7eee422f7e604afcbbe013c7da7c0ae403d2f0a7c795987fd52330f101fd19f3a985e
7
+ data.tar.gz: 3ecf92a571bd824625ea348b5f0ccdf8e4f5b7e3ba2fc3ab468a1915c90a768f5a250803d71a8a1caf41387c21d237320f605c250e6edcf4bdbcc035d4b1c90f
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in localjob.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Simon Eskildsen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Localjob
2
+
3
+ Localjob is a simple, self-contained background queue built on top of [POSIX
4
+ message queues][pmq]. Workers and the app pushing to the queue must reside on
5
+ the same machine. It's the sqlite of background queues (although sqlite scales
6
+ further than Localjob). Here's a post about [how it works][blog].
7
+
8
+ Localjob is for early-development situations where you don't need a
9
+ full-featured background queue, but just want to get started with something
10
+ simple that does not rely on any external services. A bigger goal with the
11
+ project is to be able to migrate to another background queue system by switching
12
+ adapter: `Localjob.adapter = Resque` to switch to Resque, without changes to
13
+ your own code.
14
+
15
+ The POSIX message queue is persistent till reboot. You will need to tune system
16
+ parameters for your application, please consult [posix-mqueue][pmq-gem]'s
17
+ documentation.
18
+
19
+ Localjob works on Ruby >= 2.0.0 and Linux.
20
+
21
+ WIP not everything works as advertised, but try it out and report anything that
22
+ doesn't work! It's not released as a gem yet, so add the following to your
23
+ Gemfile to use it:
24
+
25
+ ```ruby
26
+ gem 'localjob', git: "git@github.com:Sirupsen/localjob.git"
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Localjobs have the following format:
32
+
33
+ ```ruby
34
+ class EmailJob
35
+ def initialize(user_id, email)
36
+ @user, @email = User.find(user_id), email
37
+ end
38
+
39
+ def perform
40
+ @email.deliver_to(@user)
41
+ end
42
+ end
43
+ ```
44
+
45
+ To queue a job, create an instance of it and push it to the queue:
46
+
47
+ ```ruby
48
+ queue = Localjob.new
49
+ queue << EmailJob.new(current_user.id, welcome_email)
50
+ ```
51
+
52
+ Then spawn a worker with `localjob work`. It takes a few arguments, like `-d` to
53
+ deamonize itself. `localjob help work` to list all options.
54
+
55
+ [pmq]: http://linux.die.net/man/7/mq_overview
56
+ [pmq-gem]: https://github.com/Sirupsen/posix-mqueue
57
+ [blog]: http://sirupsen.com/unix-background-queue/
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => :test
4
+
5
+ require 'rake/testtask'
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.pattern = "test/**/*_test.rb"
9
+ end
data/bin/localjob ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'localjob'
3
+ require 'localjob/cli'
4
+
5
+ Localjob::CLI.start(ARGV)
@@ -0,0 +1,25 @@
1
+ class Localjob
2
+ class Channel
3
+ attr_accessor :queues
4
+
5
+ def initialize(queues)
6
+ @queues = [queues].flatten.map { |q| queue_from_name(q) }
7
+ end
8
+
9
+ def <<(queue)
10
+ @queues << queue_from_name(queue)
11
+ end
12
+
13
+ def shift
14
+ (queue,), = IO.select(@queues)
15
+ queue.shift
16
+ rescue POSIX::Mqueue::QueueEmpty
17
+ retry
18
+ end
19
+
20
+ private
21
+ def queue_from_name(queue)
22
+ queue.instance_of?(Localjob) ? queue : Localjob.new(queue)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ require 'thor'
2
+
3
+ class Localjob
4
+ class CLI < Thor
5
+ option :queues, aliases: ["-q"], type: :string, default: "localjob"
6
+ option :require, aliases: ["-r"], type: :string, default: "."
7
+ option :pid_file, aliases: ["-p"], type: :string
8
+ option :daemon, aliases: ["-d"], type: :boolean
9
+ desc "work", "Start worker to process jobs"
10
+ def work
11
+ load_environment options[:require]
12
+
13
+ queues = options[:queues].split(",")
14
+ Localjob::Worker.new(queues, options.slice(:daemon, :pid_file)).work
15
+ end
16
+
17
+ desc "size", "Outputs the size of queues"
18
+ option :queues, aliases: ["-q"], type: :string, default: "localjob"
19
+ def size
20
+ queues = options[:queues].split(",")
21
+ queues.each do |queue|
22
+ puts "Size of /#{queue}: #{Localjob.new(queue).size}/#{msg_max}"
23
+ end
24
+ end
25
+
26
+ desc "destroy", "Destroys all queues passed"
27
+ option :queues, aliases: ["-q"], type: :string, default: "localjob"
28
+ def destroy
29
+ options[:queues].split(",").each do |queue|
30
+ Localjob.new(queue).destroy
31
+ end
32
+ end
33
+
34
+ desc "list", "Lists all queues"
35
+ def list
36
+ unless File.exists?("/dev/mqueue")
37
+ system "mkdir /dev/mqueue"
38
+ system "mount -t mqueue none /dev/mqueue"
39
+ end
40
+
41
+ system "ls -l /dev/mqueue"
42
+ end
43
+
44
+ private
45
+ def load_environment(file)
46
+ if rails?(file)
47
+ require 'rails'
48
+ require File.expand_path("#{file}/config/environment.rb")
49
+ ::Rails.application.eager_load!
50
+ elsif File.file?(file)
51
+ require File.expand_path(file)
52
+ else
53
+ puts "No require path passed, requires -r if not in Rails"
54
+ exit
55
+ end
56
+ end
57
+
58
+ def rails?(file)
59
+ File.exists?(File.expand_path("#{file}/config/environment.rb"))
60
+ end
61
+
62
+ def msg_max
63
+ @msg_max ||= File.read("/proc/sys/fs/mqueue/msg_max")
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ class Localjob
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,65 @@
1
+ class Localjob
2
+ class Worker
3
+ attr_accessor :logger, :channel
4
+ attr_reader :options
5
+
6
+ def initialize(queues, logger: Logger.new(STDOUT), **options)
7
+ @channel, @logger = Channel.new(queues), logger
8
+ @options = options
9
+ @shutdown = false
10
+ end
11
+
12
+ def process(job)
13
+ logger.info "Worker #{pid}: #{job.inspect}"
14
+ job.perform
15
+ end
16
+
17
+ def pid
18
+ Process.pid
19
+ end
20
+
21
+ def work
22
+ logger.info "Worker #{pid} now listening!"
23
+ trap_signals
24
+ create_pid_file(@options[:pid_file])
25
+ deamonize if @options[:deamon]
26
+ loop { shift_and_process }
27
+ end
28
+
29
+ private
30
+
31
+ def shift_and_process
32
+ exit! if @shutdown
33
+
34
+ job = wait { @channel.shift }
35
+ process job
36
+ rescue Object => e
37
+ logger.error "Worker #{pid} job failed: #{job}"
38
+ logger.error "#{$!}\n#{$@.join("\n")}"
39
+ end
40
+
41
+ def trap_signals
42
+ Signal.trap("QUIT") { graceful_shutdown }
43
+ end
44
+
45
+ def wait
46
+ @waiting = true
47
+ job = yield
48
+ @waiting = false
49
+ job
50
+ end
51
+
52
+ def deamonize
53
+ Process.daemon(true, true)
54
+ end
55
+
56
+ def create_pid_file(path)
57
+ File.open(path, 'w') { |f| f << self.pid } if path
58
+ end
59
+
60
+ def graceful_shutdown
61
+ exit! if @waiting
62
+ @shutdown = true
63
+ end
64
+ end
65
+ end
data/lib/localjob.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'posix/mqueue'
2
+ require 'yaml'
3
+ require 'logger'
4
+
5
+ require "localjob/version"
6
+ require 'localjob/channel'
7
+ require 'localjob/worker'
8
+
9
+ class Localjob
10
+ attr_reader :queue_name
11
+
12
+ def initialize(queue = "localjob")
13
+ @queue_name = fix_queue_name(queue)
14
+ end
15
+
16
+ def serializer
17
+ YAML
18
+ end
19
+
20
+ def queue
21
+ @queue ||= POSIX::Mqueue.new(@queue_name)
22
+ end
23
+
24
+ def <<(object)
25
+ queue.timedsend serializer.dump(object)
26
+ end
27
+
28
+ def size
29
+ queue.size
30
+ end
31
+
32
+ def shift
33
+ serializer.load queue.timedreceive
34
+ end
35
+
36
+ def destroy
37
+ queue.unlink
38
+ end
39
+
40
+ def to_io
41
+ queue.to_io
42
+ end
43
+
44
+ private
45
+ def fix_queue_name(queue)
46
+ queue.start_with?('/') ? queue : "/#{queue}"
47
+ end
48
+ end
data/localjob.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'localjob/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "localjob"
8
+ spec.version = Localjob::VERSION
9
+ spec.authors = ["Simon Eskildsen"]
10
+ spec.email = ["sirup@sirupsen.com"]
11
+ spec.description = %q{Simple, self-contained background queue built on top of POSIX message queues.}
12
+ spec.summary = %q{Simple, self-contained background queue built on top of POSIX message queues. It only works on Linux.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "posix-mqueue", "0.0.9"
22
+ spec.add_dependency "thor", "0.18.1"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "mocha"
27
+ end
data/test/jobs.rb ADDED
@@ -0,0 +1,11 @@
1
+ class WalrusJob < Struct.new(:action)
2
+ def perform
3
+ "Nice try, but there's no way I'll #{action}.."
4
+ end
5
+ end
6
+
7
+ class AngryWalrusJob < Struct.new(:angryness)
8
+ def perform
9
+ raise "I am this angry: #{angryness}"
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+
3
+ class LocaljobTest < LocaljobTestCase
4
+ def setup
5
+ @localjob = queue
6
+ end
7
+
8
+ def test_push_should_put_a_job_in_queue
9
+ @localjob << WalrusJob.new("move")
10
+ assert_equal 1, @localjob.size
11
+ end
12
+
13
+ def test_pop_from_queue
14
+ @localjob << WalrusJob.new("move")
15
+
16
+ job = @localjob.shift
17
+ assert_instance_of WalrusJob, job
18
+ assert_equal "move", job.action
19
+ end
20
+
21
+ def test_throws_error_if_message_is_too_large
22
+ assert_raises Errno::EMSGSIZE do
23
+ @localjob << AngryWalrusJob.new("f" * @localjob.queue.msgsize)
24
+ end
25
+ end
26
+
27
+ def test_handles_multiple_queues
28
+ @localjob << WalrusJob.new("move")
29
+
30
+ other = queue("other-queue")
31
+ other << WalrusJob.new("dance")
32
+
33
+ assert_equal 1, @localjob.size
34
+ assert_equal 1, other.size
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ require 'minitest/unit'
2
+ require 'minitest/autorun'
3
+ $:<< File.dirname(__FILE__) + "/../lib"
4
+ require 'localjob'
5
+ require "mocha/setup"
6
+ require 'jobs'
7
+
8
+ class LocaljobTestCase < MiniTest::Unit::TestCase
9
+ protected
10
+ # This is a method to make sure the logger is set right.
11
+ def worker(queues = ["/localjob-test"])
12
+ Localjob::Worker.new(queues, logger: logger)
13
+ end
14
+
15
+ # This is a method to make sure all queues are registred and destroyed after
16
+ # each teach run.
17
+ def queue(name = "/localjob-test")
18
+ @queues ||= []
19
+ queue = Localjob.new(name)
20
+ @queues << queue
21
+ queue
22
+ end
23
+
24
+ def teardown
25
+ clear_queue
26
+ end
27
+
28
+ def logger
29
+ return @logger if @logger
30
+
31
+ output_file = ENV["DEBUG"] ? STDOUT : "/dev/null"
32
+ @logger = Logger.new(output_file)
33
+ end
34
+
35
+ def clear_queue
36
+ @queues.each(&:destroy) if @queues
37
+
38
+ # This forces the GC to garbage collect, and thus close file descriptioners
39
+ # in POSIX::Mqueue. Otherwise we'll get flooded with warnings. This is to
40
+ # ensure a clean state everytime with a new message queue for each test.
41
+ # It's slower. But safe.
42
+ GC.start
43
+ end
44
+ end
@@ -0,0 +1,85 @@
1
+ require 'test_helper'
2
+
3
+ class WorkerTest < LocaljobTestCase
4
+ def setup
5
+ @localjob = queue
6
+ @worker = worker
7
+ end
8
+
9
+ def test_pop_and_send_to_worker
10
+ WalrusJob.any_instance.expects(:perform)
11
+
12
+ @localjob << WalrusJob.new("move")
13
+
14
+ job = @localjob.shift
15
+ @worker.process(job)
16
+ end
17
+
18
+ def test_working_off_queue_in_child
19
+ @localjob << WalrusJob.new("move")
20
+
21
+ fork do
22
+ job = @localjob.shift
23
+ @worker.process(job)
24
+ end
25
+
26
+ Process.wait
27
+ assert_equal 0, @localjob.size
28
+ end
29
+
30
+ def test_sigquit_terminates_the_worker
31
+ @localjob << WalrusJob.new("move")
32
+
33
+ assert_equal 1, @localjob.size
34
+
35
+ pid = fork { @worker.work }
36
+
37
+ Process.kill("QUIT", pid)
38
+ Process.wait
39
+
40
+ assert_equal 0, @localjob.size
41
+ end
42
+
43
+ def test_doesnt_stop_on_error
44
+ @localjob << AngryWalrusJob.new(100)
45
+ @localjob << WalrusJob.new("be happy")
46
+
47
+ pid = fork { @worker.work }
48
+
49
+ # Hack to account for race condition, 0.01s should be plenty
50
+ sleep 0.01
51
+ Process.kill("QUIT", pid)
52
+ Process.wait
53
+
54
+ assert_equal 0, @localjob.size
55
+ end
56
+
57
+ def test_workers_listen_on_multiple_queues
58
+ @localjob << WalrusJob.new("move")
59
+
60
+ other = queue("other-queue")
61
+ other << WalrusJob.new("dance")
62
+
63
+ @worker.channel << 'other-queue'
64
+
65
+ pid = fork { @worker.work }
66
+
67
+ # Hack to account for race condition, 0.01s should be plenty
68
+ sleep 0.01
69
+ Process.kill("QUIT", pid)
70
+ Process.wait
71
+
72
+ assert_equal 0, @localjob.size
73
+ assert_equal 0, other.size
74
+ end
75
+
76
+ def test_worker_doesnt_die_on_bad_serialization
77
+ @localjob.queue.timedsend "--- !ruby/object:Whatever {}\n"
78
+
79
+ pid = fork { @worker.work }
80
+
81
+ sleep 0.01
82
+ Process.kill("QUIT", pid)
83
+ Process.wait
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: localjob
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Simon Eskildsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: posix-mqueue
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.9
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.18.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.18.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Simple, self-contained background queue built on top of POSIX message
84
+ queues.
85
+ email:
86
+ - sirup@sirupsen.com
87
+ executables:
88
+ - localjob
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - .gitignore
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - bin/localjob
98
+ - lib/localjob.rb
99
+ - lib/localjob/channel.rb
100
+ - lib/localjob/cli.rb
101
+ - lib/localjob/version.rb
102
+ - lib/localjob/worker.rb
103
+ - localjob.gemspec
104
+ - test/jobs.rb
105
+ - test/localjob_test.rb
106
+ - test/test_helper.rb
107
+ - test/worker_test.rb
108
+ homepage: ''
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.0.3
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Simple, self-contained background queue built on top of POSIX message queues.
132
+ It only works on Linux.
133
+ test_files:
134
+ - test/jobs.rb
135
+ - test/localjob_test.rb
136
+ - test/test_helper.rb
137
+ - test/worker_test.rb