drbarpg 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 904cd118d012de004d15e6c521f20fa962cad506
4
+ data.tar.gz: c4f985153e0ffd0c63362d3eebe4c5c8e168ee84
5
+ SHA512:
6
+ metadata.gz: 06d4ffe76e5ab4baef966ea23f12fb3b077fdf4e90eb7d746f498c28b437a28b953aeb45312c9c692ce067e19981f092ab44a8263b0b43003cda4fa9fb108fec
7
+ data.tar.gz: 578100b75206eb8ae4af1daa1a244824ae4af968f00b76d368be1fda9c98cd3d2f3e861edf21ec7bdcad1439770bcdad5cfdf190c4db31fb1c34565daabb7893
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Lars Kanis <lars@greiz-reinsdorf.de>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,64 @@
1
+ = drbarpg
2
+ {<img src="https://travis-ci.org/larskanis/drbarpg.png?branch=master" alt="Build Status" />}[https://travis-ci.org/larskanis/drbarpg]
3
+
4
+ A protocol implementation for Distributed Ruby (DRb), supporting connections
5
+ via a PostgreSQL server by using the LISTEN/NOTIFY event system.
6
+
7
+ This project rocks and uses MIT-LICENSE.
8
+
9
+ == Usage
10
+ Add to your +Gemfile+ :
11
+ gem 'drbarpg'
12
+
13
+ Run
14
+ $ bundle install
15
+ $ rake drbarpg:install:migrations
16
+ $ rake db:migrate
17
+
18
+ Ensure the value of +pool+ in your +config/database.yml+ is high enough (at least 10).
19
+
20
+ === Example server
21
+ # Startup Rails environment
22
+ require 'config/environment'
23
+
24
+ # start up the DRb service
25
+ DRb.start_service "drbarpg://myserver", []
26
+
27
+ # wait for the DRb service to finish before exiting
28
+ DRb.thread.join
29
+
30
+ === Example client:
31
+ require 'config/environment'
32
+
33
+ # Start a local DRbServer to handle callbacks.
34
+ #
35
+ # Not necessary for this small example, but will be required
36
+ # as soon as we pass a non-marshallable object as an argument
37
+ # to a dRuby call.
38
+ DRb.start_service "drbarpg://"
39
+
40
+ # attach to the DRb server via the server URI
41
+ remote_array = DRbObject.new_with_uri("drbarpg://myserver")
42
+
43
+ p remote_array.size # => 0
44
+
45
+ remote_array << 1
46
+
47
+ p remote_array.size # => 1
48
+
49
+ == Description
50
+
51
+ drbarpg makes it possible to create DRb-connections via the connection to a PostgreSQL server.
52
+ It easily integrates into rails projects and uses the configured database connection for
53
+ inter process communication.
54
+
55
+ Method calls with a size of less than 8000 bytes for all serialized parameters are directly
56
+ passed through by the NOTIFY payload. This bypasses the transaction overhead in the database,
57
+ so the calls are reasonably fast (approximately 1.5 of the time of a direct DRb-TCP connection).
58
+ Calls with bigger parameter size are transferred by INSERT/SELECT and require transaction
59
+ commits, so they usually take several milliseconds.
60
+
61
+ == Requirements
62
+
63
+ * PostgreSQL 9.0+
64
+ * Rails 3.0+
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Drbarpg'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,4 @@
1
+ module Drbarpg
2
+ class Message < ActiveRecord::Base
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ class CreateDrbarpgDrbarpgMessages < ActiveRecord::Migration
2
+ def change
3
+ create_table :drbarpg_messages do |t|
4
+ t.references "drbarpg_connection"
5
+ t.text "payload"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class CreateDrbarpgDrbarpgConnections < ActiveRecord::Migration
2
+ def change
3
+ create_table :drbarpg_connections do |t|
4
+ t.text "listen_channel"
5
+ end
6
+ add_index :drbarpg_connections, "listen_channel", :unique => true
7
+ end
8
+ end
data/lib/drb/ar_pg.rb ADDED
@@ -0,0 +1,183 @@
1
+ require 'drb/drb'
2
+ require 'stringio'
3
+
4
+ module DRb
5
+
6
+ # Implements DRb over an ActiveRecord connection to a PostgreSQL server
7
+ #
8
+ # DRb PostgreSQL socket URIs look like <code>drbarpg:?<option></code>. The
9
+ # option is optional.
10
+
11
+ class DRbArPg
12
+ attr_reader :uri
13
+
14
+ def self.open(uri, config)
15
+ server_channel, = parse_uri(uri)
16
+
17
+ self.new(uri, nil, server_channel, :master, config)
18
+ end
19
+
20
+ def self.open_server(uri, config)
21
+ channel, = parse_uri(uri)
22
+ self.new(uri, channel, nil, :server, config)
23
+ end
24
+
25
+ def accept
26
+ @pgconn.wait_for_notify do |channel, pid, payload|
27
+ return self.class.new(nil, nil, payload, :slave, @config)
28
+ end
29
+ end
30
+
31
+ def self.uri_option(uri, config)
32
+ channel, option = parse_uri(uri)
33
+ return "drbarpg://#{channel}", option
34
+ end
35
+
36
+ def initialize(uri, listen_channel, notify_connection_id, act_as, config={})
37
+ @notify_connection_id = notify_connection_id
38
+ @config = config
39
+ @msg = DRbMessage.new(@config)
40
+ @conn = Drbarpg::Message.connection_pool.checkout
41
+ @pgconn = @conn.raw_connection
42
+
43
+ @listen_connection_id = @conn.exec_query("SELECT nextval('drbarpg_connections_id_seq') AS seq").first['seq']
44
+ @listen_channel = if listen_channel.nil? || listen_channel.empty?
45
+ @conn.quote_table_name("drbarpg__#{@listen_connection_id}")
46
+ else
47
+ @conn.quote_table_name("drbarpg_#{listen_channel}")
48
+ end
49
+
50
+ @conn.execute("LISTEN #{@listen_channel}");
51
+
52
+ case act_as
53
+ when :server
54
+ # Save the connection to the database to ensure that only one server can listen
55
+ # on a given channel.
56
+ @conn.exec_insert('INSERT INTO drbarpg_connections (id, listen_channel)
57
+ VALUES ($1::int, $2::text)', nil,
58
+ [[nil, @listen_connection_id], [nil, @listen_channel]])
59
+
60
+ uri_channel, uri_option = self.class.parse_uri(uri) if uri
61
+ uri = "drbarpg://_#{@listen_connection_id}?#{uri_option}" if uri_channel.nil? || uri_channel.empty?
62
+ @uri = uri
63
+ when :master
64
+ # connect to server channel
65
+ @conn.exec_update("NOTIFY #{@conn.quote_table_name("drbarpg_#{@notify_connection_id}")}, #{@conn.quote(@listen_connection_id)}");
66
+ # wait for acknowledgement with peer channel
67
+ @pgconn.wait_for_notify do |channel, pid, payload|
68
+ @notify_connection_id = payload
69
+ @notify_channel = @conn.quote_table_name("drbarpg__#{@notify_connection_id}")
70
+ end
71
+ when :slave
72
+ @notify_channel = @conn.quote_table_name("drbarpg__#{@notify_connection_id}")
73
+ # acknowledge with peer channel
74
+ @conn.exec_update("NOTIFY #{@notify_channel}, #{@conn.quote(@listen_connection_id)}");
75
+ end
76
+ end
77
+
78
+ def close
79
+ if @conn
80
+ if @notify_channel
81
+ @conn.exec_update("NOTIFY #{@notify_channel}, 'c'");
82
+ end
83
+ @conn.execute("UNLISTEN #{@listen_channel}");
84
+ @conn.exec_delete('DELETE FROM drbarpg_connections WHERE id=$1::int', nil, [[nil, @listen_connection_id]])
85
+ Drbarpg::Message.connection_pool.checkin(@conn)
86
+ @conn = @pgconn = nil
87
+ end
88
+ end
89
+
90
+ def alive?
91
+ @pgconn.consume_input
92
+ if (n=@pgconn.notifies)
93
+ if n[:extra] == 'c'
94
+ close
95
+ else
96
+ raise DRbConnError, "received unexpected notification"
97
+ end
98
+ end
99
+ !!@conn
100
+ end
101
+
102
+ def send_request(ref, msg_id, *arg, &b)
103
+ stream = StringIO.new
104
+ @msg.send_request(stream, ref, msg_id, *arg, &b)
105
+ send_message(stream.string)
106
+ end
107
+
108
+ def recv_reply
109
+ stream = StringIO.new(wait_for_message)
110
+ return @msg.recv_reply(stream)
111
+ end
112
+
113
+ def recv_request
114
+ begin
115
+ stream = StringIO.new(wait_for_message)
116
+ return @msg.recv_request(stream)
117
+ rescue
118
+ close
119
+ raise $!
120
+ end
121
+ end
122
+
123
+ def send_reply(succ, result)
124
+ begin
125
+ stream = StringIO.new
126
+ @msg.send_reply(stream, succ, result)
127
+ send_message(stream.string)
128
+ rescue
129
+ close
130
+ raise $!
131
+ end
132
+ end
133
+
134
+ private
135
+ def send_message(payload)
136
+ payload_b64 = [payload].pack('m')
137
+ if payload_b64.length >= 8000
138
+ @conn.exec_insert('INSERT INTO drbarpg_messages (drbarpg_connection_id, payload)
139
+ VALUES ($1::int, $2::text)', nil, [[nil, @notify_connection_id], [nil, payload_b64]])
140
+ @conn.exec_update("NOTIFY #{@notify_channel}");
141
+ else
142
+ @conn.exec_update("NOTIFY #{@notify_channel}, '#{payload_b64}'");
143
+ end
144
+ end
145
+
146
+ def check_pending_message
147
+ row = @conn.exec_query('SELECT id, payload FROM drbarpg_messages WHERE drbarpg_connection_id=$1::int LIMIT 1',
148
+ nil, [ [nil, @listen_connection_id] ]).first
149
+ if row
150
+ @conn.exec_delete('DELETE FROM drbarpg_messages WHERE id=$1::int', nil, [[nil, row['id']]])
151
+ return row['payload'].unpack('m')[0]
152
+ else
153
+ raise DRbConnError, "no message received"
154
+ end
155
+ end
156
+
157
+ def wait_for_message()
158
+ @pgconn.wait_for_notify do |channel, pid, payload|
159
+ if payload=='c'
160
+ close
161
+ return ''
162
+ elsif payload
163
+ return payload.unpack('m')[0]
164
+ else
165
+ return check_pending_message
166
+ end
167
+ end
168
+ end
169
+
170
+ def self.parse_uri(uri)
171
+ if /^drbarpg:\/\/(.*?)(\?(.*))?$/ =~ uri
172
+ channel = $1
173
+ option = $3
174
+ [channel, option]
175
+ else
176
+ raise(DRbBadScheme, uri) unless uri =~ /^drbarpg:/
177
+ raise(DRbBadURI, 'can\'t parse uri:' + uri)
178
+ end
179
+ end
180
+ end
181
+
182
+ DRbProtocol.add_protocol(DRbArPg)
183
+ end
@@ -0,0 +1,5 @@
1
+ module Drbarpg
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Drbarpg
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Drbarpg
2
+ VERSION = "0.0.1"
3
+ end
data/lib/drbarpg.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'drbarpg/engine'
2
+ require 'drb/ar_pg'
3
+
4
+ module Drbarpg
5
+ end
@@ -0,0 +1,84 @@
1
+ require 'test_helper'
2
+ require 'drb/ar_pg'
3
+ require 'logger'
4
+ require 'rbconfig'
5
+
6
+ class DrbarpgTest < ActiveSupport::TestCase
7
+ self.use_transactional_fixtures = false
8
+
9
+ attr_reader :drb
10
+
11
+ UriServer = "drbarpg://server"
12
+ UriCallback = "drbarpg://"
13
+ # UriServer = "druby://localhost:33333"
14
+ # UriCallback = nil
15
+
16
+ def setup
17
+ # ActiveRecord::Base.logger = Logger.new STDERR
18
+
19
+ # Start forked server process
20
+ # ActiveRecord::Base.connection_pool.disconnect!
21
+ # rd, wr = IO.pipe
22
+ # fork do
23
+ # DRb.start_service(UriServer, Kernel)
24
+ # wr.puts "started"
25
+ # DRb.thread.join
26
+ # end
27
+
28
+ rd = IO.popen(RbConfig::CONFIG['ruby_install_name'], 'w+')
29
+ rd.write <<-EOT
30
+ $: << #{File.expand_path('..', __FILE__).inspect}
31
+ require 'test_helper'
32
+ require 'drb/ar_pg'
33
+
34
+ # ActiveRecord::Base.logger = Logger.new $stderr
35
+ DRb.start_service(#{UriServer.inspect}, Kernel)
36
+ $stdout.puts "started"
37
+ $stdout.flush
38
+ $stdout.reopen($stderr)
39
+ DRb.thread.join
40
+ EOT
41
+ rd.close_write
42
+
43
+ # Start local server for callbacks
44
+ DRb.start_service(UriCallback)
45
+
46
+ # Wait until server process has started
47
+ rd.gets
48
+
49
+ # Connect to server object
50
+ @drb = DRbObject.new_with_uri(UriServer)
51
+ end
52
+
53
+ def teardown
54
+ drb.eval("DRb.stop_service")
55
+ # Wait until server process has stopped
56
+ Process.wait
57
+ DRb.stop_service
58
+ end
59
+
60
+ test "allows two-way communication" do
61
+ remote_hash = drb.eval("@a = {}.extend(DRb::DRbUndumped)")
62
+
63
+ # Set local Proc in remote hash, re-fetch it and call it. Works without two-way comms
64
+ remote_hash["a"] = lambda { return 1 }
65
+ assert_equal 1, drb.eval("@a['a']").call
66
+
67
+ # Call same Proc in a remote context. This means establishing a connection to the local side, since
68
+ # the Proc is really a DRbObject on the remote side.
69
+ assert_equal 1, drb.eval("@a['a'].call")
70
+ end
71
+
72
+ test "allows big params" do
73
+ remote_hash = drb.eval("@a = {}.extend(DRb::DRbUndumped)")
74
+
75
+ remote_hash["a"] = "b"*8000
76
+ assert_equal "b"*8000, drb.eval("@a['a']")
77
+ end
78
+
79
+ test "runs reasonable fast" do
80
+ 2000.times do
81
+ drb.rand
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Dummy::Application.load_tasks
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
3
+ require_relative '../config/boot'
4
+ require 'rails/commands'
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../config/boot'
3
+ require 'rake'
4
+ Rake.application.run
@@ -0,0 +1,23 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require 'rails/all'
4
+
5
+ Bundler.require(*Rails.groups)
6
+ require "drbarpg"
7
+
8
+ module Dummy
9
+ class Application < Rails::Application
10
+ # Settings in config/environments/* take precedence over those specified here.
11
+ # Application configuration should go into files in config/initializers
12
+ # -- all .rb files in that directory are automatically loaded.
13
+
14
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
15
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
16
+ # config.time_zone = 'Central Time (US & Canada)'
17
+
18
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
19
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
20
+ # config.i18n.default_locale = :de
21
+ end
22
+ end
23
+
@@ -0,0 +1,5 @@
1
+ # Set up gems listed in the Gemfile.
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
3
+
4
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
5
+ $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
@@ -0,0 +1,60 @@
1
+ # PostgreSQL. Versions 8.2 and up are supported.
2
+ #
3
+ # Install the pg driver:
4
+ # gem install pg
5
+ # On OS X with Homebrew:
6
+ # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7
+ # On OS X with MacPorts:
8
+ # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9
+ # On Windows:
10
+ # gem install pg
11
+ # Choose the win32 build.
12
+ # Install PostgreSQL and put its /bin directory on your path.
13
+ #
14
+ # Configure Using Gemfile
15
+ # gem 'pg'
16
+ #
17
+ development:
18
+ adapter: postgresql
19
+ encoding: unicode
20
+ database: drbarpg_development
21
+ pool: 50
22
+ # username: drbarpg
23
+ # password:
24
+
25
+ # Connect on a TCP socket. Omitted by default since the client uses a
26
+ # domain socket that doesn't need configuration. Windows does not have
27
+ # domain sockets, so uncomment these lines.
28
+ #host: localhost
29
+
30
+ # The TCP port the server listens on. Defaults to 5432.
31
+ # If your server runs on a different port number, change accordingly.
32
+ #port: 5432
33
+
34
+ # Schema search path. The server defaults to $user,public
35
+ #schema_search_path: myapp,sharedapp,public
36
+
37
+ # Minimum log levels, in increasing order:
38
+ # debug5, debug4, debug3, debug2, debug1,
39
+ # log, notice, warning, error, fatal, and panic
40
+ # Defaults to warning.
41
+ #min_messages: notice
42
+
43
+ # Warning: The database defined as "test" will be erased and
44
+ # re-generated from your development database when you run "rake".
45
+ # Do not set this db to the same as development or production.
46
+ test:
47
+ adapter: postgresql
48
+ encoding: unicode
49
+ database: drbarpg_test
50
+ pool: 50
51
+ # username: drbarpg
52
+ # password:
53
+
54
+ production:
55
+ adapter: postgresql
56
+ encoding: unicode
57
+ database: drbarpg_production
58
+ pool: 50
59
+ # username: drbarpg
60
+ # password:
@@ -0,0 +1,5 @@
1
+ # Load the rails application.
2
+ require File.expand_path('../application', __FILE__)
3
+
4
+ # Initialize the rails application.
5
+ Dummy::Application.initialize!
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended to check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(:version => 20130601144555) do
15
+
16
+ create_table "drbarpg_connections", :force => true do |t|
17
+ t.text "listen_channel"
18
+ end
19
+
20
+ add_index "drbarpg_connections", ["listen_channel"], :name => "index_drbarpg_connections_on_listen_channel", :unique => true
21
+
22
+ create_table "drbarpg_messages", :force => true do |t|
23
+ t.integer "drbarpg_connection_id"
24
+ t.text "payload"
25
+ end
26
+
27
+ end