nobrainer 0.29.0 → 0.30.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9fbf539d3fd47eeec38c4ae88843297a7462efb5
4
- data.tar.gz: e02bde6490ffc0eb21bf3fde637dc6a4dea0350d
3
+ metadata.gz: ebe188d50e06d6ef907cdf043560143e3bd7d703
4
+ data.tar.gz: 15b1580f27a66b6670386cc117b4767f930e67c7
5
5
  SHA512:
6
- metadata.gz: 586f87afabedec2bc9716949298255c6456b7cbf9846a6b9e3090fe009cf481d68fd3eb852368a2658f1e4b59e432388f690cd2fce24606460229dbad1daf633
7
- data.tar.gz: d55889b76d3a1e6dd48338fca51eb1ad636baa00bc20c21a992cf8e5e6d02a4f958f535dc95398a89c3695ceab87cf5ededbd915ac3b0f07f041dda16148e913
6
+ metadata.gz: ff9b387f2045109c33efb88d36273325edf34573acb134bbb94d3d2e04c1efd6fdf65b761739eac61c185309cfa9deba5a07987fdf56becf3d78308b300838b9
7
+ data.tar.gz: acb0f33b44d27ac25599352b0d568dd563e13a81da91b0dfef6cf72596a1500026e739f2cc64c63e22edc72ddb57ee3b61a70acf72c139cd502f0b1d95e78c9b
@@ -2,8 +2,9 @@ require 'rethinkdb'
2
2
 
3
3
  class NoBrainer::Criteria
4
4
  extend NoBrainer::Autoload
5
- autoload_and_include :Core, :Run, :Raw, :Scope, :AfterFind, :Where, :OrderBy,
6
- :Limit, :Pluck, :Count, :Delete, :Enumerable, :Find,
7
- :First, :FirstOrCreate, :Changes, :Aggregate, :EagerLoad,
8
- :Update, :Cache, :Index, :Extend, :Join
5
+ autoload_and_include :Core, :Run, :Raw, :VirtualAttributes, :Scope,
6
+ :AfterFind, :Where, :OrderBy, :Limit, :Pluck, :Count,
7
+ :Delete, :Enumerable, :Find, :First, :FirstOrCreate,
8
+ :Changes, :Aggregate, :EagerLoad, :Update, :Cache,
9
+ :Index, :Extend, :Join
9
10
  end
@@ -38,7 +38,11 @@ module NoBrainer::Criteria::FirstOrCreate
38
38
 
39
39
  def _first_or_create(create_params, save_options, &block)
40
40
  raise "Cannot use .raw() with .first_or_create()" if raw?
41
- raise "Use first_or_create() on the root class `#{model.root_class}'" unless model.is_root_class?
41
+
42
+ if block && block.arity == 1
43
+ raise "When passing a block to first_or_create(), you must pass a block with no arguments.\n" +
44
+ "The passed block must return a hash of additional attributes for create()"
45
+ end
42
46
 
43
47
  save_method = save_options.delete(:save_method)
44
48
  should_update = save_options.delete(:update)
@@ -62,6 +66,25 @@ module NoBrainer::Criteria::FirstOrCreate
62
66
  "\nend"
63
67
  end
64
68
 
69
+ unless model.is_root_class? || (model.superclass.fields.keys & keys).empty?
70
+ # We can't allow the parent to share the keys we are matching on.
71
+ # Consider this case:
72
+ # - Base has the field :name, :uniq => true declared
73
+ # - A < Base
74
+ # - B < Base
75
+ # - A.create(:name => 'x'),
76
+ # - B.where(:name => 'x').first_or_create
77
+ # We are forced to return nil, or raise.
78
+ parent = model
79
+ parent = parent.superclass while parent.superclass < NoBrainer::Document &&
80
+ !(parent.superclass.fields.keys & keys).empty?
81
+ raise "A polymorphic problem has been detected: The fields `#{keys.inspect}' are defined on `#{parent}'.\n" +
82
+ "This is problematic as first_or_create() could return nil in some cases.\n" +
83
+ "Either 1) Only define `#{keys.inspect}' on `#{model}', \n" +
84
+ "or 2) Query the superclass, and pass :_type in first_or_create() as such:\n" +
85
+ " `#{parent}.where(...).first_or_create(:_type => \"#{model}\")'."
86
+ end
87
+
65
88
  # We don't want to access create_params yet, because invoking the block
