metrognome 0.0.1 → 0.0.2

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YzU2MDJhNDNmODk3YzA0ZGFlYWEyMDQyNWMyOGIzZWUzZDM0ZjBmOQ==
5
+ data.tar.gz: !binary |-
6
+ YTk4OWZiNmRiMWY4ODIwZDdlNmIzOGUxMmRlMzY0MmViOGQwYzFiNw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZTJkNjRjM2FhMGU4MDcyZWRlZmM5NThlMzUxZWZiZDhiNzk1N2NjNGUwZjlh
10
+ NzBhY2Q1NjdkMjBiM2ZmZTU5ZWVkMDA3MWYzMDE3ZDU3OWY4ZjA4ZWM2MmI2
11
+ YmYyNTc1YzY5NTg5ZjFjZDAzMjQ3NzUyNmM0YzVjYmNkYTkzMTc=
12
+ data.tar.gz: !binary |-
13
+ MGIyYjM0YzEzNzFjNjBmYThkNGM2ZTY3MzA0ZWY2NGZjNTI2MGUzNmZlNDMw
14
+ M2EwODE3NmEwZGRjYjhlNTdjMzQ3YTRmNmFkMzFiZGZlZWUwNmU2YzM3Nzdl
15
+ YmFlZDEyMzkxNDNiMmVjOTA2Mjc2OWEzOTBmOWFjNzk1MDlmOWU=
data/.gitignore CHANGED
@@ -2,6 +2,7 @@
2
2
  *.rbc
3
3
  .bundle
4
4
  .config
5
+ .rspec
5
6
  .yardoc
6
7
  Gemfile.lock
7
8
  InstalledFiles
@@ -15,3 +16,6 @@ spec/reports
15
16
  test/tmp
16
17
  test/version_tmp
17
18
  tmp
19
+
20
+ .ruby-version
21
+ .ruby-gemset
data/README.md CHANGED
@@ -18,7 +18,75 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- TODO: Write usage instructions here
21
+ ### Starting and Stopping
22
+
23
+ To start the metrognome, run:
24
+
25
+ $ bundle exec rake metrognome:start
26
+
27
+ In production, use RAILS\_ENV to let Rails know to start the right environment:
28
+
29
+ $ bundle exec rake RAILS_ENV=production metrognome:start
30
+
31
+ To stop the metrognome, run:
32
+
33
+ $ bundle exec rake metrognome:stop
34
+
35
+ ### Schedulers
36
+
37
+ Schedulers are registered with metrognome and run on a periodic basis.
38
+ Schedulers are `.rb` files read from the folder `config/metrognome`
39
+
40
+ #### Initialization and task
41
+
42
+ All schedulers are instances of Metrognome::Scheduler, initialized like:
43
+
44
+ ```ruby
45
+ beeper = Metrognome::Scheduler.new(10.seconds)
46
+ ```
47
+
48
+ The beeper task will be executed every 10 seconds beginning when the metrognome
49
+ starts. To define the task to execute, pass a block to `beeper.task`:
50
+
51
+ ```ruby
52
+ beeper.task do
53
+ puts "Beep!"
54
+ end
55
+ ```
56
+
57
+ Finally, register the task with `Metrognome::Registrar.register`:
58
+
59
+ ```ruby
60
+ Metrognome::Registrar.register beeper
61
+ ```
62
+
63
+ #### Setup and local variables
64
+
65
+ Most schedulers can benefit from some persistence, or at least some setup. Let's
66
+ have our beeper keep track of how many times it's beeped.
67
+
68
+ Accessors can be declared when the scheduler is initialized:
69
+
70
+ ```ruby
71
+ beeper = Metrognome::Scheduler.new(10.seconds, :counter)
72
+ ```
73
+
74
+ The counter can be given an initial value by passing a block to `beeper.setup`:
75
+
76
+ ```ruby
77
+ beeper.setup do
78
+ self.counter = 0
79
+ end
80
+ ```
81
+
82
+ Finally, the counter can be used in the task block:
83
+
84
+ ```ruby
85
+ beeper.task do
86
+ self.counter += 1
87
+ logger.info "Beep! #{counter} time(s)"
88
+ end
89
+ ```
22
90
 
23
91
  ## Contributing
24
92
 
@@ -3,5 +3,4 @@ require 'metrognome/railtie'
3
3
 
4
4
  require 'metrognome/scheduler'
5
5
  require 'metrognome/registrar'
6
- require 'metrognome/runner'
7
6
 
@@ -1,27 +1,41 @@
1
+ require 'singleton'
2
+
1
3
  module Metrognome
2
4
  class Registrar
3
- class << self
4
- def registered
5
- @registered ||= []
6
- end
5
+ include Singleton
7
6
 
