multi_worker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +110 -0
  8. data/Rakefile +22 -0
  9. data/lib/multi_worker/adapters/backburner.rb +32 -0
  10. data/lib/multi_worker/adapters/delayed_job.rb +30 -0
  11. data/lib/multi_worker/adapters/inline.rb +21 -0
  12. data/lib/multi_worker/adapters/qu.rb +30 -0
  13. data/lib/multi_worker/adapters/que.rb +47 -0
  14. data/lib/multi_worker/adapters/queue_classic.rb +29 -0
  15. data/lib/multi_worker/adapters/resque.rb +59 -0
  16. data/lib/multi_worker/adapters/sidekiq.rb +56 -0
  17. data/lib/multi_worker/adapters/sneakers.rb +32 -0
  18. data/lib/multi_worker/adapters/sucker_punch.rb +25 -0
  19. data/lib/multi_worker/adapters/threaded_in_memory_queue.rb +27 -0
  20. data/lib/multi_worker/adapters/torquebox_backgroundable.rb +25 -0
  21. data/lib/multi_worker/interface.rb +24 -0
  22. data/lib/multi_worker/tasks.rb +13 -0
  23. data/lib/multi_worker/version.rb +3 -0
  24. data/lib/multi_worker.rb +79 -0
  25. data/multi_worker.gemspec +56 -0
  26. data/spec/adapters/backburner_spec.rb +32 -0
  27. data/spec/adapters/delayed_job_spec.rb +32 -0
  28. data/spec/adapters/inline_spec.rb +18 -0
  29. data/spec/adapters/qu_spec.rb +29 -0
  30. data/spec/adapters/que_spec.rb +44 -0
  31. data/spec/adapters/queue_classic_spec.rb +41 -0
  32. data/spec/adapters/resque_spec.rb +74 -0
  33. data/spec/adapters/sidekiq_spec.rb +91 -0
  34. data/spec/adapters/sneakers_spec.rb +36 -0
  35. data/spec/adapters/sucker_punch_spec.rb +36 -0
  36. data/spec/adapters/threaded_in_memory_queue_spec.rb +25 -0
  37. data/spec/adapters/torquebox_backgroundable_spec.rb +31 -0
  38. data/spec/configuration_spec.rb +59 -0
  39. data/spec/delayed/backend/test.rb +113 -0
  40. data/spec/delayed/serialization/test.rb +0 -0
  41. data/spec/shared/worker_spec.rb +18 -0
  42. data/spec/spec_helper.rb +22 -0
  43. data/spec/test_workers.rb +7 -0
  44. metadata +427 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eff1b67f7d6f0861193b1164fd5976aa053eaa37
