maxwell_agent 0.0.1

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,92 @@
1
+ module Maxwell
2
+ module Agent
3
+ module Middleware
4
+ class Chain
5
+ include Enumerable
6
+ attr_reader :entries
7
+
8
+ def initialize
9
+ @entries = []
10
+ yield self if block_given?
11
+ end
12
+
13
+ def each(&block)
14
+ entries.each(&block)
15
+ end
16
+
17
+ def add(klass, *args)
18
+ new_entry = Entry.new(klass, *args)
19
+ if count > 0
20
+ add_at(count + 1, new_entry)
21
+ else
22
+ add_at(count, new_entry)
23
+ end
24
+ end
25
+
26
+ def remove(entry)
27
+ entries.delete_if {|e| e }
28
+ end
29
+
30
+ def insert_before(existing_klass, new_klass, *args)
31
+ new_entry = Entry.new(new_klass, *args)
32
+ i = get_index(existing_klass) || 0
33
+ add_at(i, new_entry)
34
+ end
35
+
36
+ def insert_after(existing_klass, new_klass, *args)
37
+ new_entry = Entry.new(new_klass, *args)
38
+ i = get_index(existing_klass) || count - 1
39
+ add_at(i + 1, new_entry)
40
+ end
41
+
42
+ def invoke(*args, &final_action)
43
+ chain = retrieve.dup
44
+
45
+ traverse_chain = -> do
46
+ if chain.empty?
47
+ final_action.call if final_action
48
+ else
49
+ chain.shift.call(*args, &traverse_chain)
50
+ end
51
+ end
52
+ traverse_chain.call
53
+ end
54
+
55
+ private
56
+ def add_at(index, new_entry)
57
+ remove(new_entry) if exists?(new_entry)
58
+ entries.insert(index, new_entry)
59
+ end
60
+
61
+ def get_index(klass)
62
+ find_index {|entry| entry.klass == klass }
63
+ end
64
+
65
+ def exists?(entry)
66
+ include?(entry)
67
+ end
68
+
69
+ def retrieve
70
+ entries.map(&:build)
71
+ end
72
+
73
+ class Entry
74
+ attr_reader :klass
75
+
76
+ def initialize(klass, *args)
77
+ @klass = klass
78
+ @args = args
79
+ end
80
+
81
+ def build
82
+ @klass.new(*@args)
83
+ end
84
+
85
+ def ==(other)
86
+ self.klass == other.klass
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,12 @@
1
+ module Maxwell
2
+ module Agent
3
+ module Middleware
4
+ class Logging
5
+ def call(work, &chain)
6
+ chain.call
7
+ ::Logger.new('/tmp/test2.log').info("Ran #{work.name}")
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ module Maxwell
2
+ module Agent
3
+ module Probe
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.instance_eval do
7
+ private_class_method :call_handler, :instance
8
+ attr_accessor :output, :args
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def perform(*args)
14
+ probe = instance(*args)
15
+ probe.output = probe.perform(*args)
16
+ call_handler(probe)
17
+ end
18
+
19
+ def instance(*args)
20
+ instance = new
21
+ instance.args = args
22
+ instance
23
+ end
24
+
25
+ def call_handler(probe)
26
+ probe.handle if probe.respond_to?(:handle)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ require 'agent/worker'
2
+ require 'agent/scheduler'
3
+ require 'agent/work_schedule'
4
+
5
+ module Maxwell
6
+ module Agent
7
+ class Runner < Celluloid::SupervisionGroup
8
+
9
+ attr_reader :registry
10
+
11
+ def self.worker_pool_size
12
+ Maxwell.configuration.worker_concurrency
13
+ end
14
+
15
+ supervise Maxwell::WorkSchedule, as: :work_schedule
16
+ pool Maxwell::Worker, as: :worker, size: worker_pool_size
17
+ supervise Maxwell::Scheduler, as: :scheduler
18
+
19
+ def [](actor_name)
20
+ @registry[actor_name]
21
+ end
22
+
23
+ def initialize(opts)
24
+ super
25
+ wait_for_actor_boot
26
+ end
27
+
28
+ def wait_for_actor_boot
29
+ loop do
30
+ break if self[:work_schedule] &&
31
+ self[:worker] &&
32
+ self[:scheduler]
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,36 @@
1
+ module Maxwell
2
+ module Agent
3
+ class Scheduler
4
+ include Celluloid
5
+
6
+ def initialize
7
+ async.run
8
+ end
9
+
10
+ def work_schedule
11
+ runner[:work_schedule]
12
+ end
13
+
14
+ def run
15
+ loop do
16
+ sleep Maxwell.configuration.work_poll
17
+ schedule_work
18
+ end
19
+ end
20
+
21
+ def schedule_work
22
+ work = work_schedule.get
23
+ worker.async.perform(work) if work
24
+ end
25
+
26
+ def worker
27
+ runner[:worker]
28
+ end
29
+
30
+ def runner
31
+ links.detect {|link| Celluloid::SupervisionGroup === link }
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ module Maxwell
2
+ module Agent
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ require 'erb'
2
+ require 'sinatra'
3
+ require 'agent/web_helpers'
4
+ module Maxwell
5
+ module Agent
6
+ class Web < Sinatra::Base
7
+ helpers WebHelpers
8
+
9
+ set :root, File.expand_path(File.dirname(__FILE__) + "/../../web")
10
+ set :public_folder, Proc.new { "#{root}/assets" }
11
+ set :views, Proc.new { "#{root}/views" }
12
+
13
+ get '/' do
14
+ erb :index
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ module Maxwell
2
+ module Agent
3
+ module WebHelpers
4
+ def root_path
5
+ env['SCRIPT_NAME']
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,93 @@
1
+ require 'agent/dynamic_attributes'
2
+ module Maxwell
3
+ module Agent
4
+ module Work
5
+ class MissingRequiredAttributeError < StandardError; end
6
+
7
+ REQUIRED_ATTRIBUTES = [:name, :work_class]
8
+ #WORK_ATTRIBUTES = [:last_run, :frequency, :perform_at].
9
+ # concat(REQUIRED_ATTRIBUTES)
10
+
11
+ def self.included(base)
12
+ base.send(:include, DynamicAttributes)
13
+ end
14
+
15
+
16
+ def self.load(json)
17
+ work = from_json(json)
18
+ work.delete('klass').constantize.new.load(work)
19
+ end
20
+
21
+ def self.from_json(json)
22
+ JSON.parse(json)
23
+ end
24
+
25
+ def load(attrs={})
26
+ attrs.each do |key, value|
27
+ send("#{key}=", value )
28
+ end
29
+ self
30
+ end
31
+
32
+ def to_json
33
+ verify_required_attributes!
34
+ instance_variables.inject({}) do |result, attr|
35
+ result.merge(attr.to_s.gsub('@','') => instance_variable_get(attr))
36
+ end.merge({klass: self.class}).to_json
37
+ end
38
+
39
+ def verify_required_attributes!
40
+ REQUIRED_ATTRIBUTES.each do |required_attr|
41
+ raise MissingRequiredAttributeError,
42
+ "Must set #{required_attr}" unless send(required_attr)
43
+ end
44
+ end
45
+
46
+ def last_run
47
+ super || self.last_run = Time.new(0)
48
+ end
49
+
50
+ def frequency
51
+ super || self.frequency = 30.minutes
52
+ end
53
+
54
+ def work_now?
55
+ case
56
+ when perform_at then perform_at_less_than_now?
57
+ when frequency then stale?
58
+ else true
59
+ end
60
+ end
61
+
62
+ def expected_next_run
63
+ case
64
+ when perform_at then perform_at + last_run.to_i
65
+ when (last_run && frequency) then last_run + frequency
66
+ else Time.new
67
+ end
68
+ end
69
+
70
+ def generate_rank
71
+ expected_next_run.to_i
72
+ end
73
+
74
+ def perform
75
+ work_class.perform(*arguments)
76
+ end
77
+
78
+ private
79
+ def time_since_last_run
80
+ Time.now.to_i - last_run.to_i
81
+ end
82
+
83
+ def perform_at_less_than_now?
84
+ return perform_at.find {|at| at <= Time.now } if perform_at.respond_to? :each
85
+ perform_at <= Time.now
86
+ end
87
+
88
+ def stale?
89
+ time_since_last_run >= frequency
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,99 @@
1
+ module Maxwell
2
+ module Agent
3
+ class WorkSchedule
4
+ include Celluloid
5
+
6
+ def add(work)
7
+ async.add_work(work)
8
+ work
9
+ end
10
+
11
+ def count
12
+ redis do |redis|
13
+ redis.zcard 'work_schedule'
14
+ end
15
+ end
16
+
17
+ def get
18
+ find_ready_for_work
19
+ end
20
+
21
+ def put_back(work)
22
+ remove_from_working_queue(work)
23
+ async.add(work)
24
+ end
25
+
26
+ def working
27
+ work_items = redis do |redis|
28
+ redis.smembers 'work_schedule:working'
29
+ end
30
+ work_items.map {|work| Work.load(work) }
31
+ end
32
+
33
+ def schedule
34
+ work_items = redis do |redis|
35
+ redis.zrange 'work_schedule', 0, -1
36
+ end
37
+ work_items.map {|work| Work.load(work) }
38
+ end
39
+
40
+ def all
41
+ schedule.concat(working)
42
+ end
43
+
44
+ private
45
+ def redis(&block)
46
+ Maxwell.redis(&block)
47
+ end
48
+
49
+ def find_ready_for_work
50
+ work = get_work
51
+ move_to_working_queue(work) if work && work.work_now?
52
+ work
53
+ end
54
+
55
+ def get_work
56
+ work = redis do |redis|
57
+ redis.zrange('work_schedule', 0, 0)[0]
58
+ end
59
+
60
+ Work.load(work) if work
61
+ end
62
+
63
+ def move_to_working_queue(work)
64
+ add_to_working_queue(work)
65
+ remove_from_main_queue(work)
66
+ end
67
+
68
+ def remove_from_main_queue(work)
69
+ redis do |redis|
70
+ redis.zrem 'work_schedule', work.to_json
71
+ end
72
+ end
73
+
74
+ def add_to_working_queue(work)
75
+ redis do |redis|
76
+ redis.sadd 'work_schedule:working', work.to_json
77
+ end
78
+ end
79
+
80
+ def remove_from_working_queue(work)
81
+ redis do |redis|
82
+ redis.srem 'work_schedule:working', work.to_json
83
+ end
84
+ end
85
+
86
+ def add_work(work)
87
+ redis do |redis|
88
+ redis.zadd 'work_schedule', work.generate_rank, work.to_json
89
+ end
90
+ end
91
+
92
+ def is_being_worked_on?(work)
93
+ redis do |redis|
94
+ redis.sismember 'work_schedule:working', work.to_json
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,28 @@
1
+ module Maxwell
2
+ module Agent
3
+ class Worker
4
+ include Celluloid
5
+
6
+ def perform(work)
7
+ work.perform
8
+
9
+ post_run(work)
10
+ ensure
11
+ work_schedule.put_back(work)
12
+ end
13
+
14
+ def work_schedule
15
+ Maxwell.runner[:work_schedule]
16
+ end
17
+
18
+ private
19
+
20
+ def post_run(work)
21
+ work.perform_at = nil
22
+ work.last_run = Time.now
23
+
24
+ Maxwell::Agent.middleware.invoke(work)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,88 @@
1
+ require 'bundler/setup'
2
+ Bundler.require(:default, (ENV['RACK_ENV'] || :development))
3
+ require 'json'
4
+ require 'maxwell/agent/configuration'
5
+ require 'maxwell/agent/middleware/chain'
6
+ require 'maxwell/agent/middleware/logging'
7
+ require 'maxwell/agent/host'
8
+ require 'maxwell/agent/probe'
9
+ require 'maxwell/agent/work'
10
+
11
+
12
+ module Maxwell
13
+ module Agent
14
+ class << self
15
+ def runner
16
+ @runner
17
+ end
18
+
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield configuration if block_given?
25
+ configuration
26
+ end
27
+
28
+ def start
29
+ if dead_runner?
30
+ @runner = Runner.run
31
+ end
32
+ end
33
+
34
+ def start!
35
+ if dead_runner?
36
+ @runner = Runner.run!
37
+ end
38
+ end
39
+
40
+ def stop
41
+ runner.terminate
42
+ end
43
+
44
+ def middleware
45
+ yield configuration.middleware_chain if block_given?
46
+ configuration.middleware_chain
47
+ end
48
+
49
+ def running?
50
+ runner.alive?
51
+ end
52
+
53
+ def stopped?
54
+ !running?
55
+ end
56
+
57
+ def redis(&block)
58
+ @redis ||= ConnectionPool.new(
59
+ size: (configuration.worker_concurrency + 2)) {
60
+ Redis.new configuration.redis_options
61
+ }
62
+ @redis.with(&block)
63
+ end
64
+
65
+ private
66
+
67
+ def dead_runner?
68
+ if runner && runner.alive?
69
+ false
70
+ else
71
+ true
72
+ end
73
+ end
74
+
75
+ def cleanup_dead_runner
76
+ if dead_runner?
77
+ @runner = nil
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ Dir[File.dirname(__FILE__) + '/../plugins/*.rb'].each do |file|
85
+ require file
86
+ end
87
+
88
+ require 'agent/runner'
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'maxwell/agent/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "maxwell_agent"
8
+ spec.version = Maxwell::Agent::VERSION
9
+ spec.authors = ["Brian Goff"]
10
+ spec.email = ["cpuguy83@gmail.com"]
11
+ spec.description = %q{Maxwell Agent}
12
+ spec.summary = %q{Maxwell Agent}
13
+ spec.homepage = "http://www.github.com/cpuguy83/maxwell_agent"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rpsec-given"
25
+
26
+ spec.add_runtime_dependency "celluoid", "~> 0.15.0"
27
+ spec.add_runtime_dependency "activesupport"
28
+ end
29
+
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ end
10
+
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ module Maxwell
3
+ module Agent
4
+ describe Host do
5
+ describe '.services' do
6
+ Given(:host) { Maxwell::Host.new }
7
+ Then { expect(host.services).to respond_to :count}
8
+ Then { expect(host.services).to respond_to :each }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+ module Maxwell
3
+ module Agent
4
+ describe Scheduler do
5
+ before :each do
6
+ runner = double(:runner)
7
+ Maxwell.stub(:runner).and_return(runner)
8
+ allow(runner).to receive(:[]).with(:scheduler).and_return(Maxwell::Scheduler.new)
9
+ end
10
+ Given(:scheduler) { Maxwell.runner[:scheduler] }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ module Maxwell
3
+ module Agent
4
+ describe WorkSchedule do
5
+ before :each do
6
+ runner = double(:runner)
7
+ Maxwell.stub(:runner).and_return(runner)
8
+ allow(runner).to receive(:[]).with(:work_schedule).
9
+ and_return(WorkSchedule.new)
10
+ end
11
+
12
+ Given(:queue) { Maxwell.runner[:work_schedule] }
13
+ Given(:work) { WorkTest.new(name: 'foo', work_class: 'bar') }
14
+ describe '.add' do
15
+ context 'Work is added' do
16
+ When { queue.add(work) }
17
+ Then { expect(queue.count).to be 1 }
18
+ end
19
+ context 'The same Work is added 2x' do
20
+ Given { queue.add(work) }
21
+ When { queue.add(work) }
22
+ Then { expect(queue.count).to be 1 }
23
+ end
24
+ end
25
+
26
+ describe '.working' do
27
+ Given { queue.add(work) }
28
+ When { queue.get }
29
+ Then { expect(queue.working.count).to be 1 }
30
+ And { expect(queue.count).to be 0 }
31
+ end
32
+
33
+ describe '.put_back' do
34
+ Given { queue.add(work) }
35
+ Given { queue.get }
36
+ When { queue.put_back(work) }
37
+ Then { expect(queue.count).to eq 1 }
38
+ And { expect(queue.working.count).to eq 0 }
39
+ end
40
+
41
+ describe '.all' do
42
+ Given { queue.add(work) }
43
+ Given { queue.add(WorkTest.new(name: 'foo', work_class: 'bar',
44
+ perform_at: 5.minutes.ago)) }
45
+ When { queue.get }
46
+ Then { expect(queue.all.count).to be 2 }
47
+ And { expect(queue.working.count).to be 1 }
48
+ And { expect(queue.schedule.count).to be 1 }
49
+ end
50
+
51
+ end
52
+ end
53
+ end