8
- def register scheduler
9
- raise ArgumentError.new "scheduler must be a Metrognome::Scheduler!" unless scheduler.is_a? Metrognome::Scheduler
7
+ def self.register scheduler
8
+ self.instance.register scheduler
9
+ end
10
10
 
11
- @registered ||= []
12
- @registered << scheduler
13
- scheduler.lock = next_filename
14
- scheduler.last = Time.now - 1.year
15
- scheduler.call_setup
11
+ def register scheduler
12
+ unless scheduler.is_a? Metrognome::Scheduler
13
+ raise ArgumentError.new "scheduler #{scheduler.inspect} must be a Metrognome::Scheduler."
16
14
  end
17
15
 
18
- private
19
- def next_filename
20
- @counter ||= 0
21
- @counter += 1
22
- "metrognome-#{@counter}"
23
- end
16
+ @registered ||= []
17
+ @registered << scheduler
18
+ end
19
+
20
+ def start
21
+ @registered.each { |scheduler| scheduler.run }
22
+
23
+ wait_for_sigterm
24
+ Signal.trap("TERM") { exit 1 }
25
+
26
+ @registered.each do |scheduler|
27
+ scheduler.stop
28
+ scheduler.thread.run
29
+ end
30
+ @registered.each { |scheduler| scheduler.thread.join }
24
31
  end
32
+
33
+ private
34
+ # Sleep until we get a SIGTERM.
35
+ def wait_for_sigterm
36
+ Signal.trap("TERM") { return }
37
+ sleep
38
+ end
25
39
  end
26
40
  end
27
41
 
@@ -1,18 +1,11 @@
1
1
  module Metrognome
2
2
  class Scheduler
3
- attr_accessor :interval, :lock, :last
3
+ attr_reader :thread
4
4
 
5
- def initialize interval, *variables
6
- self.interval = interval
7
-
8
- variables.each do |v|
9
- raise ArgumentError.new "All variables must be Strings or Symbols!" unless v.is_a? String or v.is_a? Symbol
10
- eval "class << self; attr_accessor :#{v}; end"
11
- end
12
- end
13
-
14
- def lock
15
- File.join(Rails.root, 'tmp', @lock)
5
+ def initialize interval, description = 'Anonymous scheduler'
6
+ @interval = interval
7
+ @description = description
8
+ @stopped = false
16
9
  end
17
10
 
18
11
  def setup &block
@@ -23,15 +16,26 @@ module Metrognome
23
16
  @task = block
24
17
  end
25
18
 
26
- def call_setup
27
- instance_eval &@setup if @setup
19
+ def stop
20
+ @stopped = true
28
21
  end
29
22
 
30
- def call_task
31
- File.open(lock, File::RDWR | File::CREAT, 0600) do |file|
32
- break unless file.flock(File::LOCK_EX | File::LOCK_NB)
33
- self.last = Time.now
34
- instance_eval &@task if @task
23
+ def run
24
+ @thread = Thread.new do
25
+ instance_eval(&@setup) if @setup
26
+
27
+ until @stopped do
28
+ start = Time.now
29
+ Rails.logger.info "#{@description} beginning task execution."
30
+ instance_eval(&@task) if @task
31
+ Rails.logger.info "#{@description} completed task execution."
32
+ # TODO log stopping task
33
+
34
+ until @stopped do
35
+ to_sleep = start + @interval - Time.now
36
+ break if to_sleep <= 0 or Kernel.sleep(to_sleep) >= to_sleep
37
+ end
38
+ end
35
39
  end
36
40
  end
37
41
  end
@@ -1,3 +1,3 @@
1
1
  module Metrognome
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -14,11 +14,13 @@ namespace :metrognome do
14
14
  desc 'Begin the metrognome scheduler'
15
15
  task :start => :register do
16
16
  unless File.exists? Metrognome::PIDFILE
17
+ # Ignore SIGHUP since we'll probably be started from an SSH session.
17
18
  Signal.trap 'HUP', 'IGNORE'
18
19
 
19
20
  pid = fork do
20
21
  Rails.logger = Logger.new 'log/metrognome.log'
21
- Metrognome::Runner.start
22
+ Rails.logger.formatter = Logger::Formatter.new
23
+ Metrognome::Registrar.instance.start
22
24
  end
23
25
 
24
26
  Process.detach pid
@@ -18,4 +18,6 @@ Gem::Specification.new do |gem|
18
18
  gem.require_paths = ["lib"]
19
19
 
20
20
  gem.add_runtime_dependency 'rails', '~> 3.2'
21
+
22
+ gem.add_development_dependency 'rspec'
21
23
  end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'active_support/time'