66
89
  # might be costly (the user might be doing some API call or w/e), and
67
90
  # so we want to invoke the block only if necessary.
@@ -108,18 +131,22 @@ module NoBrainer::Criteria::FirstOrCreate
108
131
  where_clauses = finalized_criteria.options[:where_ast]
109
132
 
110
133
  unless where_clauses.is_a?(NoBrainer::Criteria::Where::MultiOperator) &&
111
- where_clauses.op == :and && where_clauses.clauses.size > 0 &&
112
- where_clauses.clauses.all? do |c|
113
- c.is_a?(NoBrainer::Criteria::Where::BinaryOperator) &&
114
- c.op == :eq && c.key_modifier == :scalar
115
- end
134
+ where_clauses.op == :and
116
135
  raise "Please use a query of the form `.where(...).first_or_create(...)'"
117
136
  end
118
137
 
119
138
  Hash[where_clauses.clauses.map do |c|
139
+ unless c.is_a?(NoBrainer::Criteria::Where::BinaryOperator) &&
140
+ c.op == :eq && c.key_modifier == :scalar
141
+ # Ignore params on the subclass type, we are handling this case directly
142
+ # in _first_or_create()
143
+ next if c.key_path == [:_type]
144
+ raise "Please only use equal constraints in your where() query when using first_or_create()"
145
+ end
146
+
120
147
  raise "You may not use nested hash queries with first_or.create()" if c.key_path.size > 1
121
148
  [c.key_path.first.to_sym, c.value]
122
- end]
149
+ end.compact].tap { |h| raise "Missing where() clauses for first_or_create()" if h.empty? }
123
150
  end
124
151
 
125
152
  def get_model_unique_fields
@@ -53,9 +53,10 @@ module NoBrainer::Criteria::Join
53
53
  join_ast.reduce(super) do |rql, (association, criteria)|
54
54
  rql.concat_map do |doc|
55
55
  key = doc[association.eager_load_owner_key]
56
- criteria.where(association.eager_load_target_key => key).to_rql.map do |assoc_doc|
57
- doc.merge(association.target_name => assoc_doc)
58
- end
56
+ RethinkDB::RQL.new.branch(key.eq(nil), [],
57
+ criteria.where(association.eager_load_target_key => key).to_rql.map do |assoc_doc|
58
+ doc.merge(association.target_name => assoc_doc)
59
+ end)
59
60
  end
60
61
  end
61
62
  end
@@ -0,0 +1,18 @@
1
+ module NoBrainer::Criteria::VirtualAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ def compile_rql_pass2
5
+ rql = super
6
+
7
+ if model.virtual_fields
8
+ rql = rql.map do |_doc|
9
+ model.virtual_fields.reduce(_doc) do |doc, field|
10
+ field_rql = model.fields[field][:virtual].call(doc, RethinkDB::RQL.new)
11
+ field_rql.nil? ? doc : doc.merge(field => field_rql)
12
+ end
13
+ end
14
+ end
15
+
16
+ rql
17
+ end
18
+ end
@@ -195,11 +195,12 @@ module NoBrainer::Criteria::Where
195
195
  else
196
196
  # 1) Box value in array if we have an any/all modifier
197
197
  # 2) Box value in hash if we have a nested query.
198
- value = [value] if key_modifier.in?([:any, :all])
198
+ box_value = key_modifier.in?([:any, :all]) || op == :include
199
+ value = [value] if box_value
199
200
  value_hash = key_path.reverse.reduce(value) { |v,k| {k => v} }
200
201
  value = model.cast_user_to_db_for(*value_hash.first)
201
202
  value = key_path[1..-1].reduce(value) { |h,k| h[k] }
202
- value = value.first if key_modifier.in?([:any, :all])
203
+ value = value.first if box_value
203
204
  value
204
205
  end
205
206
  end
@@ -7,7 +7,7 @@ module NoBrainer::Document
7
7
  autoload_and_include :Core, :TableConfig, :InjectionLayer, :Attributes, :Readonly,