4
+ data.tar.gz: 7d54dffeb1ce6da386919473cb194b0046658e34
5
+ SHA512:
6
+ metadata.gz: bbaca27df3e2b4c4aaff3faa79658934fd846ffab3da1f76dac7d24586955983099f34e858109bb723b85b91adb31a85061e2d6165aa100e1f79d0d590369b19
7
+ data.tar.gz: 688cc31e9c28ba87847fccf894dc2bf41cab53005fbe61f7b905fe44d2845df213adee4b574b4f97a6bf82c010daa452d1b69e3e332ee3cc4b63fa536628fcb5
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/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format progress
3
+ --require spec_helper
4
+ --pattern "spec/*_spec.rb"
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - jruby-19mode
7
+ - rbx
8
+ matrix:
9
+ allow_failures:
10
+ - rvm: rbx
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in multi_worker.gemspec
4
+ gemspec
5
+
6
+ #gem 'pry-debugger'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 David Butler
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,110 @@
1
+ # MultiWorker [![Build Status](https://travis-ci.org/dwbutler/multi_worker.png?branch=master)](https://travis-ci.org/dwbutler/multi_worker)
2
+
3
+ MultiWorker provides a common interface to the (many) Ruby queueing/worker libraries.
4
+ They are all very similar, but provide slightly different interfaces. This makes it difficult
5
+ to switch from one library to another, or to use multiple libraries at the same time.
6
+
7
+ Similar to MultiJSON or ExecJS, MultiWorker automatically detects installed queuing libaries
8
+ and the correct adapter is loaded up by default. Changing queueing libraries does not require
9
+ any change to worker code, as long as the standard interface is used.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'multi_worker'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install multi_worker
24
+
25
+ ## Basic Usage
26
+
27
+ ### Define worker
28
+
29
+ ```ruby
30
+ require 'sidekiq'
31
+ require 'multi_worker'
32
+
33
+ class ExampleWorker
34
+ worker
35
+
36
+ def perform(foo, bar)
37
+ # long running code
38
+ end
39
+ end
40
+ ```
41
+
42
+ ### Queue jobs
43
+
44
+ ```ruby
45
+ ExampleWorker.perform_async(1, 2)
46
+ # Equivalent:
47
+ MultiWorker.enqueue(ExampleWorker, 1, 2)
48
+ ```
49
+
50
+ ### Work jobs
51
+
52
+ Add to Rakefile:
53
+ ```ruby
54
+ require 'resque'
55
+ require 'multi_worker/tasks'
56
+
57
+ # If not using Rails, define your own :environment task that will require dependencies
58
+ task :environment do
59
+ require 'resque'
60
+ end
61
+ ```
62
+
63
+ Run:
64
+
65
+ ```
66
+ QUEUE=default rake multi_worker:work
67
+ ```
68
+
69
+ ## Advanced Configuration
70
+
71
+ ```ruby
72
+ MultiWorker.configure do
73
+ default_queue :processing
74
+ default_adapter :resque
75
+ default_options :retry => true
76
+ end
77
+
78
+ class AdvancedWorker
79
+ worker :queue => :background, :unique => true, :adapter => :sidekiq
80
+
81
+ def perform(foo)
82
+ ...
83
+ end
84
+ end
85
+ ```
86
+
87
+ ## Feature Comparison
88
+
89
+ | Library | Backends | Status | Retry | Lock | Unique | Scheduling | Priority | Async Method Proxy | Rake Task | Inline |
90
+ |--------------------------|----------------------|--------|-------|------|--------|------------|----------|--------------------|-----------|--------|
91
+ | Resque | Redis | Gem | Gem | Gem | Gem | Gem | | Gem | ✓ | ✓ |
92
+ | Sidekiq | Redis | Gem | ✓ | Gem | Gem | ✓ | | ✓ | ✗ | ✓ |
93
+ | Delayed Job | Active Record, Mongo | | | | | ✓ | | ✓ | ✓ | ✓ |
94
+ | Qu | Redis, Mongo, SQS | | | | | | | | ✓ | ✓ |
95
+ | Queue Classic | PostgreSQL | | | | | | | | ✓ | ✗ |
96
+ | Que | PostgreSQL | | | | | ✓ | ✓ | | ✓ | ✓ |
97
+ | Sneakers | RabbitMQ | | | | | | | | ✗ | ✗ |
98
+ | TorqueBox Backgroundable | HornetQ | ✓ | | | | | | ✓ | ✗ | ✗ |
99
+ | Backburner | Beanstalkd | | ✓ | | | ✓ | ✓ | ✓ | ✓ | ✓ |
100
+ | Threaded in Memory Queue | In-Memory | | | | | | | | N/A | ✓ |
101
+ | Sucker Punch | In-Memory | | | | | | | | N/A | ✓ |
102
+ | Inline | N/A | | | | | | | | N/A | ✓ |
103
+
104
+ ## Contributing
105
+
106
+ 1. Fork it ( http://github.com/dwbutler/multi_worker/fork )
107
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
108
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
109
+ 4. Push to the branch (`git push origin my-new-feature`)
110
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require "bundler/gem_tasks"
2
+ require "multi_worker"
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Run RSpec"
7
+ RSpec::Core::RakeTask.new do |t|
8
+ t.verbose = true
9
+ t.pattern = "spec/*_spec.rb"
10
+ end
11
+
12
+ MultiWorker::AdapterNames.each do |adapter_name|
13
+ namespace :spec do
14
+ desc "Run #{adapter_name} adapter spec"
15
+ RSpec::Core::RakeTask.new(adapter_name) do |t|
16
+ t.verbose = true
17
+ t.pattern = "spec/adapters/#{adapter_name}_spec.rb"
18
+ end
19
+ end
20
+ end
21
+
22
+ task :default => [:spec].concat(MultiWorker::AdapterNames.map {|adapter_name| "spec:#{adapter_name}"})
@@ -0,0 +1,32 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Backburner
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+ include ::Backburner::Queue
8
+ queue @queue
9
+
10
+ def self.perform(*args)
11
+ self.new.perform(*args)
12
+ end
13
+
14
+ def self.perform_async(*args)
15
+ ::Backburner.enqueue(self, *args)
16
+ end
17
+
18
+ def perform_async(*args)
19
+ self.class.perform_async(*args)
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.rake_task
25
+ require 'rake'
26
+ require 'backburner/tasks'
27
+
28
+ ::Rake::Task['backburner:work']
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class DelayedJob
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+
8
+ def self.perform(*args)
9
+ self.new.perform(*args)
10
+ end
11
+
12
+ def self.perform_async(*args)
13
+ delay(:queue => @queue).perform(*args)
14
+ end
15
+
16
+ def perform_async(*args)
17
+ self.class.perform_async(*args)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.rake_task
23
+ require 'rake'
24
+ require 'delayed/tasks'
25
+
26
+ ::Rake::Task['jobs:work']
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Inline
4
+ def self.configure(base, opts={})
5
+ base.instance_eval do
6
+ def self.perform(*args)
7
+ self.new.perform(*args)
8
+ end
9
+
10
+ def self.perform_async(*args)
11
+ perform(*args)
12
+ end
13
+
14
+ def perform_async(*args)
15
+ perform(*args)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Qu
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+
8
+ def self.perform(*args)
9
+ self.new.perform(*args)
10
+ end
11
+
12
+ def self.perform_async(*args)
13
+ ::Qu.enqueue(self, *args)
14
+ end
15
+
16
+ def perform_async(*args)
17
+ self.class.perform_async(*args)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.rake_task
23
+ require 'rake'
24
+ require 'qu/tasks'
25
+
26
+ ::Rake::Task['qu:work']
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Que
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+
8
+ def self.perform(*args)
9
+ self.new.perform(*args)
10
+ end
11
+
12
+ def self.perform_async(*args)
13
+ @job_klass.queue(*args)
14
+ end
15
+
16
+ def perform_async(*args)
17
+ self.class.perform_async(*args)
18
+ end
19
+
20
+ worker_klass = self
21
+
22
+ @job_klass = Class.new(::Que::Job)
23
+ @job_klass.class_eval do
24
+ @worker_klass = worker_klass
25
+
26
+ class << self
27
+ attr_reader :worker_klass
28
+ end
29
+
30
+ def run(*args)
31
+ self.class.worker_klass.new.perform(*args)
32
+ end
33
+ end
34
+
35
+ const_set :Job, @job_klass
36
+ end
37
+ end
38
+
39
+ def self.rake_task
40
+ require 'rake'
41
+ require 'que/rake_tasks'
42
+
43
+ ::Rake::Task['que:work']
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class QueueClassic
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = ::QC::Queue.new(opts[:queue])
7
+
8
+ def self.perform(*args)
9
+ self.new.perform(*args)
10
+ end
11
+
12
+ def self.perform_async(*args)
13
+ @queue.enqueue("#{self}.perform", *args)
14
+ end
15
+
16
+ def perform_async(*args)
17
+ self.class.perform_async(*args)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.rake_task
23
+ require 'rake'
24
+ require 'queue_classic/tasks'
25
+ ::Rake::Task["qc:work"]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,59 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Resque
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+
8
+ if @retry = opts[:retry]
9
+ require "resque-retry"
10
+ extend ::Resque::Plugins::Retry
11
+
12
+ @retry_limit = @retry[:limit]
13
+ @retry_delay = @retry[:delay]
14
+ end
15
+
16
+ if @lock = opts[:lock]
17
+ require "resque-lock-timeout"
18
+ extend ::Resque::Plugins::LockTimeout
19
+
20
+ if @lock.is_a?(Hash) && @lock[:timeout]
21
+ @lock_timeout = @lock[:timeout].to_i
22
+ end
23
+
24
+ if opts[:unique]
25
+ @loner = true
26
+ end
27
+ elsif opts[:unique]
28
+ require "resque-loner"
29
+ include ::Resque::Plugins::UniqueJob
30
+ end
31
+
32
+ if opts[:status]
33
+ require "resque-status"
34
+ include ::Resque::Plugins::Status
35
+ end
36
+
37
+ def self.perform(*args)
38
+ self.new.perform(*args)
39
+ end
40
+
41
+ def self.perform_async(*args)
42
+ ::Resque.enqueue(self, *args)
43
+ end
44
+
45
+ def perform_async(*args)
46
+ self.class.perform_async(*args)
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.rake_task
52
+ require 'rake'
53
+ require 'resque/tasks'
54
+
55
+ ::Rake::Task['resque:work']
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,56 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Sidekiq
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ if opts[:status]
7
+ require 'sidekiq_status'
8
+ include ::SidekiqStatus::Worker
9
+ else
10
+ include ::Sidekiq::Worker
11
+ end
12
+
13
+ @sidekiq_options = ::Sidekiq.default_worker_options.symbolize_keys
14
+ @sidekiq_options.merge! :queue => opts[:queue]
15
+
16
+ if opts.has_key?(:backtrace)
17
+ @sidekiq_options.merge! :backtrace => opts[:backtrace]
18
+ end
19
+
20
+ if opts.has_key?(:retry)
21
+ @retry = opts[:retry]
22
+ case @retry
23
+ when Hash
24
+ @sidekiq_options.merge!(:retry => @retry[:limit])
25
+ sidekiq_retry_in(&@retry[:delay])
26
+ when true, false, Fixnum
27
+ @sidekiq_options.merge!(:retry => @retry)
28
+ end
29
+ end
30
+
31
+ if opts[:lock]
32
+ require 'sidekiq-lock'
33
+ include ::Sidekiq::Lock::Worker
34
+
35
+ @sidekiq_options.merge!(:lock => opts[:lock])
36
+ end
37
+
38
+ if opts[:unique]
39
+ require "sidekiq-unique-jobs"
40
+ @sidekiq_options.merge!(:unique => true)
41
+ end
42
+
43
+ sidekiq_options @sidekiq_options
44
+
45
+ def self.perform(*args)
46
+ self.new.perform(*args)
47
+ end
48
+
49
+ def perform_async(*args)
50
+ self.class.perform_async(*args)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class Sneakers
4
+ def self.configure(base, opts={})
5
+ require 'json'
6
+
7
+ base.class_eval do
8
+ include ::Sneakers::Worker
9
+ from_queue opts[:queue], opts.fetch(:adapter_opts, {})
10
+
11
+ def work(msg)
12
+ args = JSON.parse(msg)
13
+ perform(*args)
14
+ end
15
+
16
+ def self.perform(*args)
17
+ self.new.perform(*args)
18
+ end
19
+
20
+ def self.perform_async(*args)
21
+ ::Sneakers.publish(args.to_json, :to_queue => @queue)
22
+ end
23
+
24
+ def perform_async(*args)
25
+ self.class.perform_async(*args)
26
+ #@queue.exchange.publish(args.to_json, :to_queue => @queue)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class SuckerPunch
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+
8
+ include ::SuckerPunch::Job
9
+
10
+ def self.perform(*args)
11
+ self.new.perform(*args)
12
+ end
13
+
14
+ def self.perform_async(*args)
15
+ self.new.async.perform(*args)
16
+ end
17
+
18
+ def perform_async(*args)
19
+ async.perform(*args)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class ThreadedInMemoryQueue
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ @queue = opts[:queue]
7
+
8
+ def self.call(*args)
9
+ perform(*args)
10
+ end
11
+
12
+ def self.perform(*args)
13
+ self.new.perform(*args)
14
+ end
15
+
16
+ def self.perform_async(*args)
17
+ ::ThreadedInMemoryQueue.enqueue(self, *args)
18
+ end
19
+
20
+ def perform_async(*args)
21
+ self.class.perform_async(*args)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module MultiWorker
2
+ module Adapters
3
+ class TorqueboxBackgroundable
4
+ def self.configure(base, opts={})
5
+ base.class_eval do
6
+ include ::TorqueBox::Messaging::Backgroundable
7
+
8
+ def self.perform(*args)
9
+ self.new.perform(*args)
10
+ end
11
+
12
+ def self.perform_async(*args)
13
+ perform(*args)
14
+ end
15
+
16
+ def perform_async(*args)
17
+ perform(*args)
18
+ end
19
+
20
+ always_background :perform_async
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module MultiWorker
2
+ module Interface
3
+ # Configures queueing for a class or module.
4
+ # Options:
5
+ # :queue => name of the queue to use (defaults to :default)
6
+ # :mailer => Queue mail messages (only for ActionMailer) (defaults to false)
7
+ # :lockable => Use locking on the job (defaults to true)
8
+ # :lock_timeout => Optional lock timeout
9
+ # :loner => Make this job unique in the queue (defaults to false)
10
+ # :status => Turn on status tracking (defaults to false)
11
+ #
12
+ # Example:
13
+ #
14
+ # class WorkerClass
15
+ # worker :queue => :processing, :loner => true
16
+ def worker(opts={})
17
+ opts = MultiWorker.default_options.merge(opts)
18
+ adapter_klass = MultiWorker.adapter(opts[:adapter])
19
+ adapter_klass.configure(self, opts)
20
+ end
21
+ end
22
+ end
23
+
24
+ Class.send :include, MultiWorker::Interface
@@ -0,0 +1,13 @@
1
+ require 'rake'
2
+ require 'multi_worker'
3
+
4
+ # No-op task in case it doesn't already exist
5
+ task :environment
6
+
7
+ namespace :multi_worker do
8
+ desc "Start a new worker for #{MultiWorker.default_adapter}"
9
+ task :work => :environment do
10
+ rake_task = MultiWorker.adapter.rake_task rescue fail("No rake task available for #{MultiWorker.default_adapter}")
11
+ rake_task.execute
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module MultiWorker
2
+ VERSION = "0.1.0"
3
+ end