swift 1.0.3 → 1.1.0

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.
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: