scheduled-robot 1.0.0
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 +7 -0
- data/.autotest +23 -0
- data/.gitignore +3 -0
- data/.rvmrc +48 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +15 -0
- data/LICENSE.md +7 -0
- data/Manifest.txt +14 -0
- data/README.md +56 -0
- data/Rakefile +9 -0
- data/changelog.md +33 -0
- data/lib/mutex/pid_mutex.rb +84 -0
- data/lib/robot.rb +73 -0
- data/lib/robot/decorator.rb +19 -0
- data/lib/robot/failsafe.rb +22 -0
- data/lib/robot/hoptoad.rb +24 -0
- data/lib/robot/logger.rb +32 -0
- data/lib/robot/synchronizer.rb +31 -0
- data/lib/robot/task.rb +15 -0
- data/lib/robot/version.rb +9 -0
- data/robot.gemspec +18 -0
- data/test/test_pid_mutex.rb +68 -0
- data/test/test_robot.rb +148 -0
- metadata +68 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 00600e2154beda443d4a9608c478d9436cbb6d43
|
4
|
+
data.tar.gz: 15a6d90c7e039b069265b9b50039e4f64c9d5f65
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6cff12eff3603b6d99b412080d4691a94c220b7b13380cda228bbb48c7388dc581c94de9cef9e6fe96bbcdd590c95c31133f60a23146ea0029e32330c25b5db3
|
7
|
+
data.tar.gz: fc810c8d4e38ae0469ebe8834ecd51e7bbeec2ff2af935633abe1bb74f3b0582aa16856e5e9f3c055dfd87ee855e4ac59fb91da4be859f4a871b796c7c62a615
|
data/.autotest
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'autotest/restart'
|
4
|
+
|
5
|
+
# Autotest.add_hook :initialize do |at|
|
6
|
+
# at.extra_files << "../some/external/dependency.rb"
|
7
|
+
#
|
8
|
+
# at.libs << ":../some/external"
|
9
|
+
#
|
10
|
+
# at.add_exception 'vendor'
|
11
|
+
#
|
12
|
+
# at.add_mapping(/dependency.rb/) do |f, _|
|
13
|
+
# at.files_matching(/test_.*rb$/)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# %w(TestA TestB).each do |klass|
|
17
|
+
# at.extra_class_map[klass] = "test/test_misc.rb"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
|
21
|
+
# Autotest.add_hook :run_command do |at|
|
22
|
+
# system "rake build"
|
23
|
+
# end
|
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
# This is an RVM Project .rvmrc file, used to automatically load the ruby
|
4
|
+
# development environment upon cd'ing into the directory
|
5
|
+
|
6
|
+
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
|
7
|
+
# Only full ruby name is supported here, for short names use:
|
8
|
+
# echo "rvm use 2.0.0" > .rvmrc
|
9
|
+
environment_id="ruby-2.0.0-p0@robot"
|
10
|
+
|
11
|
+
# Uncomment the following lines if you want to verify rvm version per project
|
12
|
+
rvmrc_rvm_version="1.17.8 (stable)" # 1.10.1 seams as a safe start
|
13
|
+
eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
|
14
|
+
echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
|
15
|
+
return 1
|
16
|
+
}
|
17
|
+
|
18
|
+
# First we attempt to load the desired environment directly from the environment
|
19
|
+
# file. This is very fast and efficient compared to running through the entire
|
20
|
+
# CLI and selector. If you want feedback on which environment was used then
|
21
|
+
# insert the word 'use' after --create as this triggers verbose mode.
|
22
|
+
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
|
23
|
+
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
|
24
|
+
then
|
25
|
+
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
|
26
|
+
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
|
27
|
+
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
|
28
|
+
else
|
29
|
+
# If the environment file has not yet been created, use the RVM CLI to select.
|
30
|
+
rvm --create "$environment_id" || {
|
31
|
+
echo "Failed to create RVM environment '${environment_id}'."
|
32
|
+
return 1
|
33
|
+
}
|
34
|
+
fi
|
35
|
+
|
36
|
+
# If you use bundler, this might be useful to you:
|
37
|
+
if [[ -s Gemfile ]] && {
|
38
|
+
! builtin command -v bundle >/dev/null ||
|
39
|
+
builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
|
40
|
+
}
|
41
|
+
then
|
42
|
+
printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
|
43
|
+
gem install bundler
|
44
|
+
fi
|
45
|
+
if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
|
46
|
+
then
|
47
|
+
bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
|
48
|
+
fi
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 IZEA
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sublicense copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
.autotest
|
2
|
+
changelog.md
|
3
|
+
Manifest.txt
|
4
|
+
README.md
|
5
|
+
Rakefile
|
6
|
+
lib/robot.rb
|
7
|
+
lib/robot/decorator.rb
|
8
|
+
lib/robot/failsafe.rb
|
9
|
+
lib/robot/hoptoad.rb
|
10
|
+
lib/robot/logger.rb
|
11
|
+
lib/robot/synchronizer.rb
|
12
|
+
lib/robot/task.rb
|
13
|
+
lib/robot/version.rb
|
14
|
+
test/test_robot.rb
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Robot
|
2
|
+
|
3
|
+
__DESCRIPTION:__
|
4
|
+
|
5
|
+
Robot does your bidding. Use it for good, not for evil.
|
6
|
+
|
7
|
+
__FEATURES/PROBLEMS:__
|
8
|
+
|
9
|
+
* Logs the start / end times of each task.
|
10
|
+
* Rescues exceptions so that if a task crashes, it won't take down the Robot
|
11
|
+
itself.
|
12
|
+
* Synchronizes tasks using a mutex (either a standard Ruby Mutex or something
|
13
|
+
that implements its interface). This ensures only one instance of Robot will
|
14
|
+
run at a time.
|
15
|
+
|
16
|
+
__SYNOPSIS:__
|
17
|
+
|
18
|
+
Add the following to an initializer (config/initializers/robot.rb):
|
19
|
+
|
20
|
+
# the following options can be set here and they wont need to be set in each definition below
|
21
|
+
#this sets default behavoir for robots. redefining these will overwrite the default
|
22
|
+
#set a log file
|
23
|
+
Robot.logger = Logger.new("log/robot.log")
|
24
|
+
#specifying a pid root will default all robots to create a pid at <PID_ROOT>/robot.<DEFINED_NAME>.pid
|
25
|
+
Robot.pid_root = Rails.root("tmp","pids")
|
26
|
+
#will default hoptoad in each robot
|
27
|
+
Robot.hoptoad = HoptoadNotifier
|
28
|
+
#this will default the failsafe ... it already defaults to true ... use this to default to false
|
29
|
+
Robot.failsafe = true
|
30
|
+
|
31
|
+
Robot.define(:hourly) do |robot|
|
32
|
+
# Omit this line if you don't want logging, or define it as a custom logger or it is defined globally
|
33
|
+
# if you want a separate log file for your Robot tasks.
|
34
|
+
robot.logger = Logger.new("log/robot.log")
|
35
|
+
|
36
|
+
# Omit this line if you don't want to synchronize tasks or it is defined globally. Adding a PIDMutex
|
37
|
+
# here will ensure that only 1 process is running this task at any time.
|
38
|
+
# Note that since PIDMutex uses a file as a locking mechanism, this method
|
39
|
+
# doesn't scale beyond a single filesystem.
|
40
|
+
robot.mutex = PIDMutex.new("tmp/pids/hourly.pid")
|
41
|
+
|
42
|
+
# Omit this line if you're not using Hoptoad or it is defined globally.
|
43
|
+
robot.hoptoad = HoptoadNotifier
|
44
|
+
|
45
|
+
# This is true by default, I'm just including it here as an example. If
|
46
|
+
# you set it to false, then there won't be a failsafe mechanism, meaning
|
47
|
+
# any exceptions raised by tasks will be unchecked. You'll usually want to
|
48
|
+
# leave this set to true.
|
49
|
+
robot.failsafe = true
|
50
|
+
|
51
|
+
robot.perform("Some long-running task") { LongTask.run }
|
52
|
+
end
|
53
|
+
|
54
|
+
And then add an entry to crontab to run your new Robot.hourly method:
|
55
|
+
|
56
|
+
@hourly script/runner -e production "Robot.run(:hourly)"
|
data/Rakefile
ADDED
data/changelog.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
__0.2.0 / 2012-02-07 Grady Griffin__
|
2
|
+
|
3
|
+
* added defaulting most robot options
|
4
|
+
|
5
|
+
* failsafe
|
6
|
+
* mutex
|
7
|
+
* logger
|
8
|
+
* hoptoad
|
9
|
+
|
10
|
+
|
11
|
+
__0.1.0 / 2009-08-27 Dray Lacy__
|
12
|
+
|
13
|
+
* 5 minor enhancements
|
14
|
+
|
15
|
+
* Changed the way tasks are run. Rather than becoming methods on the Robot
|
16
|
+
module (now a class), they are invoked using Robot.run(:task_name). So
|
17
|
+
before, where you would call Robot.daily, you should now call
|
18
|
+
Robot.run(:daily).
|
19
|
+
* Moved the logger, mutex, hoptoad, etc. config from class-level to
|
20
|
+
task-level, so you can define different loggers and mutexes for each task.
|
21
|
+
* Changed the failsafe decorator so that it writes to $stderr instead of
|
22
|
+
STDERR.
|
23
|
+
* Also changed the failsafe decorator so that it only writes errors to
|
24
|
+
$stderr if $VERBOSE is set to true (ie. if ruby is being run in strict, or
|
25
|
+
"-w", mode).
|
26
|
+
* Added tests.
|
27
|
+
|
28
|
+
__0.0.1 / 2009-08-25 Dray Lacy__
|
29
|
+
|
30
|
+
* 1 major enhancement
|
31
|
+
|
32
|
+
* Birthday!
|
33
|
+
* Extracted from SocialSpark
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# PIDMutex implements a simple semaphore that can be used to coordinate access
|
2
|
+
# to shared data from multiple concurrent processes. It does this by managing
|
3
|
+
# access to a PID file.
|
4
|
+
#
|
5
|
+
# Generally, this class is meant to be as close as possible to Ruby's standard
|
6
|
+
# Mutex class, so it can be easily swapped in its place for switching between
|
7
|
+
# thread- and process-based concurrency.
|
8
|
+
class PIDMutex
|
9
|
+
# Amount of time (in seconds) to sleep while waiting for a lock to become
|
10
|
+
# available.
|
11
|
+
PERIOD = 0.005
|
12
|
+
|
13
|
+
# Creates a new PIDMutex using the given +pid_file+. The +pid_file+ is
|
14
|
+
# expected to be a fully resolved path, but a file need not exist there
|
15
|
+
# (PIDMutex will automatically create it).
|
16
|
+
def initialize(pid_file)
|
17
|
+
@pid_file = pid_file
|
18
|
+
|
19
|
+
# Define a finalizer so that when this object is destroyed, it
|
20
|
+
# automatically unlocks the PID file.
|
21
|
+
ObjectSpace.define_finalizer(self, method(:unlock))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Full path to the PID file.
|
25
|
+
attr_reader :pid_file
|
26
|
+
|
27
|
+
# Returns true if this lock is currently held by some process.
|
28
|
+
def locked?
|
29
|
+
return false unless File.exists?(@pid_file)
|
30
|
+
|
31
|
+
begin
|
32
|
+
pid = File.read(@pid_file)
|
33
|
+
|
34
|
+
# if there is nothing in the file, delete the file and return false
|
35
|
+
if pid.to_s.length == 0
|
36
|
+
unlink_pid_file and return false
|
37
|
+
end
|
38
|
+
|
39
|
+
Process.kill 0, pid.to_i # this will raise Errno::ESRCH if there is a process
|
40
|
+
|
41
|
+
true
|
42
|
+
rescue Errno::ESRCH => e
|
43
|
+
# no process, lets delete the pid and return false
|
44
|
+
unlink_pid_file and return false
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
# Obtains a lock, runs the block, and releases the lock when the block
|
50
|
+
# completes.
|
51
|
+
def synchronize # :yields:
|
52
|
+
lock
|
53
|
+
yield
|
54
|
+
ensure
|
55
|
+
unlock
|
56
|
+
end
|
57
|
+
|
58
|
+
# Attempts to grab the lock and waits if it isn‘t available.
|
59
|
+
def lock
|
60
|
+
sleep(PERIOD) until try_lock
|
61
|
+
end
|
62
|
+
|
63
|
+
# Attempts to obtain the lock and returns immediately. Returns true if the
|
64
|
+
# lock was granted.
|
65
|
+
def try_lock
|
66
|
+
if locked?
|
67
|
+
false
|
68
|
+
else
|
69
|
+
File.open(@pid_file, 'w') { |f| f.write(Process.pid) }
|
70
|
+
File.chmod(0644, @pid_file)
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Removes the file (if any) created by #write_pid_file.
|
76
|
+
def unlock
|
77
|
+
return unless locked?
|
78
|
+
unlink_pid_file
|
79
|
+
end
|
80
|
+
|
81
|
+
def unlink_pid_file
|
82
|
+
File.unlink(@pid_file)
|
83
|
+
end
|
84
|
+
end
|
data/lib/robot.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'robot/version'
|
2
|
+
require 'pathname'
|
3
|
+
require 'mutex/pid_mutex'
|
4
|
+
|
5
|
+
class Robot
|
6
|
+
autoload :Decorator, 'robot/decorator'
|
7
|
+
autoload :Failsafe, 'robot/failsafe'
|
8
|
+
autoload :Hoptoad, 'robot/hoptoad'
|
9
|
+
autoload :Logger, 'robot/logger'
|
10
|
+
autoload :Synchronizer, 'robot/synchronizer'
|
11
|
+
autoload :Task, 'robot/task'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :robots
|
15
|
+
attr_accessor :logger
|
16
|
+
attr_accessor :pid_root
|
17
|
+
attr_accessor :hoptoad
|
18
|
+
attr_accessor :failsafe
|
19
|
+
end
|
20
|
+
|
21
|
+
self.robots = {}
|
22
|
+
|
23
|
+
attr_reader :name
|
24
|
+
|
25
|
+
attr_accessor :logger
|
26
|
+
attr_accessor :mutex
|
27
|
+
attr_accessor :hoptoad
|
28
|
+
attr_accessor :failsafe
|
29
|
+
|
30
|
+
def log_info(msg)
|
31
|
+
@logger.info(msg) if @logger
|
32
|
+
end
|
33
|
+
|
34
|
+
def perform(name, &block)
|
35
|
+
@blocks << [name, block]
|
36
|
+
end
|
37
|
+
|
38
|
+
def run
|
39
|
+
@blocks.each do |(name, block)|
|
40
|
+
task = Task.new(name)
|
41
|
+
task = Synchronizer.new(task, @mutex) if @mutex
|
42
|
+
task = Logger.new(task, @logger) if @logger
|
43
|
+
task = Hoptoad.new(task, @hoptoad) if @hoptoad
|
44
|
+
task = Failsafe.new(task) if @failsafe
|
45
|
+
task.call(&block)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.define(name)
|
50
|
+
instance = new(name)
|
51
|
+
yield(instance)
|
52
|
+
self.robots[instance.name] = instance
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.robot_by_name(name)
|
56
|
+
self.robots[name] || raise(ArgumentError, "No such robot: #{name}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.run(name)
|
60
|
+
robot_by_name(name).run
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def initialize(name)
|
66
|
+
@name = name
|
67
|
+
@failsafe = self.class.failsafe.nil? ? true : self.class.failsafe
|
68
|
+
@logger = self.class.logger if self.class.logger
|
69
|
+
@hoptoad = self.class.hoptoad if self.class.hoptoad
|
70
|
+
@mutex = PIDMutex.new(self.class.pid_root.join("robot.#{name}.pid")) if self.class.pid_root
|
71
|
+
@blocks = []
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Robot
|
2
|
+
# Task decorator which rescues any errors raised by the task.
|
3
|
+
class Failsafe < Decorator
|
4
|
+
# Runs the decorated task, rescuing any exceptions that it raises.
|
5
|
+
def call
|
6
|
+
super
|
7
|
+
rescue Exception => exception
|
8
|
+
# Eat any exceptions, so that other tasks can run.
|
9
|
+
handle_exception(exception)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def handle_exception(exception) # :nodoc:
|
15
|
+
return unless $VERBOSE
|
16
|
+
|
17
|
+
message = "/!\\ FAILSAFE /!\\ #{Time.now}\n"
|
18
|
+
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
|
19
|
+
$stderr.puts(message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Robot
|
2
|
+
# Task decorator which reports any errors to Hoptoad.
|
3
|
+
class Hoptoad < Decorator
|
4
|
+
# Runs the decorated task, reporting any exceptions that it raises. Note
|
5
|
+
# that the exceptions will be re-raised.
|
6
|
+
def call
|
7
|
+
super
|
8
|
+
rescue Exception => exception
|
9
|
+
handle_exception(exception)
|
10
|
+
raise
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize(task, notifier)
|
16
|
+
super(task)
|
17
|
+
@notifier = notifier
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_exception(exception) # :nodoc:
|
21
|
+
@notifier.notify(exception)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/robot/logger.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
class Robot
|
4
|
+
# Task decorator which adds logging and benchmarking.
|
5
|
+
class Logger < Decorator
|
6
|
+
attr_accessor :logger
|
7
|
+
|
8
|
+
# Log the running time of the decorated task, as well as any exceptions
|
9
|
+
# that are raised.
|
10
|
+
def call
|
11
|
+
@logger.info("Beginning #{name}")
|
12
|
+
@logger.info('Completed %s (%.5fs)' % [name, Benchmark.realtime { super }])
|
13
|
+
rescue Exception => exception
|
14
|
+
log_exception(exception)
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def initialize(task, logger)
|
21
|
+
super(task)
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def log_exception(exception)
|
26
|
+
message = "/!\\ ERROR /!\\ #{Time.now}\n"
|
27
|
+
message << " During task #{name.inspect}\n"
|
28
|
+
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
|
29
|
+
@logger.fatal(message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Robot
|
2
|
+
# Task decorator which wraps the task inside a mutex lock.
|
3
|
+
class Synchronizer < Decorator
|
4
|
+
class SynchronizationError < StandardError # :nodoc:
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_accessor :mutex
|
8
|
+
|
9
|
+
# Attempts to obtain a lock on the mutex before delegating to the task's
|
10
|
+
# #call method. If this method is unable to obtain a lock, it will raise
|
11
|
+
# SynchronizationError.
|
12
|
+
def call
|
13
|
+
if @mutex.try_lock
|
14
|
+
begin
|
15
|
+
super
|
16
|
+
ensure
|
17
|
+
@mutex.unlock
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise SynchronizationError, "Task #{name.inspect} is already running"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def initialize(task, mutex)
|
27
|
+
super(task)
|
28
|
+
@mutex = mutex
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/robot/task.rb
ADDED
data/robot.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "robot/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "scheduled-robot"
|
7
|
+
s.version = Robot::VERSION::STRING
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Dray Lacy", "Grady Griffin", "Brian Fisher"]
|
10
|
+
s.email = "opensource@izea.com"
|
11
|
+
s.homepage = "https://github.com/IZEA/robot"
|
12
|
+
s.summary = %q{Use Robot to run scheduled jobs}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
class TestPIDMutex < Test::Unit::TestCase
|
4
|
+
PID_FILE = "tmp/pidmutex.pid"
|
5
|
+
def setup
|
6
|
+
@mutex = PIDMutex.new(PID_FILE)
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
# Just in case a test doesn't clean up after itself.
|
11
|
+
File.unlink(PID_FILE) if File.exists?(PID_FILE)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_no_file
|
15
|
+
assert !File.exists?(PID_FILE)
|
16
|
+
assert !@mutex.locked?
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_file_with_no_corresponding_process_should_unlink_file
|
20
|
+
File.open(PID_FILE, 'w') {|f| f.write('111111') }
|
21
|
+
assert !@mutex.locked?
|
22
|
+
assert !File.exists?(PID_FILE), "Checking locked mutex didn't remove erroneous file."
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_empty_file_should_unlink_file
|
26
|
+
File.open(PID_FILE, 'w') {|f| f.write('') }
|
27
|
+
assert !@mutex.locked?
|
28
|
+
assert !File.exists?(PID_FILE), "Checking locked mutex didn't remove erroneous file."
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_file_with_current_pid_should_not_unlink_file
|
32
|
+
File.open(PID_FILE, 'w') {|f| f.write(Process.pid.to_s) }
|
33
|
+
assert @mutex.locked?
|
34
|
+
assert File.exists?(PID_FILE), "PID file didn't exist."
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_locking_mutex_should_create_file
|
38
|
+
assert !File.exists?(PID_FILE), "PID file already exists."
|
39
|
+
@mutex.lock
|
40
|
+
assert File.exists?(PID_FILE), "PID file was not created."
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_locking_mutex_should_update_locked_status
|
44
|
+
assert !@mutex.locked?
|
45
|
+
@mutex.lock
|
46
|
+
assert @mutex.locked?
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_synchronize_should_run_given_block
|
50
|
+
block_was_run = false
|
51
|
+
@mutex.synchronize { block_was_run = true }
|
52
|
+
assert block_was_run
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_unlock_should_deleted_file
|
56
|
+
@mutex.lock
|
57
|
+
assert File.exists?(PID_FILE)
|
58
|
+
@mutex.unlock
|
59
|
+
assert !File.exists?(PID_FILE), "Unlock didn't remove PID file."
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_unlock_should_update_locked_status
|
63
|
+
@mutex.lock
|
64
|
+
assert @mutex.locked?
|
65
|
+
@mutex.unlock
|
66
|
+
assert !@mutex.locked?
|
67
|
+
end
|
68
|
+
end
|
data/test/test_robot.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'robot'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestRobot < Test::Unit::TestCase
|
5
|
+
class TestError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class Mutex
|
9
|
+
def try_lock
|
10
|
+
@allow
|
11
|
+
end
|
12
|
+
|
13
|
+
def unlock
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def initialize(allow)
|
19
|
+
@allow = allow
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Logger
|
24
|
+
attr_reader :infos
|
25
|
+
attr_reader :fatals
|
26
|
+
|
27
|
+
def info(msg)
|
28
|
+
@infos << msg
|
29
|
+
end
|
30
|
+
|
31
|
+
def fatal(msg)
|
32
|
+
@fatals << msg
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@infos = []
|
39
|
+
@fatals = []
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_run
|
44
|
+
block_was_run = false
|
45
|
+
robot = Robot.new(:test)
|
46
|
+
robot.perform('test') { block_was_run = true }
|
47
|
+
|
48
|
+
assert ! block_was_run
|
49
|
+
robot.run
|
50
|
+
assert block_was_run
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_failsafe
|
54
|
+
robot = Robot.new(:test)
|
55
|
+
robot.perform('test') { raise(TestError, "KABOOM") }
|
56
|
+
|
57
|
+
silence_warnings { assert_nothing_raised { robot.run } }
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_failsafe_class_default
|
61
|
+
Robot.failsafe = true
|
62
|
+
robot = Robot.new(:test)
|
63
|
+
robot.perform('test') { raise(TestError, "KABOOM") }
|
64
|
+
|
65
|
+
silence_warnings { assert_nothing_raised { robot.run } }
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_failsafe_disabled
|
69
|
+
robot = Robot.new(:test)
|
70
|
+
robot.failsafe = false
|
71
|
+
robot.perform('fail') { raise(TestError, "KABOOM") }
|
72
|
+
|
73
|
+
assert_raises(TestError) { robot.run }
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_failsafe_disabled_class_default
|
77
|
+
Robot.failsafe = false
|
78
|
+
robot = Robot.new(:test)
|
79
|
+
robot.perform('fail') { raise(TestError, "KABOOM") }
|
80
|
+
|
81
|
+
assert_raises(TestError) { robot.run }
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_logger
|
85
|
+
logger = Logger.new
|
86
|
+
robot = Robot.new(:test)
|
87
|
+
robot.logger = logger
|
88
|
+
robot.perform('test') {}
|
89
|
+
|
90
|
+
assert logger.infos.empty?
|
91
|
+
robot.run
|
92
|
+
assert_equal 2, logger.infos.size
|
93
|
+
assert_equal 'Beginning test', logger.infos.first
|
94
|
+
assert_match /Completed test \(\d+\.\d+s\)/, logger.infos.last
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_logger_class_default
|
98
|
+
Robot.logger = Logger.new
|
99
|
+
robot = Robot.new(:test)
|
100
|
+
robot.perform('test') {}
|
101
|
+
logger = robot.logger
|
102
|
+
|
103
|
+
assert logger.infos.empty?
|
104
|
+
robot.run
|
105
|
+
assert_equal 2, logger.infos.size
|
106
|
+
assert_equal 'Beginning test', logger.infos.first
|
107
|
+
assert_match /Completed test \(\d+\.\d+s\)/, logger.infos.last
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_mutex
|
111
|
+
mutex = Mutex.new(true)
|
112
|
+
robot = Robot.new(:test)
|
113
|
+
robot.mutex = mutex
|
114
|
+
robot.perform('test') {}
|
115
|
+
|
116
|
+
robot.run
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_mutex_synchronize_error
|
120
|
+
mutex = Mutex.new(false)
|
121
|
+
robot = Robot.new(:test)
|
122
|
+
robot.mutex = mutex
|
123
|
+
robot.failsafe = false
|
124
|
+
robot.perform('perform') {}
|
125
|
+
|
126
|
+
exception = assert_raises(Robot::Synchronizer::SynchronizationError) { robot.run }
|
127
|
+
assert_equal 'Task "perform" is already running', exception.message
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_pid_class_default
|
131
|
+
Robot.pid_root = Pathname.pwd.join('tmp')
|
132
|
+
robot = Robot.new(:test)
|
133
|
+
robot.failsafe = false
|
134
|
+
robot.perform('perform') {}
|
135
|
+
assert_equal true,!!robot.mutex
|
136
|
+
assert_equal robot.mutex.class,PIDMutex
|
137
|
+
assert_equal robot.mutex.pid_file.to_s, "#{Robot.pid_root}/robot.test.pid"
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def silence_warnings
|
143
|
+
old_verbose, $VERBOSE = $VERBOSE, nil
|
144
|
+
yield
|
145
|
+
ensure
|
146
|
+
$VERBOSE = old_verbose
|
147
|
+
end
|
148
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scheduled-robot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dray Lacy
|
8
|
+
- Grady Griffin
|
9
|
+
- Brian Fisher
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-05-23 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
15
|
+
description:
|
16
|
+
email: opensource@izea.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .autotest
|
22
|
+
- .gitignore
|
23
|
+
- .rvmrc
|
24
|
+
- Gemfile
|
25
|
+
- Gemfile.lock
|
26
|
+
- LICENSE.md
|
27
|
+
- Manifest.txt
|
28
|
+
- README.md
|
29
|
+
- Rakefile
|
30
|
+
- changelog.md
|
31
|
+
- lib/mutex/pid_mutex.rb
|
32
|
+
- lib/robot.rb
|
33
|
+
- lib/robot/decorator.rb
|
34
|
+
- lib/robot/failsafe.rb
|
35
|
+
- lib/robot/hoptoad.rb
|
36
|
+
- lib/robot/logger.rb
|
37
|
+
- lib/robot/synchronizer.rb
|
38
|
+
- lib/robot/task.rb
|
39
|
+
- lib/robot/version.rb
|
40
|
+
- robot.gemspec
|
41
|
+
- test/test_pid_mutex.rb
|
42
|
+
- test/test_robot.rb
|
43
|
+
homepage: https://github.com/IZEA/robot
|
44
|
+
licenses: []
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.0.0
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Use Robot to run scheduled jobs
|
66
|
+
test_files:
|
67
|
+
- test/test_pid_mutex.rb
|
68
|
+
- test/test_robot.rb
|