swift 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -303,6 +303,26 @@ or use the `em-synchrony` api for `swift`
303
303
  end
304
304
  ```
305
305
 
306
+ ### Fibers and Connection Pools
307
+
308
+ If you intend to use `Swift::Record` with `em-synchrony` you will need to use a fiber aware connection pool.
309
+
310
+ ```ruby
311
+ require 'swift/fiber_connection_pool'
312
+
313
+ EM.run do
314
+ Swift.setup(:default) do
315
+ Swift::FiberConnectionPool.new(size: 5) {Swift::Adapter::Postgres.new(db: 'swift')}
316
+ end
317
+
318
+ 5.times do
319
+ EM.synchrony do
320
+ p User.execute("select * from users").entries
321
+ end
322
+ end
323
+ end
324
+ ```
325
+
306
326
  ## Performance
307
327
 
308
328
  Swift prefers performance when it doesn't compromise the Ruby-ish interface. It's unfair to compare Swift to DataMapper
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.3
1
+ 1.1.0
@@ -67,11 +67,15 @@ module Swift
67
67
  # @return [Swift::Adapter]
68
68
  #
69
69
  # @see Swift::Adapter
70
- def setup name, type, options = {}
71
- unless type.kind_of?(Class) && type < Swift::Adapter
72
- raise TypeError, "Expected +type+ Swift::Adapter subclass but got #{type.inspect}"
70
+ def setup name, type = nil, options = {}
71
+ if block_given?
72
+ repositories[name] = yield
73
+ else
74
+ unless type.kind_of?(Class) && type < Swift::Adapter
75
+ raise TypeError, "Expected +type+ Swift::Adapter subclass but got #{type.inspect}"
76
+ end
77
+ repositories[name] = type.new(options)
73
78
  end
74
- (@repositories ||= {})[name] = type.new(options)
75
79
  end
76
80
 
77
81
  # Fetch or scope a block to a specific DB by name.
@@ -90,14 +94,9 @@ module Swift
90
94
  # @param [Symbol] name Adapter name.
91
95
  # @param [Proc] block Scope this block to the named adapter instead of <tt>:default</tt>.
92
96
  # @return [Swift::Adapter]
93
- #--
94
- # I pilfered the logic from DM but I don't really understand what is/isn't thread safe.
95
97
  def db name = nil, &block
96
- repository = if name || scopes.empty?
97
- @repositories[name || :default] or raise "Unknown db '#{name || :default}', did you forget to #setup ?"
98
- else
99
- scopes.last
100
- end
98
+ repository = name || scopes.size < 1 ? repositories[name ||= :default] : scopes.last
99
+ repository or raise "Unknown db '#{name}', did you forget to #setup ?"
101
100
 
102
101
  if block_given?
103
102
  begin
@@ -119,10 +118,22 @@ module Swift
119
118
  @schema ||= []
120
119
  end
121
120
 
121
+ # Trace the command execution in currently scoped adapter
122
+ #
123
+ # @example
124
+ # Swift.trace { Swift.db.execute("select * from users") }
125
+ #
126
+ # @see Swift::Adapter#trace
122
127
  def trace io = $stdout, &block
123
128
  Swift.db.trace(io, &block)
124
129
  end
125
130
 
131
+ # @private
132
+ def repositories
133
+ @repositories ||= {}
134
+ end
135
+
136
+ # @private
126
137
  def scopes
127
138
  Thread.current[:swift_db] ||= []
128
139
  end
@@ -219,8 +219,7 @@ module Swift
219
219
  log_command(start, command, bind) if @trace
220
220
  end
221
221
 
222
- private
223
-
222
+ # :nodoc:
224
223
  def log_command start, command, bind
225
224
  @trace.print Time.now.strftime('%F %T.%N'), ' - %.9f' % (Time.now - start).to_f, ' - ', command
226
225
  @trace.print ' ', bind if bind && bind.size > 0
@@ -10,6 +10,7 @@ module Swift
10
10
 
11
11
  class EMHandler < EM::Connection
12
12
  def initialize adapter, record, defer
13
+ @started = Time.now
13
14
  @adapter = adapter
14
15
  @record = record
15
16
  @defer = defer
@@ -17,7 +18,9 @@ module Swift
17
18
 
18
19
  def notify_readable
19
20
  detach
20
- @adapter.pending.shift
21
+ start, command, bind = @adapter.pending.shift
22
+ @adapter.log_command(start, command, bind) if @adapter.trace?
23
+
21
24
  begin
22
25
  @defer.succeed(@record ? Result.new(@record, @adapter.result) : @adapter.result)
23
26
  rescue Exception => e
@@ -41,9 +44,9 @@ module Swift
41
44
  def execute command, *bind
42
45
  raise RuntimeError, 'Command already in progress' unless pending.empty?
43
46
 
44
- pending << command
45
- start = Time.now
47
+ start = Time.now
46
48
  record, command = command, bind.shift if command.kind_of?(Class) && command < Record
49
+ pending << [start, command, bind]
47
50
  query(command, *bind)
48
51
 
49
52
  EM::DefaultDeferrable.new.tap do |defer|
@@ -0,0 +1,94 @@
1
+ # Based on EM::Synchrony::ConnectionPool
2
+
3
+ require 'swift/synchrony'
4
+
5
+ module Swift
6
+ class FiberConnectionPool
7
+
8
+ def initialize opts, &block
9
+ @reserved = {} # map of in-progress connections
10
+ @available = [] # pool of free connections
11
+ @pending = [] # pending reservations (FIFO)
12
+ @trace = nil
13
+
14
+ opts[:size].times do
15
+ @available.push(block.call)
16
+ end
17
+ end
18
+
19
+ def trace io = $stdout
20
+ if block_given?
21
+ begin
22
+ _io, @trace = @trace, io
23
+ yield
24
+ ensure
25
+ @trace = _io
26
+ end
27
+ else
28
+ @trace = io
29
+ end
30
+ end
31
+
32
+ private
33
+ # Choose first available connection and pass it to the supplied
34
+ # block. This will block indefinitely until there is an available
35
+ # connection to service the request.
36
+ def __reserve__
37
+ id = "#{Fiber.current.object_id}:#{rand}"
38
+ fiber = Fiber.current
39
+ begin
40
+ yield acquire(id, fiber)
41
+ ensure
42
+ release(id)
43
+ end
44
+ end
45
+
46
+ # Acquire a lock on a connection and assign it to executing fiber
47
+ # - if connection is available, pass it back to the calling block
48
+ # - if pool is full, yield the current fiber until connection is available
49
+ def acquire id, fiber
50
+ if conn = @available.pop
51
+ @reserved[id] = conn
52
+ else
53
+ Fiber.yield @pending.push(fiber)
54
+ acquire(id, fiber)
55
+ end
56
+ end
57
+
58
+ # Release connection assigned to the supplied fiber and
59
+ # resume any other pending connections (which will
60
+ # immediately try to run acquire on the pool)
61
+ def release(id)
62
+ @available.push(@reserved.delete(id))
63
+ if pending = @pending.shift
64
+ pending.resume
65
+ end
66
+ end
67
+
68
+ # Allow the pool to behave as the underlying connection
69
+ def method_missing method, *args, &blk
70
+ __reserve__ do |conn|
71
+ if @trace
72
+ conn.trace(@trace) {conn.__send__(method, *args, &blk)}
73
+ else
74
+ conn.__send__(method, *args, &blk)
75
+ end
76
+ end
77
+ end
78
+ end # FiberConnectionPool
79
+
80
+ class Adapter::Sql
81
+ def serialized_transaction &block
82
+ Swift.scopes.push(self)
83
+ execute('begin')
84
+ res = yield(self)
85
+ execute('commit')
86
+ res
87
+ rescue => e
88
+ execute('rollback')
89
+ raise e
90
+ ensure
91
+ Swift.scopes.pop
92
+ end
93
+ end # Adapter::Sql
94
+ end # Swift
@@ -27,7 +27,10 @@ module Swift
27
27
  # @see [Swift::Adapter]
28
28
  def execute *args
29
29
  res = EM::Synchrony.sync aexecute(*args)
30
- raise res if res.kind_of?(Error)
30
+ if res.kind_of?(Error)
31
+ res.set_backtrace caller.reject {|subject| subject =~ %r{swift/fiber_connection_pool}}
32
+ raise res
33
+ end
31
34
  yield res if block_given?
32
35
  res
33
36
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "swift"
8
- s.version = "1.0.3"
8
+ s.version = "1.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Shane Hanna", "Bharanee 'Barney' Rathna"]
12
- s.date = "2012-09-21"
12
+ s.date = "2013-02-18"
13
13
  s.description = "A rational rudimentary database abstraction."
14
14
  s.email = ["shane.hanna@gmail.com", "deepfryed@gmail.com"]
15
15
  s.extra_rdoc_files = [
@@ -30,6 +30,7 @@ Gem::Specification.new do |s|
30
30
  "lib/swift/adapter/sqlite3.rb",
31
31
  "lib/swift/attribute.rb",
32
32
  "lib/swift/eventmachine.rb",
33
+ "lib/swift/fiber_connection_pool.rb",
33
34
  "lib/swift/header.rb",
34
35
  "lib/swift/identity_map.rb",
35
36
  "lib/swift/migrations.rb",
@@ -52,6 +53,7 @@ Gem::Specification.new do |s|
52
53
  "test/test_io.rb",
53
54
  "test/test_record.rb",
54
55
  "test/test_swift.rb",
56
+ "test/test_synchrony.rb",
55
57
  "test/test_timestamps.rb",
56
58
  "test/test_transactions.rb",
57
59
  "test/test_types.rb",
@@ -0,0 +1,58 @@
1
+ require 'helper'
2
+
3
+ describe 'fiber connection pool' do
4
+ before do
5
+ skip 'swift/synchrony re-defines Adapter#execute' unless ENV['TEST_SWIFT_SYNCHRONY']
6
+
7
+ require 'swift/fiber_connection_pool'
8
+ EM.synchrony do
9
+ Swift.setup(:default, Swift::Adapter::Postgres, db: 'swift_test')
10
+ Swift.db.execute('drop table if exists users')
11
+ Swift.db.execute('create table users(id serial primary key, name text)')
12
+
13
+ @user = Class.new(Swift::Record) do
14
+ store :users
15
+ attribute :id, Swift::Type::Integer, key: true, serial: true
16
+ attribute :name, Swift::Type::String
17
+ end
18
+
19
+ 10.times { @user.create(name: 'test') }
20
+ EM.stop
21
+ end
22
+ end
23
+
24
+ it 'can synchronize queries across fibers' do
25
+ EM.run do
26
+ Swift.setup(:default) { Swift::FiberConnectionPool.new(size: 2) {Swift::Adapter::Postgres.new(db: 'swift_test')}}
27
+
28
+ @counts = []
29
+ 5.times do
30
+ EM.synchrony do
31
+ @counts << @user.execute('select * from users').selected_rows
32
+ end
33
+ end
34
+
35
+ EM.add_timer(0.2) { EM.stop }
36
+ end
37
+
38
+ assert_equal 5, @counts.size
39
+ assert_equal [10], @counts.uniq
40
+ end
41
+
42
+ it 'sets appropriate backtrace for errors' do
43
+ EM.synchrony do
44
+ error = nil
45
+ Swift.setup(:default) { Swift::FiberConnectionPool.new(size: 2) {Swift::Adapter::Postgres.new(db: 'swift_test')}}
46
+
47
+ begin
48
+ Swift.db.execute 'foo bar baz'
49
+ rescue => e
50
+ error = e
51
+ end
52
+
53
+ assert error
54
+ assert_match %r{test/test_synchrony.rb:48}, error.backtrace.first
55
+ EM.stop
56
+ end
57
+ end
58
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swift
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,24 +10,24 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-09-21 00:00:00.000000000 Z
13
+ date: 2013-02-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
- name: minitest
17
16
  requirement: !ruby/object:Gem::Requirement
18
- none: false
19
17
  requirements:
20
18
  - - ! '>='
21
19
  - !ruby/object:Gem::Version
22
20
  version: 1.7.0
23
- type: :development
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
21
  none: false
22
+ version_requirements: !ruby/object:Gem::Requirement
27
23
  requirements:
28
24
  - - ! '>='
29
25
  - !ruby/object:Gem::Version
30
26
  version: 1.7.0
27
+ none: false
28
+ name: minitest
29
+ prerelease: false
30
+ type: :development
31
31
  description: A rational rudimentary database abstraction.
32
32
  email:
33
33
  - shane.hanna@gmail.com
@@ -51,6 +51,7 @@ files:
51
51
  - lib/swift/adapter/sqlite3.rb
52
52
  - lib/swift/attribute.rb
53
53
  - lib/swift/eventmachine.rb
54
+ - lib/swift/fiber_connection_pool.rb
54
55
  - lib/swift/header.rb
55
56
  - lib/swift/identity_map.rb
56
57
  - lib/swift/migrations.rb
@@ -73,6 +74,7 @@ files:
73
74
  - test/test_io.rb
74
75
  - test/test_record.rb
75
76
  - test/test_swift.rb
77
+ - test/test_synchrony.rb
76
78
  - test/test_timestamps.rb
77
79
  - test/test_transactions.rb
78
80
  - test/test_types.rb
@@ -84,17 +86,20 @@ rdoc_options: []
84
86
  require_paths:
85
87
  - lib
86
88
  required_ruby_version: !ruby/object:Gem::Requirement
87
- none: false
88
89
  requirements:
89
90
  - - ! '>='
90
91
  - !ruby/object:Gem::Version
92
+ hash: -3874151873791212661
91
93
  version: '0'
92
- required_rubygems_version: !ruby/object:Gem::Requirement
94
+ segments:
95
+ - 0
93
96
  none: false
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
98
  requirements:
95
99
  - - ! '>='
96
100
  - !ruby/object:Gem::Version
97
101
  version: '0'
102
+ none: false
98
103
  requirements: []
99
104
  rubyforge_project:
100
105
  rubygems_version: 1.8.24
@@ -102,4 +107,3 @@ signing_key:
102
107
  specification_version: 3
103
108
  summary: A rational rudimentary database abstraction.
104
109
  test_files: []
105
- has_rdoc: