rplex 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/Gemfile +7 -0
- data/History.txt +5 -0
- data/Manifest.txt +12 -0
- data/README.txt +60 -0
- data/Rakefile +18 -0
- data/lib/rplex/client.rb +68 -0
- data/lib/rplex/jobs.rb +52 -0
- data/lib/rplex/server.rb +55 -0
- data/lib/rplex.rb +8 -0
- data/test/test_jobs.rb +37 -0
- data/test/test_rplex.rb +9 -0
- data/test/test_server.rb +42 -0
- metadata +151 -0
data/.gemtest
ADDED
File without changes
|
data/Gemfile
ADDED
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
= rplex
|
2
|
+
|
3
|
+
http://c11865-prj.zuehlke.com
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Simple asynchronous data based job management
|
8
|
+
|
9
|
+
== INSTALL:
|
10
|
+
|
11
|
+
* sudo gem install rplex
|
12
|
+
|
13
|
+
=== Why?
|
14
|
+
Because anything involving redis, postgres etc. is just a pain to install on Windows and we just wanted to parallelize a bunch of build tasks.
|
15
|
+
|
16
|
+
=== How do you use it?
|
17
|
+
In a CI system to reduce our build times by parallelizing test execution:
|
18
|
+
|
19
|
+
The main build finishes and posts as data the revision and the location of the build artifacts to rplex. rplex queues the data for every worker that has appeared until that point.
|
20
|
+
Each worker polls the rplex queue, and then uses the information to grab the build and run a bunch of tests.
|
21
|
+
|
22
|
+
=== Is that all?
|
23
|
+
Queues are created automatically when a worker tries to get data from the queue, data can be posted to a subset of the workers if necessary and that as they say is it.
|
24
|
+
|
25
|
+
== Example
|
26
|
+
Start the rplex service.
|
27
|
+
|
28
|
+
Use Rplex::Client to post data:
|
29
|
+
|
30
|
+
Rplex::Client.new("name","http://rplex.host:7777/job").new_job(job_id,data)
|
31
|
+
|
32
|
+
Use Rplex::Processor to check the queue and do something with any data present
|
33
|
+
|
34
|
+
Rplex::Processor.new(Rplex::Client.new("name","http://rplex.host:7777/job"),5).run!{|job_data| p job_data}
|
35
|
+
|
36
|
+
|
37
|
+
== LICENSE:
|
38
|
+
|
39
|
+
(The MIT License)
|
40
|
+
|
41
|
+
Copyright (c) 2011-2012 Vassilis Rizopoulos
|
42
|
+
|
43
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
44
|
+
a copy of this software and associated documentation files (the
|
45
|
+
'Software'), to deal in the Software without restriction, including
|
46
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
47
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
48
|
+
permit persons to whom the Software is furnished to do so, subject to
|
49
|
+
the following conditions:
|
50
|
+
|
51
|
+
The above copyright notice and this permission notice shall be
|
52
|
+
included in all copies or substantial portions of the Software.
|
53
|
+
|
54
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
55
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
56
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
57
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
58
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
59
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
60
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__),"lib")
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require 'rplex'
|
6
|
+
|
7
|
+
Hoe.spec 'rplex' do |prj|
|
8
|
+
developer('Vassilis Rizopoulos', 'var@zuehlke.com')
|
9
|
+
prj.version=Rplex::Version::STRING
|
10
|
+
prj.summary = 'rplex multiplexes jobs across multiple workers'
|
11
|
+
prj.description = prj.paragraphs_of('README.txt', 1..4).join("\n\n")
|
12
|
+
prj.url = "http://github.com/damphyr/rplex"
|
13
|
+
prj.changes = prj.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
|
+
prj.extra_deps<<["sinatra", "~>1.3.1"]
|
15
|
+
prj.extra_deps<<['rest-client',"~>1.6.7"]
|
16
|
+
end
|
17
|
+
|
18
|
+
# vim: syntax=ruby
|
data/lib/rplex/client.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'json'
|
3
|
+
module Rplex
|
4
|
+
class ClientError<RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
class Client
|
8
|
+
def initialize name,srv
|
9
|
+
@name=name
|
10
|
+
@service=srv
|
11
|
+
end
|
12
|
+
|
13
|
+
def new_job identifier,data,workers=[]
|
14
|
+
puts "Adding #{identifier} to #{@service}"
|
15
|
+
response=RestClient.post(@service, {"identifier"=>identifier, "data" => data,"workers"=>workers}, :content_type => :json, :accept => :json)
|
16
|
+
return response
|
17
|
+
rescue
|
18
|
+
raise ClientError, $!.message
|
19
|
+
end
|
20
|
+
|
21
|
+
def next_job
|
22
|
+
srv="#{@service}/#{@name}"
|
23
|
+
response=RestClient.get(srv,:accept => :json)
|
24
|
+
unless response.empty?
|
25
|
+
return JSON.parse(response)
|
26
|
+
else
|
27
|
+
return {}
|
28
|
+
end
|
29
|
+
rescue
|
30
|
+
raise ClientError, $!.message
|
31
|
+
end
|
32
|
+
|
33
|
+
def backlog
|
34
|
+
response=RestClient.get(@service,:accept => :json)
|
35
|
+
unless response.empty?
|
36
|
+
return JSON.parse(response)
|
37
|
+
else
|
38
|
+
return []
|
39
|
+
end
|
40
|
+
rescue
|
41
|
+
raise ClientError, $!.message
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
"#{@name} working with #{@service}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Processor
|
50
|
+
def initialize client,interval=30
|
51
|
+
@client=client
|
52
|
+
@interval=interval
|
53
|
+
end
|
54
|
+
|
55
|
+
def run!
|
56
|
+
raise "You need to provide a block" unless block_given?
|
57
|
+
while true do
|
58
|
+
begin
|
59
|
+
job_data=@client.next_job
|
60
|
+
yield job_data unless job_data.empty?
|
61
|
+
rescue ClientError
|
62
|
+
puts $!
|
63
|
+
end
|
64
|
+
sleep @interval
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/rplex/jobs.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Rplex
|
4
|
+
class InvalidData < RuntimeError
|
5
|
+
end
|
6
|
+
#Simple queue management for Rplex job data
|
7
|
+
class Overseer
|
8
|
+
def initialize
|
9
|
+
@queues={}
|
10
|
+
end
|
11
|
+
#Add a job for all workers currently active
|
12
|
+
def << job_data
|
13
|
+
add_job(job_data)
|
14
|
+
end
|
15
|
+
#Add a job.
|
16
|
+
#
|
17
|
+
#You can limit the workers it is distributed to by providing an Array
|
18
|
+
#with the worker identifiers
|
19
|
+
def add_job job_data,workers=[]
|
20
|
+
queued_in=0
|
21
|
+
workers=@queues.keys if workers.empty?
|
22
|
+
if valid?(job_data)
|
23
|
+
@queues.each do |w,q|
|
24
|
+
if workers.include?(w)
|
25
|
+
q.push(job_data)
|
26
|
+
queued_in+=1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
raise InvalidData
|
31
|
+
end
|
32
|
+
return queued_in
|
33
|
+
end
|
34
|
+
#Get the next job for the worker
|
35
|
+
#
|
36
|
+
#If there is no Queue for the worker, create an empty one
|
37
|
+
def [](worker)
|
38
|
+
@queues[worker]||=Queue.new
|
39
|
+
@queues[worker].pop(true) rescue nil
|
40
|
+
end
|
41
|
+
#Get an array of [name,queue size]
|
42
|
+
def backlog
|
43
|
+
@queues.map{|k,v| [k,v.size]}
|
44
|
+
end
|
45
|
+
#Returns true if the job data is valid
|
46
|
+
def valid? job_data
|
47
|
+
job_data["identifier"] && job_data["data"]
|
48
|
+
rescue
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/rplex/server.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'rplex'
|
3
|
+
require 'rplex/jobs'
|
4
|
+
require 'json'
|
5
|
+
module Rplex
|
6
|
+
|
7
|
+
class Server<Sinatra::Base
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@overseer = Rplex::Overseer.new
|
11
|
+
end
|
12
|
+
post '/job' do
|
13
|
+
begin
|
14
|
+
reply={}
|
15
|
+
workers= params["workers"] ? params["workers"] : []
|
16
|
+
reply[:workers]=@overseer.add_job(params,workers)
|
17
|
+
[200,{'Content-Type' => 'application/json'},reply.to_json]
|
18
|
+
rescue
|
19
|
+
status 500
|
20
|
+
end
|
21
|
+
end
|
22
|
+
get '/job/:worker' do |worker|
|
23
|
+
reply={}
|
24
|
+
reply=@overseer[worker]
|
25
|
+
if reply
|
26
|
+
[200,{'Content-Type' => 'application/json'},reply.to_json]
|
27
|
+
else
|
28
|
+
status 204
|
29
|
+
end
|
30
|
+
end
|
31
|
+
get '/' do
|
32
|
+
[200,{'Content-Type' => 'application/json'},{"version"=>Rplex::Version::STRING}.to_json]
|
33
|
+
end
|
34
|
+
|
35
|
+
get '/backlog' do
|
36
|
+
[200,{'Content-Type' => 'application/json'},@overseer.backlog.to_json]
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.define_settings cfg={}
|
40
|
+
cfg||={}
|
41
|
+
#the settings that are not public
|
42
|
+
enable :logging
|
43
|
+
enable :run
|
44
|
+
enable :static
|
45
|
+
set :server, %w[thin mongrel webrick]
|
46
|
+
set :root, File.dirname(__FILE__)
|
47
|
+
#the settings that can be changed
|
48
|
+
cfg[:public_folder] ||= File.dirname(__FILE__) + '/public'
|
49
|
+
cfg[:port] ||= 7777
|
50
|
+
#set them
|
51
|
+
set :port, cfg[:port]
|
52
|
+
set :public_folder,cfg[:public_folder]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/rplex.rb
ADDED
data/test/test_jobs.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__),"..","lib")
|
2
|
+
require "test/unit"
|
3
|
+
require "rplex/jobs"
|
4
|
+
|
5
|
+
class TestOverseer < Test::Unit::TestCase
|
6
|
+
def test_invalid_data
|
7
|
+
ov=Rplex::Overseer.new
|
8
|
+
job_data={:foo=>"8888"}
|
9
|
+
assert_raise(Rplex::InvalidData) { ov << job_data }
|
10
|
+
job_data="foo"
|
11
|
+
assert_raise(Rplex::InvalidData) { ov << job_data }
|
12
|
+
assert_raise(Rplex::InvalidData) { ov << nil }
|
13
|
+
end
|
14
|
+
def test_basic
|
15
|
+
ov=Rplex::Overseer.new
|
16
|
+
job_data={"identifier"=>"8888","data"=>{:foo=>"bar",:bar=>"foo"}}
|
17
|
+
assert_nil(ov["worker"])
|
18
|
+
#expect that there is only 1 queue there
|
19
|
+
assert_equal(1, ov<<job_data)
|
20
|
+
assert_nil(ov["slave"])
|
21
|
+
assert_equal(2, ov<<job_data)
|
22
|
+
assert_equal(1, ov.add_job(job_data,["slave"]) )
|
23
|
+
#go through the slave queue
|
24
|
+
assert_equal(job_data,ov["slave"])
|
25
|
+
assert_equal(job_data,ov["slave"])
|
26
|
+
assert_nil(ov["slave"])
|
27
|
+
end
|
28
|
+
def test_backlog
|
29
|
+
ov=Rplex::Overseer.new
|
30
|
+
assert(ov.backlog.empty?)
|
31
|
+
job_data={"identifier"=>"8888","data"=>{:foo=>"bar",:bar=>"foo"}}
|
32
|
+
ov['worker1']
|
33
|
+
ov['worker2']
|
34
|
+
ov<<job_data
|
35
|
+
assert_equal([['worker1',1],['worker2',1]], ov.backlog)
|
36
|
+
end
|
37
|
+
end
|
data/test/test_rplex.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__),"..","lib")
|
2
|
+
require "test/unit"
|
3
|
+
require "rplex"
|
4
|
+
|
5
|
+
class TestRplex < Test::Unit::TestCase
|
6
|
+
def test_version
|
7
|
+
assert_equal(Rplex::Version::STRING, "#{Rplex::Version::MAJOR}.#{Rplex::Version::MINOR}.#{Rplex::Version::TINY}")
|
8
|
+
end
|
9
|
+
end
|
data/test/test_server.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__),"..","lib")
|
2
|
+
require 'test/unit'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rplex/server'
|
5
|
+
require 'rack/test'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
class ServerTest < Test::Unit::TestCase
|
9
|
+
include Rack::Test::Methods
|
10
|
+
|
11
|
+
def app
|
12
|
+
Rplex::Server
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_root
|
16
|
+
get '/'
|
17
|
+
assert_equal({"version"=>Rplex::Version::STRING},JSON.parse(last_response.body))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_new
|
21
|
+
post '/job', :identifier=>"8888",:data=>{:url=>"http://foo.bar/drops",:revision=>"8888"}
|
22
|
+
assert_not_nil(JSON.parse(last_response.body)["workers"])
|
23
|
+
|
24
|
+
post '/job', :foo=>"bar"
|
25
|
+
assert_equal(500, last_response.status)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_job
|
29
|
+
get '/job/worker'
|
30
|
+
assert_equal(204,last_response.status)
|
31
|
+
payload={"identifier"=>"8888","data"=>{"url"=>"http://foo.bar/drops","revision"=>"8888"}}
|
32
|
+
post '/job', payload
|
33
|
+
get '/job/worker'
|
34
|
+
assert_equal(200,last_response.status)
|
35
|
+
assert_equal(payload,JSON.parse(last_response.body))
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_backlog
|
39
|
+
get '/backlog'
|
40
|
+
assert_equal(200, last_response.status)
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rplex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Vassilis Rizopoulos
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-01-04 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: sinatra
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 25
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 3
|
32
|
+
- 1
|
33
|
+
version: 1.3.1
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rest-client
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 1
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 6
|
48
|
+
- 7
|
49
|
+
version: 1.6.7
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rdoc
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 19
|
61
|
+
segments:
|
62
|
+
- 3
|
63
|
+
- 10
|
64
|
+
version: "3.10"
|
65
|
+
type: :development
|
66
|
+
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: hoe
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 27
|
76
|
+
segments:
|
77
|
+
- 2
|
78
|
+
- 12
|
79
|
+
version: "2.12"
|
80
|
+
type: :development
|
81
|
+
version_requirements: *id004
|
82
|
+
description: |-
|
83
|
+
http://c11865-prj.zuehlke.com
|
84
|
+
|
85
|
+
== DESCRIPTION:
|
86
|
+
|
87
|
+
Simple asynchronous data based job management
|
88
|
+
|
89
|
+
== INSTALL:
|
90
|
+
email:
|
91
|
+
- var@zuehlke.com
|
92
|
+
executables: []
|
93
|
+
|
94
|
+
extensions: []
|
95
|
+
|
96
|
+
extra_rdoc_files:
|
97
|
+
- History.txt
|
98
|
+
- Manifest.txt
|
99
|
+
- README.txt
|
100
|
+
files:
|
101
|
+
- Gemfile
|
102
|
+
- History.txt
|
103
|
+
- lib/rplex.rb
|
104
|
+
- lib/rplex/client.rb
|
105
|
+
- lib/rplex/jobs.rb
|
106
|
+
- lib/rplex/server.rb
|
107
|
+
- Manifest.txt
|
108
|
+
- Rakefile
|
109
|
+
- README.txt
|
110
|
+
- test/test_jobs.rb
|
111
|
+
- test/test_server.rb
|
112
|
+
- test/test_rplex.rb
|
113
|
+
- .gemtest
|
114
|
+
homepage: http://github.com/damphyr/rplex
|
115
|
+
licenses: []
|
116
|
+
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options:
|
119
|
+
- --main
|
120
|
+
- README.txt
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
none: false
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
hash: 3
|
129
|
+
segments:
|
130
|
+
- 0
|
131
|
+
version: "0"
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
hash: 3
|
138
|
+
segments:
|
139
|
+
- 0
|
140
|
+
version: "0"
|
141
|
+
requirements: []
|
142
|
+
|
143
|
+
rubyforge_project: rplex
|
144
|
+
rubygems_version: 1.8.6
|
145
|
+
signing_key:
|
146
|
+
specification_version: 3
|
147
|
+
summary: rplex multiplexes jobs across multiple workers
|
148
|
+
test_files:
|
149
|
+
- test/test_jobs.rb
|
150
|
+
- test/test_rplex.rb
|
151
|
+
- test/test_server.rb
|