8
8
  :Persistance, :Callbacks, :Validation, :Types, :Dirty, :PrimaryKey,
9
9
  :Association, :Serialization, :Criteria, :Polymorphic, :Index, :Aliases,
10
- :MissingAttributes, :LazyFetch, :AtomicOps
10
+ :MissingAttributes, :LazyFetch, :AtomicOps, :VirtualAttributes
11
11
 
12
12
  autoload :DynamicAttributes, :Timestamps
13
13
 
@@ -2,7 +2,7 @@ module NoBrainer::Document::Attributes
2
2
  VALID_FIELD_OPTIONS = [:index, :default, :type, :readonly, :primary_key,
3
3
  :lazy_fetch, :store_as, :validates, :required, :unique,
4
4
  :uniq, :format, :in, :length, :min_length, :max_length,
5
- :prefix, :suffix]
5
+ :prefix, :suffix, :virtual]
6
6
  RESERVED_FIELD_NAMES = [:index, :default, :and, :or, :selector, :associations, :pk_value] +
7
7
  NoBrainer::SymbolDecoration::OPERATORS
8
8
 
@@ -15,6 +15,7 @@ module NoBrainer::Document::Index
15
15
  name = case name
16
16
  when String then name.to_sym
17
17
  when Symbol then name
18
+ when Array then args.unshift(name); name.map(&:to_s).join('_').to_sym
18
19
  else raise ArgumentError, "Incorrect index specification"
19
20
  end
20
21
 
@@ -18,18 +18,13 @@ module NoBrainer::Document::Persistance
18
18
  !new_record? && !destroyed?
19
19
  end
20
20
 
21
- def _reload_selector(options={})
22
- rql = selector
21
+ def _reload(options={})
22
+ criteria = root_class.raw
23
23
  if opt = options[:missing_attributes]
24
- rql = rql.pluck(self.class.with_fields_aliased(opt[:pluck])) if opt[:pluck]
25
- rql = rql.without(self.class.with_fields_aliased(opt[:without])) if opt[:without]
24
+ criteria = criteria.pluck(opt[:pluck]) if opt[:pluck]
25
+ criteria = criteria.without(opt[:without]) if opt[:without]
26
26
  end
27
- rql
28
- end
29
-
30
- def _reload(options={})
31
- attrs = NoBrainer.run { _reload_selector(options) }
32
- raise NoBrainer::Error::DocumentNotFound, "#{self.class} :#{self.class.pk_name}=>\"#{pk_value}\" not found" unless attrs
27
+ attrs = criteria.find(pk_value)
33
28
 
34
29
  options = options.merge(:pristine => true, :from_db => true)
35
30
 
@@ -60,7 +55,7 @@ module NoBrainer::Document::Persistance
60
55
  end
61
56
 
62
57
  def _create(options={})
63
- attrs = self.class.persistable_attributes(@_attributes, :instance => self)
58
+ attrs = self.class.persistable_attributes(@_attributes)
64
59
  result = NoBrainer.run(self.class.rql_table.insert(attrs))
65
60
  self.pk_value ||= result['generated_keys'].to_a.first
66
61
  @new_record = false
@@ -69,7 +64,7 @@ module NoBrainer::Document::Persistance
69
64
  end
70
65
 
71
66
  def _update(attrs)
72
- rql = ->(doc){ self.class.persistable_attributes(attrs, :instance => self, :rql_doc => doc) }
67
+ rql = ->(doc){ self.class.persistable_attributes(attrs, :rql_doc => doc) }
73
68
  NoBrainer.run { selector.update(&rql) }
74
69
  end
75
70
 
