collective 0.2.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.
Files changed (61) hide show
  1. data/.autotest +13 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +16 -0
  5. data/Guardfile +6 -0
  6. data/README +7 -0
  7. data/Rakefile +11 -0
  8. data/bin/collective +37 -0
  9. data/collective.gemspec +23 -0
  10. data/demo/demo +36 -0
  11. data/demo/demo.rb +30 -0
  12. data/demo/demo3 +36 -0
  13. data/demo/job1.rb +31 -0
  14. data/demo/job2.rb +42 -0
  15. data/demo/job3.rb +44 -0
  16. data/demo/populate.rb +22 -0
  17. data/lib/collective.rb +52 -0
  18. data/lib/collective/checker.rb +51 -0
  19. data/lib/collective/configuration.rb +219 -0
  20. data/lib/collective/idler.rb +81 -0
  21. data/lib/collective/key.rb +48 -0
  22. data/lib/collective/lifecycle_observer.rb +25 -0
  23. data/lib/collective/log.rb +29 -0
  24. data/lib/collective/messager.rb +218 -0
  25. data/lib/collective/mocks/storage.rb +108 -0
  26. data/lib/collective/monitor.rb +58 -0
  27. data/lib/collective/policy.rb +60 -0
  28. data/lib/collective/pool.rb +180 -0
  29. data/lib/collective/redis/storage.rb +142 -0
  30. data/lib/collective/registry.rb +123 -0
  31. data/lib/collective/squiggly.rb +20 -0
  32. data/lib/collective/utilities/airbrake_observer.rb +26 -0
  33. data/lib/collective/utilities/hoptoad_observer.rb +26 -0
  34. data/lib/collective/utilities/log_observer.rb +40 -0
  35. data/lib/collective/utilities/observeable.rb +18 -0
  36. data/lib/collective/utilities/observer_base.rb +59 -0
  37. data/lib/collective/utilities/process.rb +82 -0
  38. data/lib/collective/utilities/signal_hook.rb +47 -0
  39. data/lib/collective/utilities/storage_base.rb +41 -0
  40. data/lib/collective/version.rb +3 -0
  41. data/lib/collective/worker.rb +161 -0
  42. data/spec/checker_spec.rb +20 -0
  43. data/spec/configuration_spec.rb +24 -0
  44. data/spec/helper.rb +33 -0
  45. data/spec/idler_spec.rb +58 -0
  46. data/spec/key_spec.rb +41 -0
  47. data/spec/messager_spec.rb +131 -0
  48. data/spec/mocks/storage_spec.rb +108 -0
  49. data/spec/monitor_spec.rb +15 -0
  50. data/spec/policy_spec.rb +43 -0
  51. data/spec/pool_spec.rb +119 -0
  52. data/spec/redis/storage_spec.rb +133 -0
  53. data/spec/registry_spec.rb +52 -0
  54. data/spec/support/jobs.rb +58 -0
  55. data/spec/support/redis.rb +22 -0
  56. data/spec/support/timing.rb +32 -0
  57. data/spec/utilities/observer_base_spec.rb +50 -0
  58. data/spec/utilities/process_spec.rb +17 -0
  59. data/spec/worker_spec.rb +121 -0
  60. data/unused/times.rb +45 -0
  61. metadata +148 -0
