localjob 0.0.1

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