@@ -0,0 +1,52 @@
1
+ module NoBrainer::Document::VirtualAttributes
2
+ extend NoBrainer::Autoload
3
+ extend ActiveSupport::Concern
4
+
5
+ VALID_VIRTUAL_FIELD_OPTIONS = [:type, :lazy_fetch, :virtual]
6
+
7
+ included do
8
+ cattr_accessor :virtual_fields, :instance_accessor => false
9
+ end
10
+
11
+ module ClassMethods
12
+ def virtual_field(attr, rql=nil, options={}, &block)
13
+ rql, options = nil, rql if rql.is_a?(Hash)
14
+ rql ||= block
15
+ rql_proc = rql.is_a?(Proc) ? rql : proc { rql }
16
+ field(attr, options.merge(:virtual => rql_proc))
17
+ end
18
+
19
+ def field(attr, options={})
20
+ return super unless options.key?(:virtual)
21
+
22
+ raise "virtual attributes are limited to the root class `#{self.root_class}' for the moment.\n" +
23
+ "Ask on GitHub for polymorphic support." unless is_root_class?
24
+
25
+ raise "You cannot index a virtual attribute. Use an index with a lambda expression instead" if options[:index]
26
+ options.assert_valid_keys(*VALID_VIRTUAL_FIELD_OPTIONS)
27
+
28
+ self.virtual_fields ||= Set.new
29
+ virtual_fields << attr
30
+
31
+ inject_in_layer :virtual_attributes do
32
+ define_method("#{attr}=") do |value|
33
+ raise NoBrainer::Error::ReadonlyField.new("#{attr} is a virtual attribute and thus readonly.")
34
+ end
35
+ end
36
+
37
+ super
38
+ end
39
+
40
+ def remove_field(attr, options={})
41
+ super
42
+
43
+ if fields[:virtual]
44
+ virtual_fields.try(:delete, attr)
45
+
46
+ inject_in_layer :virtual_attributes do
47
+ remove_method("#{attr}=")
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -11,6 +11,7 @@ module NoBrainer::Error
11
11
  class UnknownAttribute < RuntimeError; end
12
12
  class AtomicBlock < RuntimeError; end
13
13
  class LostLock < RuntimeError; end
14
+ class LockInvalidOp < RuntimeError; end
14
15
  class LockUnavailable < RuntimeError; end
15
16
  class InvalidPolymorphicType < RuntimeError; end
16
17
 
@@ -7,27 +7,33 @@ class NoBrainer::Lock
7
7
 
8
8
  # Since PKs are limited to 127 characters, we can't use the user's key as a PK
9
9
  # as it could be arbitrarily long.
10
- field :key_hash, :type => String, :primary_key => true, :default => ->{ Digest::SHA1.base64digest(key) }
11
- field :key, :type => String
12
- field :token, :type => String
13
- field :expires_at, :type => Time
14
-
15
- # We always use a new token, even when reading from the DB, because that's
16
- # what represent our instance.
17
- after_initialize { self.token = NoBrainer::Document::PrimaryKey::Generator.generate }
10
+ field :key_hash, :type => String, :primary_key => true, :default => ->{ Digest::SHA1.base64digest(key.to_s) }
11
+ field :key, :type => String
12
+ field :instance_token, :type => String, :default => ->{ get_new_instance_token }
13
+ field :expires_at, :type => Time
18
14
 
19
15
  scope :expired, where(:expires_at.lt(RethinkDB::RQL.new.now))
20
16
 
21
- def initialize(key, options={})
22
- return super if options[:from_db]
17
+ def self.find(key)
18
+ super(Digest::SHA1.base64digest(key.to_s))
19
+ end
23
20
 
24
- key = case key
25
- when Symbol then key.to_s
26
- when String then key
27
- else raise ArgumentError
21
+ def initialize(key, options={})
22
+ if options[:from_db]
23
+ super
24
+ # We reset our instance_token to allow recoveries.
25
+ self.instance_token = get_new_instance_token
26
+ else
27
+ @default_options = options.slice(:expire, :timeout)
28
+ options.delete(:expire); options.delete(:timeout);
29
+
30
+ super(options.merge(:key => key))
31
+ raise ArgumentError unless valid?
28
32
  end
33
+ end
29
34
 
30
- super(options.merge(:key => key))
35
+ def get_new_instance_token
36
+ NoBrainer::Document::PrimaryKey::Generator.generate
31
37
  end
32
38
 
33
39
  def synchronize(options={}, &block)
@@ -41,22 +47,21 @@ class NoBrainer::Lock
41
47
 
42
48
  def lock(options={})
43
49
  options.assert_valid_keys(:expire, :timeout)