3
+
4
+ describe Metrognome::Scheduler do
5
+ before do
6
+ @dummy_scheduler = Metrognome::Scheduler.new 1.minute, 'Test scheduler'
7
+ @dummy_scheduler.setup { @task_counter = 0 }
8
+ @dummy_scheduler.task do
9
+ if @task_counter == 5
10
+ self.stop
11
+ else
12
+ @task_counter += 1
13
+ end
14
+ end
15
+
16
+ @slept = 0
17
+ Kernel.stub(:sleep) do |arg|
18
+ # Make sure we're only sleeping up to the schedule time.
19
+ arg.should be < 1.minute
20
+ @slept += 1
21
+ arg + 1 # Always break out of the sleep loop.
22
+ end
23
+
24
+ # Metrognome should run in a Rails app, so set the logger we use.
25
+ Rails.logger = Logger.new nil
26
+ end
27
+
28
+ it 'should count to 5' do
29
+ @dummy_scheduler.run
30
+ Thread.pass while @dummy_scheduler.thread.alive?
31
+ @slept.should == 5
32
+ @dummy_scheduler.instance_eval { @task_counter }.should == 5
33
+ end
34
+
35
+ it 'should exit without exception' do
36
+ @dummy_scheduler.run
37
+ Thread.pass while @dummy_scheduler.thread.alive?
38
+
39
+ # The scheduler thread status is nil on exit due to exception.
40
+ @dummy_scheduler.thread.status.should === false
41
+ end
42
+ end
@@ -0,0 +1,4 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'metrognome'
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metrognome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 0.0.2
6
5
  platform: ruby
7
6
  authors:
8
7
  - Chris Carlon
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-22 00:00:00.000000000 Z
11
+ date: 2014-03-31 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rails
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
@@ -22,11 +20,24 @@ dependencies:
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ~>
28
25
  - !ruby/object:Gem::Version
29
26
  version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
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'
30
41
  description: A simple repeating task scheduler for Rails applications
31
42
  email:
32
43
  - chris.carlon25@gmail.com
@@ -42,33 +53,35 @@ files:
42
53
  - lib/metrognome.rb
43
54
  - lib/metrognome/railtie.rb
44
55
  - lib/metrognome/registrar.rb
45
- - lib/metrognome/runner.rb
46
56
  - lib/metrognome/scheduler.rb
47
57
  - lib/metrognome/version.rb
48
58
  - lib/tasks/metrognome.rake
49
59
  - metrognome.gemspec
60
+ - spec/metrognome/scheduler_spec.rb
61
+ - spec/spec_helper.rb
50
62
  homepage: https://github.com/cjc25/metrognome
51
63
  licenses: []
64
+ metadata: {}
52
65
  post_install_message:
53
66
  rdoc_options: []
54
67
  require_paths:
55
68
  - lib
56
69
  required_ruby_version: !ruby/object:Gem::Requirement
57
- none: false
58
70
  requirements:
59
71
  - - ! '>='
60
72
  - !ruby/object:Gem::Version
61
73
  version: '0'
62
74
  required_rubygems_version: !ruby/object:Gem::Requirement
63
- none: false
64
75
  requirements:
65
76
  - - ! '>='
66
77
  - !ruby/object:Gem::Version
67
78
  version: '0'
68
79
  requirements: []
69
80
  rubyforge_project:
70
- rubygems_version: 1.8.24
81
+ rubygems_version: 2.1.11
71
82
  signing_key:
72
- specification_version: 3
83
+ specification_version: 4
73
84
  summary: A simple repeating task scheduler for Rails applications
74
- test_files: []
85
+ test_files:
86
+ - spec/metrognome/scheduler_spec.rb
87
+ - spec/spec_helper.rb
@@ -1,55 +0,0 @@
1
- module Metrognome
2
- class Runner
3
- class << self
4
- # TODO pull out reaper_interval into config
5
- attr_accessor :running, :reaper_last
6
- REAPER_INTERVAL = 1
7
-
8
- def start
9
- setup
10
-
11
- loop do
12
- if @_sigterm
13
- running.each { |t| t.kill }
14
- reap
15
- break
16
- end
17
- reap if Time.now - reaper_last > REAPER_INTERVAL
18
-
19
- Metrognome::Registrar.registered.each do |sched|
20
- if Time.now - sched.last > sched.interval
21
- running << Thread.new do
22
- sched.call_task
23
-
24
- # DB connections will no longer be closed automatically, so
25
- # close it manually here
26
- ActiveRecord::Base.connection.close
27
- end
28
- end
29
- end
30
- # TODO GCD sleep instead of 1
31
- sleep 1
32
- end
33
-
34
- Metrognome::Registrar.registered.each { |s| File.delete s.lock }
35
- end
36
-
37
- private
38
- def setup
39
- require 'set'
40
- self.running = Set.new
41
- self.reaper_last = Time.now
42
-
43
- Signal.trap "TERM" do
44
- exit 1 if @_sigterm
45
- @_sigterm = 1
46
- end
47
- end
48
-
49
- def reap
50
- running.delete_if { |t| t.join(0) }
51
- self.reaper_last = Time.now
52
- end
53
- end
54
- end
55
- end