@@ -0,0 +1,13 @@
1
+ # (this file is loaded automatically by autotest)
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ # Bundler support
5
+ # @see https://www.relishapp.com/rspec/rspec-core/docs/autotest
6
+ require "autotest/bundler"
7
+
8
+ # ... add the following line after all other requires in your ~/.autotest:
9
+ begin
10
+ require "autotest/fsevent"
11
+ rescue LoadError => x
12
+ raise x if RUBY_PLATFORM =~ /darwin/
13
+ end
@@ -0,0 +1,5 @@
1
+ *.log
2
+ *.pid
3
+ .bundle
4
+ coverage
5
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in collective.gemspec
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem "guard-rspec"
8
+ gem "rb-fsevent" # for guard
9
+ gem "growl_notify" # for guard
10
+ gem "rspec"
11
+ gem "ruby-debug19", require: false
12
+ end
13
+
14
+ group :test do
15
+ gem "simplecov", require: false
16
+ end
@@ -0,0 +1,6 @@
1
+ guard "rspec" do
2
+ watch(%r{^lib/collective\.rb$}) { "spec" }
3
+ watch(%r{^lib/collective/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch("spec/helper.rb") { "spec" }
6
+ end
data/README ADDED
@@ -0,0 +1,7 @@
1
+ Collective helps to run pools of workers.
2
+
3
+ A pool contains several workers.
4
+
5
+ Workers are processes which run jobs.
6
+
7
+ Jobs are simple tasks.
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ # the minitest way
5
+ # task :test do
6
+ # require_relative "./spec/helper"
7
+ # FileList["spec/**/*_{spec}.rb"].each { |f| load(f) }
8
+ # end
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+ task :default => :spec
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ =begin
4
+
5
+ Usage:
6
+ collective [options] [check] | monitor | restart | status | stop
7
+
8
+ e.g.
9
+ collective --dry-run --verbose --env production --name it --chdir /tmp/it check
10
+ collective /opt/rails/it/current/config/it monitor
11
+ Options must preceed the command.
12
+
13
+ Note: the pid file depends on chdir, env, and name, so you have to specify them for any command to work.
14
+ They can be specified in a configuration file.
15
+
16
+ =end
17
+
18
+ # We don't use Bundler because that is up to the client.
19
+ # The client can use 'bundle exec collective' if desired.
20
+ File.expand_path(File.dirname(__FILE__)+"/../lib").tap { |d| $:.unshift(d) if ! $:.member?(d) }
21
+ require "collective"
22
+
23
+ my = Collective::Configuration.parse(ARGV)
24
+
25
+ # TODO use my.options_for_daemon_spawn
26
+ # TODO use my.args_for_daemon_spawn
27
+
28
+ if ! my.dry_run then
29
+ case ARGV.first
30
+ when "stop"
31
+ Collective::Monitor.new(my).stop_all
32
+ when "monitor"
33
+ Collective::Monitor.new(my).monitor
34
+ else
35
+ Kernel.abort "Unknown command #{ARGV.first}"
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ File.expand_path("../lib", __FILE__).tap { |p| $:.push(p) unless $:.member?(p) }
3
+
4
+ require "collective/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "collective"
8
+ s.version = Collective::VERSION
9
+ s.authors = ["Mark Lanett"]
10
+ s.email = ["mark.lanett@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Manage a collection of worker processes}
13
+
14
+ s.rubyforge_project = "collective"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency "redis"
22
+ s.add_dependency "redis-namespace"
23
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby bin/collective
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ # This file is evaluated in the context of a Collective::Configuration instance.
5
+ # DSL attributes include: env, set_env, name, set_name (best to set these via command options)
6
+ # DSL methods include: chdir, add_path, set_defaults, add_pool, after_fork.
7
+
8
+ add_path File.dirname(__FILE__)
9
+ require "job1"
10
+
11
+ # best to pass these on the command line
12
+ # set_env "development" if ! env
13
+ # set_name "demo"
14
+ # chdir "/tmp/demo"
15
+
16
+ set_defaults observers: [ :log ] unless env == "test" # No noise when testing please.
17
+
18
+ set_defaults(
19
+ pool_min_workers: 1,
20
+ worker_late: 10,
21
+ worker_hung: 100,
22
+ batchsize: 100,
23
+ worker_max_lifetime: 3600,
24
+ observers: [],
25
+ storage: :redis
26
+ )
27
+
28
+ add_pool Job1,
29
+ worker_late: 60,
30
+ pool_min_workers: 1,
31
+ pool_max_workers: 10
32
+
33
+ after_fork do
34
+ # reset ActiveRecord and other pools
35
+ puts "Forked!"
36
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # This file is evaluated in the context of a Collective::Configuration instance.
4
+ # DSL attributes include: env, set_env, name, set_name (best to set these via command options)
5
+ # DSL methods include: chdir, add_path, set_defaults, add_pool, after_fork.
6
+
7
+ add_path File.dirname(__FILE__)
8
+ require "job1"
9
+
10
+ set_env "development" if ! env
11
+ set_name "demo"
12
+ chdir "/tmp/demo"
13
+
14
+ set_defaults(
15
+ pool_min_workers: 1,
16
+ worker_late: 10,
17
+ worker_hung: 100,
18
+ batchsize: 100,
19
+ worker_max_lifetime: 3600
20
+ )
21
+
22
+ add_pool Job1,
23
+ worker_late: 60,
24
+ pool_min_workers: 1,
25
+ pool_max_workers: 10
26
+
27
+ after_fork do
28
+ # reset ActiveRecord and other pools
29
+ puts "Forked!"
30
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby bin/collective
2
+ # -*- encoding: utf-8 -*-
3
+
4
+ # This file is evaluated in the context of a Collective::Configuration instance.
5
+ # DSL attributes include: env, set_env, name, set_name (best to set these via command options)
6
+ # DSL methods include: chdir, add_path, set_defaults, add_pool, after_fork.
7
+
8
+ add_path File.dirname(__FILE__)
9
+ require "job3"
10
+
11
+ # best to pass these on the command line
12
+ # set_env "development" if ! env
13
+ # set_name "demo"
14
+ # chdir "/tmp/demo"
15
+
16
+ set_defaults observers: [ :log ] unless env == "test" # No noise when testing please.
17
+
18
+ set_defaults(
19
+ pool_min_workers: 3,
20
+ worker_late: 10,
21
+ worker_hung: 100,
22
+ batchsize: 100,
23
+ worker_max_lifetime: 3600,
24
+ observers: [],
25
+ storage: :redis
26
+ )
27
+
28
+ add_pool Job3,
29
+ worker_late: 60,
30
+ pool_min_workers: 10,
31
+ pool_max_workers: 20
32
+
33
+ after_fork do
34
+ # reset ActiveRecord and other pools
35
+ puts "Forked!"
36
+ end
@@ -0,0 +1,31 @@
1
+ require "collective"
2
+
3
+ =begin
4
+
5
+ Mostly does nothing.
6
+ Otherwise runs in 1-10 seconds.
7
+
8
+ =end
9
+
10
+ class Job1
11
+
12
+ def initialize( options = {} )
13
+ end
14
+
15
+ include Collective::Log
16
+
17
+ def call(context)
18
+ p = rand
19
+
20
+ case
21
+ when p < 0.7 # 10%
22
+ log "No action"
23
+ false
24
+ else
25
+ log "Something"
26
+ sleep(rand(10))
27
+ true
28
+ end
29
+ end
30
+
31
+ end # Job1
@@ -0,0 +1,42 @@
1
+ require "collective"
2
+
3
+ =begin
4
+
5
+ Sometimes exits, hangs, or aborts.
6
+ Otherwise does something.
7
+
8
+ =end
9
+
10
+ class Job2
11
+
12
+ def initialize( options = {} )
13
+ end
14
+
15
+ include Collective::Log
16
+
17
+ def call(context)
18
+ p = rand
19
+
20
+ case
21
+ when p < 0.1 # 10%
22
+ log "Going to exit"
23
+ exit
24
+ when p < 0.2 # 10%
25
+ log "Hang"
26
+ loop do
27
+ true
28
+ end
29
+ when p < 0.3 # 10%
30
+ log "Going to abort"
31
+ abort!
32
+ when p < 0.9 # 60%
33
+ log "Nothing to do"
34
+ false
35
+ else # 10%
36
+ log "Doing something..."
37
+ sleep(rand(10))
38
+ true
39
+ end
40
+ end
41
+
42
+ end # Job2
@@ -0,0 +1,44 @@
1
+ require "collective"
2
+ require "collective/checker"
3
+
4
+ =begin
5
+
6
+ Sometimes does something but is slow.
7
+
8
+ =end
9
+
10
+ class Job3
11
+
12
+ attr :redis
13
+ attr :storage
14
+
15
+ def initialize( options = {} )
16
+ @redis = Redis.connect url: "redis://127.0.0.1:6379/0"
17
+ @storage = Collective::Redis::Storage.new(redis)
18
+ end
19
+
20
+ include Collective::Log
21
+
22
+ def call(context)
23
+ page = storage.queue_pop( "Next" )
24
+ activity_count = storage.map_get "Activity", page
25
+ last_time = storage.map_get( "Last", page )
26
+ start = Time.now
27
+ checker = Checker.new activity_count, last_time, start
28
+ begin
29
+ return if ! checker.check? # executes ensure block
30
+
31
+ log "Processing #{page} with activity #{checker.activity_count}; estimated delay #{checker.estimated_delay} sec"
32
+
33
+ sleep(checker.estimated_delay)
34
+ # checker.checked(rand)
35
+
36
+ log "Processed #{page}; updated activity count to #{checker.activity_count}; estimated next time #{checker.next_time}"
37
+
38
+ storage.map_set "Last", page, start.to_i
39
+ ensure
40
+ storage.queue_add( "Next", page, checker.next_time )
41
+ end
42
+ end
43
+
44
+ end # Job3
@@ -0,0 +1,22 @@
1
+ File.expand_path(File.dirname(__FILE__)+"/../lib").tap { |d| $: << d unless $:.member?(d) }
2
+ require "collective"
3
+ require "collective/squiggly"
4
+ require "redis"
5
+
6
+ redis = Redis.connect url: "redis://127.0.0.1:6379/0"
7
+ storage = Collective::Redis::Storage.new(redis)
8
+
9
+ storage.del "Names"
10
+ storage.del "Activity"
11
+ storage.del "Weight"
12
+ storage.del "Next"
13
+
14
+ (1..1000).each do |i|
15
+ name = Squiggly.subject
16
+ activity = rand(1000)
17
+
18
+ storage.map_set "Names", "Page-#{i}", name
19
+ storage.map_set "Activity", "Page-#{i}", activity
20
+
21
+ storage.queue_add( "Next", "Page-#{i}", 0 )
22
+ end
@@ -0,0 +1,52 @@
1
+ File.expand_path(File.dirname(__FILE__)).tap { |d| $: << d unless $:.member?(d) }
2
+
3
+ require "collective/version"
4
+
5
+ module Collective
6
+ autoload :Configuration, "collective/configuration"
7
+ autoload :Idler, "collective/idler"
8
+ autoload :Key, "collective/key"
9
+ autoload :LifecycleObserver,"collective/lifecycle_observer"
10
+ autoload :Log, "collective/log"
11
+ autoload :Messager, "collective/messager"
12
+ autoload :Monitor, "collective/monitor"
13
+ autoload :Policy, "collective/policy"
14
+ autoload :Pool, "collective/pool"
15
+ autoload :Registry, "collective/registry"
16
+ autoload :Worker, "collective/worker"
17
+ end
18
+
19
+ module Collective::Mocks
20
+ autoload :Storage, "collective/mocks/storage"
21
+ end
22
+
23
+ module Collective::Utilities
24
+ autoload :AirbrakeObserver, "collective/utilities/airbrake_observer"
25
+ autoload :HoptoadObserver, "collective/utilities/hoptoad_observer"
26
+ autoload :LogObserver, "collective/utilities/log_observer"
27
+ autoload :NullObserver, "collective/utilities/null_observer"
28
+ autoload :Observeable, "collective/utilities/observeable"
29
+ autoload :ObserverBase, "collective/utilities/observer_base"
30
+ autoload :Process, "collective/utilities/process"
31
+ autoload :SignalHook, "collective/utilities/signal_hook"
32
+ autoload :StorageBase, "collective/utilities/storage_base"
33
+ end
34
+
35
+ module Collective::Redis
36
+ autoload :Storage, "collective/redis/storage"
37
+ end
38
+
39
+ module Collective
40
+ class << self
41
+
42
+ # @param classname
43
+ # @returns class object
44
+ def resolve_class(classname)
45
+ classname.split(/::/).inject(Object) { |a,i| a.const_get(i) }
46
+ end
47
+
48
+ end # class
49
+ end
50
+
51
+ class Collective::ConfigurationError < Exception
52
+ end
@@ -0,0 +1,51 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class Checker
4
+
5
+ DAY = 86400
6
+
7
+ # @param activity_count is a daily level of activities
8
+ # @param last_time is the last time we checked
9
+ def initialize( activity_count, last_time, check_time = Time.now )
10
+ @activity_count = activity_count.to_i # e.g. 192 actions
11
+ @last_time = Time.at last_time.to_i # e.g. 1 hour ago
12
+ @check_time = check_time # e.g. now
13
+
14
+ @last_time = [ @check_time - DAY, @last_time ].max # reject huge intervals like since epoch
15
+ @interval = @check_time - @last_time # e.g. 1 hour
16
+
17
+ # estimations
18
+ variation = 2 * ( rand + rand + rand + rand ) / 4 # ~1.0 more or less
19
+ day_est = variation * @activity_count # e.g. ~192
20
+ @estimate = ( day_est * @interval / DAY ).to_i # e.g. 8 (per hour)
21
+ next_interval = @estimate > 0 ? @interval / @estimate : @interval * 2
22
+ end
23
+
24
+ def estimate
25
+ @estimate
26
+ end
27
+
28
+ def checked( actual )
29
+ @activity_count = ( actual * DAY / @interval ).to_i # e.g. 12
30
+ next_interval = actual > 0 ? @interval / actual : @interval * 2 # e.g. 300 seconds (3600 / 12)
31
+ @next_time = @check_time + next_interval # e.g. now + 300 seconds
32
+ end
33
+
34
+ # refuse to check too often
35
+ def check?
36
+ @last_time < @check_time - 100
37
+ end
38
+
39
+ def activity_count
40
+ @activity_count
41
+ end
42
+
43
+ def estimated_delay
44
+ @estimate > 0 ? Math.sqrt(@estimate).to_i : 1
45
+ end
46
+
47
+ def next_time
48
+ # If not determined yet, calculate a safety time past the Facebook api limit
49
+ @next_time ? @next_time : @check_time + 1000
50
+ end
51
+ end