44
- timeout = NoBrainer::Config.lock_options.merge(options)[:timeout]
50
+ timeout = get_option_value(options, :timeout)
45
51
  sleep_amount = 0.1
46
52
 
47
53
  start_at = Time.now
48
- while Time.now - start_at < timeout
49
- return if try_lock(options.select { |k,_| k == :expire })
54
+ loop do
55
+ return if try_lock(options.slice(:expire))
56
+ raise_lock_unavailable! if Time.now - start_at + sleep_amount > timeout
50
57
  sleep(sleep_amount)
51
58
  sleep_amount = [1, sleep_amount * 2].min
52
59
  end
53
-
54
- raise NoBrainer::Error::LockUnavailable.new("Lock on `#{key}' unavailable")
55
60
  end
56
61
 
57
62
  def try_lock(options={})
58
63
  options.assert_valid_keys(:expire)
59
- raise "Lock instance `#{key}' already locked" if @locked
64
+ raise_if_locked!
60
65
 
61
66
  set_expiration(options)
62
67
 
@@ -71,28 +76,28 @@ class NoBrainer::Lock
71
76
  end
72
77
 
73
78
  def unlock
74
- raise "Lock instance `#{key}' not locked" unless @locked
79
+ raise_unless_locked!
75
80
 
76
81
  result = NoBrainer.run do |r|
77
82
  selector.replace do |doc|
