execache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/LICENSE +18 -0
- data/README.md +75 -0
- data/Rakefile +1 -0
- data/bin/execache +3 -0
- data/execache.gemspec +26 -0
- data/lib/execache.rb +117 -0
- data/lib/execache/client.rb +31 -0
- data/spec/execache_spec.rb +123 -0
- data/spec/fixtures/execache.yml +6 -0
- data/spec/fixtures/fixture.rb +5 -0
- data/spec/spec_helper.rb +9 -0
- metadata +97 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2010
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
Execache
|
2
|
+
========
|
3
|
+
|
4
|
+
Run commands in parallel and cache the output. Redis queues jobs and stores the result.
|
5
|
+
|
6
|
+
Requirements
|
7
|
+
------------
|
8
|
+
|
9
|
+
<pre>
|
10
|
+
gem install execache
|
11
|
+
</pre>
|
12
|
+
|
13
|
+
How Your Binaries Should Behave
|
14
|
+
-------------------------------
|
15
|
+
|
16
|
+
Execache assumes that the script or binary you are executing has multiple results and sometimes multiple groups of results.
|
17
|
+
|
18
|
+
Example output:
|
19
|
+
|
20
|
+
$ bin/some/binary preliminary_arg arg1a arg1b arg2a arg2b
|
21
|
+
$ arg1_result_1
|
22
|
+
$ arg1_result_2
|
23
|
+
$ [END]
|
24
|
+
$ arg2_result_1
|
25
|
+
$ arg2_result_2
|
26
|
+
|
27
|
+
Your binary may take zero or more preliminary arguments (e.g. `preliminary_arg`), followed by argument "groups" that dictate output (e.g. `arg1a arg1b`).
|
28
|
+
|
29
|
+
Configure
|
30
|
+
---------
|
31
|
+
|
32
|
+
Given the above example, our `execache.yml` looks like this:
|
33
|
+
|
34
|
+
redis: localhost:6379/0
|
35
|
+
some_binary:
|
36
|
+
command: '/bin/some/binary'
|
37
|
+
separators:
|
38
|
+
result: "\n"
|
39
|
+
group: "[END]"
|
40
|
+
|
41
|
+
Start the Server
|
42
|
+
----------------
|
43
|
+
|
44
|
+
$ execache /path/to/execache.yml
|
45
|
+
|
46
|
+
Execute Commands
|
47
|
+
----------------
|
48
|
+
|
49
|
+
require 'rubygems'
|
50
|
+
require 'execache'
|
51
|
+
|
52
|
+
client = Execache::Client.new("localhost:6379/0")
|
53
|
+
|
54
|
+
results = client.exec(
|
55
|
+
:some_binary => {
|
56
|
+
:args => 'preliminary_arg',
|
57
|
+
:groups => [
|
58
|
+
{
|
59
|
+
:args => 'arg1a arg1b',
|
60
|
+
:ttl => 60
|
61
|
+
},
|
62
|
+
{
|
63
|
+
:args => 'arg2a arg2b',
|
64
|
+
:ttl => 60
|
65
|
+
}
|
66
|
+
]
|
67
|
+
}
|
68
|
+
)
|
69
|
+
|
70
|
+
results == {
|
71
|
+
:some_binary => [
|
72
|
+
[ 'arg1_result_1', 'arg1_result_2' ],
|
73
|
+
[ 'arg2_result_1', 'arg2_result_2' ]
|
74
|
+
]
|
75
|
+
}
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/bin/execache
ADDED
data/execache.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
root = File.expand_path('../', __FILE__)
|
3
|
+
lib = "#{root}/lib"
|
4
|
+
|
5
|
+
$:.unshift lib unless $:.include?(lib)
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "execache"
|
9
|
+
s.version = '0.1.0'
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.authors = [ "Winton Welsh" ]
|
12
|
+
s.email = [ "mail@wintoni.us" ]
|
13
|
+
s.homepage = "http://github.com/winton/execache"
|
14
|
+
s.summary = %q{Run commands in parallel and cache the output, controlled by Redis}
|
15
|
+
s.description = %q{Run commands in parallel and cache the output. Redis queues jobs and stores the result.}
|
16
|
+
|
17
|
+
s.executables = `cd #{root} && git ls-files bin/*`.split("\n").collect { |f| File.basename(f) }
|
18
|
+
s.files = `cd #{root} && git ls-files`.split("\n")
|
19
|
+
s.require_paths = %w(lib)
|
20
|
+
s.test_files = `cd #{root} && git ls-files -- {features,test,spec}/*`.split("\n")
|
21
|
+
|
22
|
+
s.add_development_dependency "rspec", "~> 1.0"
|
23
|
+
|
24
|
+
s.add_dependency "redis", "~> 2.2.2"
|
25
|
+
s.add_dependency "yajl-ruby", "~> 1.0.0"
|
26
|
+
end
|
data/lib/execache.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require "digest/sha1"
|
2
|
+
require "timeout"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
gem "yajl-ruby", "~> 1.0.0"
|
6
|
+
require "yajl"
|
7
|
+
|
8
|
+
gem "redis", "~> 2.2.2"
|
9
|
+
require "redis"
|
10
|
+
|
11
|
+
$:.unshift File.dirname(__FILE__)
|
12
|
+
|
13
|
+
require 'execache/client'
|
14
|
+
|
15
|
+
class Execache
|
16
|
+
|
17
|
+
def initialize(yaml)
|
18
|
+
options = YAML.load(File.read(yaml))
|
19
|
+
|
20
|
+
puts "\nStarting execache server (redis @ #{options['redis']})..."
|
21
|
+
|
22
|
+
redis = Redis.connect(:url => "redis://#{options['redis']}")
|
23
|
+
retries = 0
|
24
|
+
|
25
|
+
begin
|
26
|
+
while true
|
27
|
+
request = redis.lpop('execache:request')
|
28
|
+
if request
|
29
|
+
Thread.new do
|
30
|
+
request = Yajl::Parser.parse(request)
|
31
|
+
channel = request.delete('channel')
|
32
|
+
commands = []
|
33
|
+
|
34
|
+
request.each do |cmd_type, cmd_options|
|
35
|
+
# Command with preliminary args
|
36
|
+
command = [
|
37
|
+
options[cmd_type]['command'],
|
38
|
+
cmd_options['args']
|
39
|
+
]
|
40
|
+
|
41
|
+
# Fill results with caches if present
|
42
|
+
cmd_options['groups'].each do |group|
|
43
|
+
cache_key = Digest::SHA1.hexdigest(
|
44
|
+
"#{cmd_options['args']} #{group['args']}"
|
45
|
+
)
|
46
|
+
group['cache_key'] = cache_key = "execache:cache:#{cache_key}"
|
47
|
+
cache = redis.get(cache_key)
|
48
|
+
|
49
|
+
if cache
|
50
|
+
group['result'] = Yajl::Parser.parse(cache)
|
51
|
+
else
|
52
|
+
command << group['args']
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add command to be executed if not all args are cached
|
58
|
+
if command.length > 2
|
59
|
+
cmd_options['cmd'] = command.join(' ')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Build response
|
64
|
+
response = request.inject({}) do |hash, (cmd_type, cmd_options)|
|
65
|
+
hash[cmd_type] = []
|
66
|
+
|
67
|
+
if cmd_options['cmd']
|
68
|
+
separators = options[cmd_type]['separators'] || {}
|
69
|
+
separators['group'] ||= "[END]"
|
70
|
+
separators['result'] ||= "\n"
|
71
|
+
output = `#{cmd_options['cmd']}`
|
72
|
+
output = output.split(separators['group'] + separators['result'])
|
73
|
+
output = output.collect { |r| r.split(separators['result']) }
|
74
|
+
end
|
75
|
+
|
76
|
+
cmd_options['groups'].each do |group|
|
77
|
+
if group['result']
|
78
|
+
hash[cmd_type] << group['result']
|
79
|
+
else
|
80
|
+
hash[cmd_type] << output.shift
|
81
|
+
redis.set(
|
82
|
+
group['cache_key'],
|
83
|
+
Yajl::Encoder.encode(hash[cmd_type].last)
|
84
|
+
)
|
85
|
+
if group['ttl']
|
86
|
+
redis.expire(group['cache_key'], group['ttl'])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
hash
|
92
|
+
end
|
93
|
+
|
94
|
+
redis.publish(
|
95
|
+
"execache:response:#{channel}",
|
96
|
+
Yajl::Encoder.encode(response)
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
sleep(1.0 / 1000.0)
|
101
|
+
end
|
102
|
+
rescue Interrupt
|
103
|
+
shut_down
|
104
|
+
rescue Exception => e
|
105
|
+
puts "\nError: #{e.message}"
|
106
|
+
puts "\t#{e.backtrace.join("\n\t")}"
|
107
|
+
retries += 1
|
108
|
+
shut_down if retries >= 10
|
109
|
+
retry
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def shut_down
|
114
|
+
puts "\nShutting down execache server..."
|
115
|
+
exit
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Execache
|
2
|
+
class Client
|
3
|
+
|
4
|
+
attr_reader :redis_1, :redis_2
|
5
|
+
|
6
|
+
def initialize(redis_url)
|
7
|
+
@redis_1 = Redis.connect(:url => "redis://#{redis_url}")
|
8
|
+
@redis_2 = Redis.connect(:url => "redis://#{redis_url}")
|
9
|
+
end
|
10
|
+
|
11
|
+
def exec(options)
|
12
|
+
options[:channel] = Digest::SHA1.hexdigest("#{rand}")
|
13
|
+
response = nil
|
14
|
+
|
15
|
+
Timeout.timeout(60) do
|
16
|
+
@redis_1.subscribe("execache:response:#{options[:channel]}") do |on|
|
17
|
+
on.subscribe do |channel, subscriptions|
|
18
|
+
@redis_2.rpush "execache:request", Yajl::Encoder.encode(options)
|
19
|
+
end
|
20
|
+
|
21
|
+
on.message do |channel, message|
|
22
|
+
response = Yajl::Parser.parse(message)
|
23
|
+
@redis_1.unsubscribe
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
response
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Execache do
|
4
|
+
|
5
|
+
def client_exec
|
6
|
+
@client.exec(
|
7
|
+
:some_binary => {
|
8
|
+
:args => 'preliminary_arg',
|
9
|
+
:groups => [
|
10
|
+
{
|
11
|
+
:args => 'arg1a arg1b',
|
12
|
+
:ttl => 60
|
13
|
+
},
|
14
|
+
{
|
15
|
+
:args => 'arg2a arg2b',
|
16
|
+
:ttl => 60
|
17
|
+
}
|
18
|
+
]
|
19
|
+
}
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
before(:all) do
|
24
|
+
@thread = Thread.new do
|
25
|
+
Execache.new("#{$root}/spec/fixtures/execache.yml")
|
26
|
+
end
|
27
|
+
@client = Execache::Client.new("localhost:6379/0")
|
28
|
+
@client.redis_1.keys("execache:cache:*").each do |key|
|
29
|
+
@client.redis_1.del(key)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
after(:all) do
|
34
|
+
@thread.kill
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return proper results" do
|
38
|
+
client_exec.should == {
|
39
|
+
"some_binary" => [
|
40
|
+
["arg1_result_1", "arg1_result_2"],
|
41
|
+
["arg2_result_1", "arg2_result_2"]
|
42
|
+
]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should write to cache" do
|
47
|
+
keys = @client.redis_1.keys("execache:cache:*")
|
48
|
+
keys.length.should == 2
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should read from cache" do
|
52
|
+
keys = @client.redis_1.keys("execache:cache:*")
|
53
|
+
@client.redis_1.set(keys[0], "[\"cached!\"]")
|
54
|
+
@client.redis_1.set(keys[1], "[\"cached!\"]")
|
55
|
+
client_exec.should == {
|
56
|
+
"some_binary" => [
|
57
|
+
["cached!"],
|
58
|
+
["cached!"]
|
59
|
+
]
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should read from cache for individual groups" do
|
64
|
+
@client.exec(
|
65
|
+
:some_binary => {
|
66
|
+
:args => 'preliminary_arg',
|
67
|
+
:groups => [
|
68
|
+
{
|
69
|
+
:args => 'arg2a arg2b',
|
70
|
+
:ttl => 60
|
71
|
+
}
|
72
|
+
]
|
73
|
+
}
|
74
|
+
).should == {
|
75
|
+
"some_binary" => [
|
76
|
+
["cached!"]
|
77
|
+
]
|
78
|
+
}
|
79
|
+
|
80
|
+
@client.exec(
|
81
|
+
:some_binary => {
|
82
|
+
:args => 'preliminary_arg',
|
83
|
+
:groups => [
|
84
|
+
{
|
85
|
+
:args => 'arg1a arg1b',
|
86
|
+
:ttl => 60
|
87
|
+
}
|
88
|
+
]
|
89
|
+
}
|
90
|
+
).should == {
|
91
|
+
"some_binary" => [
|
92
|
+
["cached!"]
|
93
|
+
]
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should not read cache if preliminary arg changes" do
|
98
|
+
@client.exec(
|
99
|
+
:some_binary => {
|
100
|
+
:args => 'preliminary_arg2',
|
101
|
+
:groups => [
|
102
|
+
{
|
103
|
+
:args => 'arg2a arg2b',
|
104
|
+
:ttl => 60
|
105
|
+
}
|
106
|
+
]
|
107
|
+
}
|
108
|
+
).should == {
|
109
|
+
"some_binary" => [
|
110
|
+
["arg1_result_1", "arg1_result_2"]
|
111
|
+
]
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should still read from original cache" do
|
116
|
+
client_exec.should == {
|
117
|
+
"some_binary" => [
|
118
|
+
["cached!"],
|
119
|
+
["cached!"]
|
120
|
+
]
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: execache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Winton Welsh
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-11 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70093750276540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70093750276540
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: redis
|
27
|
+
requirement: &70093750276060 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.2.2
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70093750276060
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: yajl-ruby
|
38
|
+
requirement: &70093750275600 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.0.0
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70093750275600
|
47
|
+
description: Run commands in parallel and cache the output. Redis queues jobs and
|
48
|
+
stores the result.
|
49
|
+
email:
|
50
|
+
- mail@wintoni.us
|
51
|
+
executables:
|
52
|
+
- execache
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- .gitignore
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- bin/execache
|
62
|
+
- execache.gemspec
|
63
|
+
- lib/execache.rb
|
64
|
+
- lib/execache/client.rb
|
65
|
+
- spec/execache_spec.rb
|
66
|
+
- spec/fixtures/execache.yml
|
67
|
+
- spec/fixtures/fixture.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
homepage: http://github.com/winton/execache
|
70
|
+
licenses: []
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.6
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Run commands in parallel and cache the output, controlled by Redis
|
93
|
+
test_files:
|
94
|
+
- spec/execache_spec.rb
|
95
|
+
- spec/fixtures/execache.yml
|
96
|
+
- spec/fixtures/fixture.rb
|
97
|
+
- spec/spec_helper.rb
|