execache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg
6
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
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.
@@ -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
+ }
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path("../../lib/execache", __FILE__)
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ redis: localhost:6379/0
2
+ some_binary:
3
+ command: 'ruby spec/fixtures/fixture.rb'
4
+ separators:
5
+ result: "\n"
6
+ group: "[END]"
@@ -0,0 +1,5 @@
1
+ puts "arg1_result_1
2
+ arg1_result_2
3
+ [END]
4
+ arg2_result_1
5
+ arg2_result_2"
@@ -0,0 +1,9 @@
1
+ require "pp"
2
+ require "bundler"
3
+
4
+ Bundler.require(:default)
5
+ Bundler.require(:development)
6
+
7
+ $root = File.expand_path('../../', __FILE__)
8
+
9
+ require "#{$root}/lib/execache"
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