78
- r.branch(doc[:token].eq(self.token),
83
+ r.branch(doc[:instance_token].default(nil).eq(self.instance_token),
79
84
  nil, doc)
80
85
  end
81
86
  end
82
87
 
83
88
  @locked = false
84
- raise NoBrainer::Error::LostLock.new("Lost lock on `#{key}'") unless result['deleted'] == 1
89
+ raise_lost_lock! unless result['deleted'] == 1
85
90
  end
86
91
 
87
92
  def refresh(options={})
88
93
  options.assert_valid_keys(:expire)
89
- raise "Lock instance `#{key}' not locked" unless @locked
94
+ raise_unless_locked!
90
95
 
91
- set_expiration(options)
96
+ set_expiration(options.merge(:use_previous_expire => true))
92
97
 
93
98
  result = NoBrainer.run do |r|
94
99
  selector.update do |doc|
95
- r.branch(doc[:token].eq(self.token),
100
+ r.branch(doc[:instance_token].eq(self.instance_token),
96
101
  { :expires_at => self.expires_at }, nil)
97
102
  end
98
103
  end
@@ -102,7 +107,7 @@ class NoBrainer::Lock
102
107
  # unlikely to happen and should not harmful.
103
108
  unless result['replaced'] == 1
104
109
  @locked = false
105
- raise NoBrainer::Error::LostLock.new("Lost lock on `#{key}'")
110
+ raise_lost_lock!
106
111
  end
107
112
  end
108
113
 
@@ -112,7 +117,29 @@ class NoBrainer::Lock
112
117
  private
113
118
 
114
119
  def set_expiration(options)
115
- expire = NoBrainer::Config.lock_options.merge(options)[:expire]
120
+ expire = @previous_expire if options[:use_previous_expire] && !options[:expire]
121
+ expire ||= get_option_value(options, :expire)
122
+ @previous_expire = expire
116
123
  self.expires_at = RethinkDB::RQL.new.now + expire
117
124
  end
125
+
126
+ def get_option_value(options, key)
127
+ NoBrainer::Config.lock_options.merge(@default_options || {}).merge(options)[key]
128
+ end
129
+
130
+ def raise_if_locked!
131
+ raise NoBrainer::Error::LockInvalidOp.new("Lock instance `#{key}' already locked") if @locked
132
+ end
133
+
134
+ def raise_unless_locked!
135
+ raise NoBrainer::Error::LockInvalidOp.new("Lock instance `#{key}' not locked") unless @locked
136
+ end
137
+
138
+ def raise_lost_lock!
139
+ raise NoBrainer::Error::LostLock.new("Lost lock on `#{key}'")
140
+ end
141
+
142
+ def raise_lock_unavailable!
143
+ raise NoBrainer::Error::LockUnavailable.new("Lock on `#{key}' unavailable")
144
+ end
118
145
  end
@@ -47,6 +47,8 @@ module NoBrainer::Profiler::ControllerRuntime
47
47
  end
48
48
 
49
49
  def cleanup_view_runtime
50
+ return super unless Profiler.current
51
+
50
52
  time_spent_in_db_before_views = Profiler.current.total_duration
51
53
  runtime = super
52
54
  time_spent_in_db_after_views = Profiler.current.total_duration
@@ -0,0 +1,36 @@
1
+ class NoBrainer::ReentrantLock < NoBrainer::Lock
2
+ field :lock_count, :type => Integer
3
+
4
+ def try_lock(options={})
5
+ options.assert_valid_keys(:expire)
6
+ set_expiration(options)
7
+
8
+ result = NoBrainer.run do |r|
9
+ selector.replace do |doc|
10
+ r.branch(doc[:instance_token].default(nil).eq(self.instance_token),
11
+ doc.merge(:expires_at => self.expires_at,
12
+ :lock_count => doc[:lock_count] + 1),
13
+ r.branch(doc.eq(nil).or(doc[:expires_at] < r.now),
14
+ self.attributes.merge(:lock_count => 1), doc))
15
+ end
16
+ end
17
+
18
+ @locked = true # to make refresh() and synchronize() happy, somewhat hacky
19
+ return (result['inserted'] + result['replaced']) == 1
20
+ end
21
+
22
+ def unlock
23
+ set_expiration(:use_previous_expire => true)
24
+
25
+ result = NoBrainer.run do |r|
26
+ selector.replace do |doc|
27
+ r.branch(doc[:instance_token].default(nil).eq(self.instance_token),
28
+ r.branch(doc[:lock_count] > 1,
29
+ doc.merge(:expires_at => self.expires_at,
30
+ :lock_count => doc[:lock_count] - 1), nil), doc)
31
+ end
32
+ end
33
+
34
+ raise_lost_lock! unless (result['deleted'] + result['replaced']) == 1
35
+ end
36
+ end
@@ -16,7 +16,7 @@ module NoBrainer
16
16
  # Code that is loaded through the DSL of NoBrainer should not be eager loaded.
17
17
  autoload :Document, :IndexManager, :Loader, :Fork, :Geo, :SymbolDecoration
18
18
  eager_autoload :Config, :Connection, :ConnectionManager, :Error,
19
- :QueryRunner, :Criteria, :RQL, :Lock, :Profiler, :System
19
+ :QueryRunner, :Criteria, :RQL, :Lock, :ReentrantLock, :Profiler, :System
20
20
 
21
21
  class << self
22
22
  delegate :connection, :disconnect, :to => 'NoBrainer::ConnectionManager'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nobrainer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Viennot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-19 00:00:00.000000000 Z
11
+ date: 2015-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rethinkdb
@@ -116,6 +116,7 @@ files:
116
116
  - lib/no_brainer/criteria/run.rb
117
117
  - lib/no_brainer/criteria/scope.rb
118
118
  - lib/no_brainer/criteria/update.rb
119
+ - lib/no_brainer/criteria/virtual_attributes.rb
119
120
  - lib/no_brainer/criteria/where.rb
120
121
  - lib/no_brainer/document.rb
121
122
  - lib/no_brainer/document/aliases.rb
@@ -167,6 +168,7 @@ files:
167
168
  - lib/no_brainer/document/validation/core.rb
168
169
  - lib/no_brainer/document/validation/not_null.rb
169
170
  - lib/no_brainer/document/validation/uniqueness.rb
171
+ - lib/no_brainer/document/virtual_attributes.rb
170
172
  - lib/no_brainer/error.rb
171
173
  - lib/no_brainer/fork.rb
172
174
  - lib/no_brainer/geo.rb
@@ -194,6 +196,7 @@ files:
194
196
  - lib/no_brainer/query_runner/write_error.rb
195
197
  - lib/no_brainer/railtie.rb
196
198
  - lib/no_brainer/railtie/database.rake
199
+ - lib/no_brainer/reentrant_lock.rb
197
200
  - lib/no_brainer/rql.rb
198
201
  - lib/no_brainer/symbol_decoration.rb
199
202
  - lib/no_brainer/system.rb