fist_of_fury 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: aaac55f7aa5ff55179438b0849987c260000e695
4
+ data.tar.gz: cb691189d955e8d22fe2b2a25997da15f2321c47
5
+ SHA512:
6
+ metadata.gz: daec4f0b46b06ae89773d9c68783ce2d59c2c201fde9ea21a5d59f3d17545790c1ecc293edf0227f93f8a39dcf9c5c35a8a9643816af688910d0712e7eb6b812
7
+ data.tar.gz: 90c799dab5cae395152b2bdecd623cd200a8e93ba0c1f48fae0d021faad3a720a12bf0d010b2c2d60bb095aa1dbcd95ccfb101af4e312ec261dd578345408e75
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 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fist_of_fury.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Joshua Rieken
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,79 @@
1
+ # Fist of Fury
2
+
3
+ Recurring jobs for [Sucker Punch](https://github.com/brandonhilkert/sucker_punch).
4
+
5
+ **This is alpha software. Use at your own risk!**
6
+
7
+ ## Why
8
+
9
+ Sucker Punch offers the capability to run background jobs within the web server process, allowing for less use of resources and utilization of free hosting on services such as Heroku. However, it does not include the ability to run jobs at scheduled intervals. This is where Fist of Fury comes in.
10
+
11
+ ## What
12
+
13
+ Fist of Fury is heavily inspired by [Sidetiq](https://github.com/tobiassvn/sidetiq) for [Sidekiq](https://github.com/mperham/sidekiq). It uses the [ice_cube](https://github.com/seejohnrun/ice_cube) gem for easy creation of recurrence rules.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'fist_of_fury', '~> 0.1'
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install fist_of_fury
28
+
29
+ ## Usage
30
+
31
+ You can schedule your jobs within the jobs themselves:
32
+
33
+ ```Ruby
34
+ # app/jobs/say_hi_job.rb
35
+
36
+ class SayHiJob
37
+ include SuckerPunch::Job
38
+ include FistOfFury::Recurrent
39
+
40
+ recurs { minutely }
41
+
42
+ def perform
43
+ Rails.logger.info 'Hi!'
44
+ end
45
+ end
46
+ ```
47
+
48
+ Then in the initializer:
49
+
50
+ ```Ruby
51
+ # config/initializers/fist_of_fury.rb
52
+
53
+ # Ensures the jobs don't run while in the Rails console.
54
+ unless defined?(Rails::Console)
55
+ FistOfFury.attack!
56
+ end
57
+ ```
58
+
59
+ Or you can even specify the recurrence rules all within the initializer:
60
+
61
+ ```Ruby
62
+ # config/initializers/fist_of_fury.rb
63
+
64
+ # Ensures the jobs don't run while in the Rails console.
65
+ unless defined?(Rails::Console)
66
+ FistOfFury.attack! do
67
+ LogJob.recurs { secondly(3) }
68
+ end
69
+ end
70
+ ```
71
+
72
+
73
+ ## Contributing
74
+
75
+ 1. Fork it ( http://github.com/<my-github-username>/fist_of_fury/fork )
76
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
77
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
78
+ 4. Push to the branch (`git push origin my-new-feature`)
79
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fist_of_fury/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'fist_of_fury'
8
+ gem.version = FistOfFury::VERSION
9
+ gem.authors = ['Joshua Rieken']
10
+ gem.email = ['joshua@joshuarieken.com']
11
+ gem.description = %q{Recurring jobs for Sucker Punch}
12
+ gem.summary = gem.description
13
+ gem.homepage = 'https://github.com/facto/fist_of_fury'
14
+ gem.license = 'MIT'
15
+
16
+ gem.files = `git ls-files -z`.split("\x0")
17
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ['lib']
20
+
21
+ gem.add_development_dependency 'pry'
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'rspec'
24
+ gem.add_development_dependency 'guard'
25
+ gem.add_development_dependency 'guard-rspec'
26
+
27
+ gem.add_dependency 'sucker_punch', '>= 1.0.2'
28
+ gem.add_dependency 'celluloid', '>= 0.15.2'
29
+ gem.add_dependency 'ice_cube', '~> 0.11.3'
30
+ end
@@ -0,0 +1,50 @@
1
+ module FistOfFury
2
+ module Actor
3
+ class Clock
4
+ include Logging
5
+ include FistOfFury::Actor
6
+
7
+ attr_reader :schedules
8
+
9
+ def initialize(*args, &block)
10
+ after(0) do
11
+ debug 'FistOfFury::Clock looping...'
12
+ loop!
13
+ end
14
+ end
15
+
16
+ def schedule_for(worker)
17
+ worker.schedule if worker.respond_to? :schedule
18
+ end
19
+
20
+ def tick(tick = current_time)
21
+ FistOfFury.workers.each do |worker|
22
+ FistOfFury.dispatcher.dispatch(worker, tick)
23
+ end
24
+ end
25
+
26
+ def current_time
27
+ # TODO: configurable time zone support
28
+ Time.now
29
+ end
30
+
31
+ private
32
+
33
+ def loop!
34
+ after([time { tick }, 0].max) do
35
+ loop!
36
+ end
37
+ rescue StandardError => e
38
+ # TODO: global exception handling support
39
+ # handle_exception(e, context: 'FistOfFury::Clock#loop!')
40
+ raise e
41
+ end
42
+
43
+ def time
44
+ start = current_time
45
+ yield
46
+ 1 - (current_time.to_f - start.to_f)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ module FistOfFury
2
+ module Actor
3
+ class Dispatcher
4
+ include Logging
5
+ include FistOfFury::Actor
6
+
7
+ def dispatch(worker, tick)
8
+ schedule = worker.schedule
9
+ return unless schedule.schedule_next?(tick)
10
+ enqueue(worker, schedule.next_occurrence(tick))
11
+ rescue StandardError => e
12
+ # TODO: exception handling support
13
+ raise e
14
+ end
15
+
16
+ private
17
+
18
+ def enqueue(worker, next_run_time)
19
+ klass = worker.to_s
20
+ info "Enqueue: #{klass}"
21
+ last_run_time = FistOfFury.store["#{klass}:next"] || -1
22
+ next_run_time = next_run_time.to_f
23
+ FistOfFury.store["#{klass}:last"] = last_run_time
24
+ FistOfFury.store["#{klass}:next"] = next_run_time
25
+ worker.new.async.perform
26
+ rescue StandardError => e
27
+ # TODO: exception handling support
28
+ raise e
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module FistOfFury
2
+ module Actor
3
+ def self.included(base)
4
+ base.__send__(:include, Celluloid)
5
+ base.finalizer :fist_of_fury_finalizer
6
+ end
7
+
8
+ def initialize(*args, &block)
9
+ log_call 'initialize'
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def fist_of_fury_finalizer
16
+ log_call 'shutting down ...'
17
+ end
18
+
19
+ def log_call(call)
20
+ info "#{self.class.name} id: #{object_id} #{call}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module FistOfFury
2
+ module API
3
+ def workers
4
+ FistOfFury::Recurrent.subclasses(true)
5
+ end
6
+
7
+ def schedules
8
+ workers.map(&:schedule)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module FistOfFury
2
+ module Logging
3
+ [:fatal, :error, :warn, :info, :debug].each do |level|
4
+ define_method(level) do |msg|
5
+ FistOfFury.logger.__send__(level, "[FistOfFury] #{msg}")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module FistOfFury
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'fist_of_fury' do
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,42 @@
1
+ module FistOfFury
2
+ module Recurrent
3
+ extend SubclassTracking
4
+
5
+ module ClassMethods
6
+ include SubclassTracking
7
+
8
+ attr_writer :schedule
9
+
10
+ def schedule
11
+ @schedule ||= FistOfFury::Schedule.new
12
+ end
13
+
14
+ def last_scheduled_occurrence
15
+ scheduled_occurrence 'last'
16
+ end
17
+
18
+ def next_scheduled_occurrence
19
+ scheduled_occurrence 'next'
20
+ end
21
+
22
+ def recurs(options={}, &block)
23
+ schedule.instance_eval(&block)
24
+ schedule.options = options
25
+ end
26
+
27
+ private
28
+
29
+ def scheduled_occurrence(key)
30
+ (FistOfFury.store["#{self.class.to_s}:#{key}"] || -1).to_f
31
+ end
32
+ end
33
+
34
+ def self.included(klass)
35
+ super
36
+
37
+ klass.extend(FistOfFury::Recurrent::ClassMethods)
38
+ klass.extend(FistOfFury::SubclassTracking)
39
+ subclasses << klass
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ module FistOfFury
2
+ class Schedule
3
+ attr_accessor :options
4
+ attr_reader :last_occurrence
5
+
6
+ # TODO: configurable timezone support
7
+ START_TIME = Time.local(2010, 1, 1)
8
+
9
+ def initialize
10
+ @schedule = IceCube::Schedule.new(START_TIME)
11
+ end
12
+
13
+ def method_missing(meth, *args, &block)
14
+ if IceCube::Rule.respond_to?(meth)
15
+ rule = IceCube::Rule.send(meth, *args, &block)
16
+ @schedule.add_recurrence_rule(rule)
17
+ rule
18
+ elsif @schedule.respond_to?(meth)
19
+ @schedule.send(meth, *args, &block)
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def schedule_next?(time)
26
+ next_occurrence = @schedule.next_occurrence(time)
27
+ if @last_scheduled != next_occurrence
28
+ @last_scheduled = next_occurrence
29
+ true
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ def to_s
36
+ @schedule.to_s
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ module FistOfFury
2
+ module SubclassTracking
3
+ def subclasses(deep = false)
4
+ @subclasses ||= []
5
+
6
+ if deep
7
+ @subclasses.inject([]) do |result, subclass|
8
+ (result << subclass) + subclass.subclasses(true)
9
+ end
10
+ else
11
+ @subclasses
12
+ end
13
+ end
14
+
15
+ def inherited(klass)
16
+ super
17
+ subclasses << klass
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ module FistOfFury
2
+ class Supervisor < Celluloid::SupervisionGroup
3
+ supervise FistOfFury::Actor::Clock, as: :fist_of_fury_clock
4
+ # TODO: configurable pool size. For now, use Celluloid's CPU-based default.
5
+ pool FistOfFury::Actor::Dispatcher, as: :fist_of_fury_dispatcher
6
+
7
+ class << self
8
+ include Logging
9
+
10
+ def clock
11
+ run! if Celluloid::Actor[:fist_of_fury_clock].nil?
12
+ Celluloid::Actor[:fist_of_fury_clock]
13
+ end
14
+
15
+ def dispatcher
16
+ run! if Celluloid::Actor[:fist_of_fury_dispatcher].nil?
17
+ Celluloid::Actor[:fist_of_fury_dispatcher]
18
+ end
19
+
20
+ def run!
21
+ info 'FistOfFury::Supervisor start'
22
+ super
23
+ end
24
+
25
+ def run
26
+ raise "FistOfFury::Supervisor should not be run in the foreground."
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module FistOfFury
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,59 @@
1
+ # stdlib
2
+ require 'ostruct'
3
+ require 'singleton'
4
+ require 'socket'
5
+ require 'time'
6
+
7
+ # gems
8
+ require 'ice_cube'
9
+ require 'sucker_punch'
10
+ require 'celluloid'
11
+
12
+ # internal
13
+ require 'fist_of_fury/subclass_tracking'
14
+ require 'fist_of_fury/api'
15
+ require 'fist_of_fury/logging'
16
+ require 'fist_of_fury/schedule'
17
+ require 'fist_of_fury/recurrent'
18
+ require 'fist_of_fury/version'
19
+
20
+ # actor topology
21
+ require 'fist_of_fury/actor'
22
+ require 'fist_of_fury/actor/clock'
23
+ require 'fist_of_fury/actor/dispatcher'
24
+ require 'fist_of_fury/supervisor'
25
+
26
+ module FistOfFury
27
+ include FistOfFury::API
28
+
29
+ extend self
30
+
31
+ class << self
32
+ attr_writer :logger
33
+ end
34
+
35
+ def logger
36
+ @logger ||= SuckerPunch.logger
37
+ end
38
+
39
+ def clock
40
+ FistOfFury::Supervisor.clock
41
+ end
42
+
43
+ def dispatcher
44
+ FistOfFury::Supervisor.dispatcher
45
+ end
46
+
47
+ def store
48
+ @store ||= {}
49
+ end
50
+
51
+ def attack!
52
+ return true if @attacking
53
+ yield if block_given?
54
+ FistOfFury::Supervisor.run!
55
+ @attacking = true
56
+ end
57
+ end
58
+
59
+ require 'fist_of_fury/railtie' if defined?(::Rails)
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe FistOfFury do
4
+ describe 'logger' do
5
+ it "delegates get to Sucker Punch's logger" do
6
+ expect(FistOfFury.logger).to eq SuckerPunch.logger
7
+ end
8
+ end
9
+
10
+ describe 'clock' do
11
+ it "delegates get to FistOfFury::Supervisor's clock" do
12
+ expect(FistOfFury::Supervisor).to receive(:clock)
13
+ FistOfFury.clock
14
+ end
15
+ end
16
+
17
+ describe 'store' do
18
+ it 'starts out as an empty hash' do
19
+ expect(FistOfFury.store).to eq Hash.new
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ begin
2
+ require 'pry'
3
+ rescue LoadError
4
+ end
5
+
6
+ require 'fist_of_fury'
7
+
8
+ RSpec.configure do |config|
9
+ config.after(:each) do
10
+ # Clean up the master queue list
11
+ SuckerPunch::Queues.instance_variable_set(:@queues, Set.new)
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,180 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fist_of_fury
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joshua Rieken
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
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: guard-rspec
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
+ - !ruby/object:Gem::Dependency
84
+ name: sucker_punch
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 1.0.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.0.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: celluloid
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 0.15.2
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 0.15.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: ice_cube
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.11.3
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.11.3
125
+ description: Recurring jobs for Sucker Punch
126
+ email:
127
+ - joshua@joshuarieken.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rspec"
134
+ - Gemfile
135
+ - Guardfile
136
+ - LICENSE.txt
137
+ - README.md
138
+ - Rakefile
139
+ - fist_of_fury.gemspec
140
+ - lib/fist_of_fury.rb
141
+ - lib/fist_of_fury/actor.rb
142
+ - lib/fist_of_fury/actor/clock.rb
143
+ - lib/fist_of_fury/actor/dispatcher.rb
144
+ - lib/fist_of_fury/api.rb
145
+ - lib/fist_of_fury/logging.rb
146
+ - lib/fist_of_fury/railtie.rb
147
+ - lib/fist_of_fury/recurrent.rb
148
+ - lib/fist_of_fury/schedule.rb
149
+ - lib/fist_of_fury/subclass_tracking.rb
150
+ - lib/fist_of_fury/supervisor.rb
151
+ - lib/fist_of_fury/version.rb
152
+ - spec/fist_of_fury_spec.rb
153
+ - spec/spec_helper.rb
154
+ homepage: https://github.com/facto/fist_of_fury
155
+ licenses:
156
+ - MIT
157
+ metadata: {}
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.2.2
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Recurring jobs for Sucker Punch
178
+ test_files:
179
+ - spec/fist_of_fury_spec.rb
180
+ - spec/spec_helper.rb