nobrainer 0.13.0 → 0.14.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +6 -4
  3. data/lib/no_brainer/connection.rb +14 -12
  4. data/lib/no_brainer/criteria/core.rb +1 -6
  5. data/lib/no_brainer/criteria/delete.rb +2 -2
  6. data/lib/no_brainer/criteria/order_by.rb +33 -10
  7. data/lib/no_brainer/criteria/preload.rb +0 -5
  8. data/lib/no_brainer/criteria/update.rb +2 -2
  9. data/lib/no_brainer/criteria/where.rb +34 -7
  10. data/lib/no_brainer/decorated_symbol.rb +2 -1
  11. data/lib/no_brainer/document/association/belongs_to.rb +19 -11
  12. data/lib/no_brainer/document/association/core.rb +1 -1
  13. data/lib/no_brainer/document/association/has_many.rb +10 -4
  14. data/lib/no_brainer/document/attributes.rb +29 -4
  15. data/lib/no_brainer/document/callbacks.rb +3 -2
  16. data/lib/no_brainer/document/core.rb +14 -5
  17. data/lib/no_brainer/document/criteria.rb +9 -9
  18. data/lib/no_brainer/document/dirty.rb +11 -5
  19. data/lib/no_brainer/document/dynamic_attributes.rb +4 -0
  20. data/lib/no_brainer/document/id.rb +49 -4
  21. data/lib/no_brainer/document/index.rb +12 -3
  22. data/lib/no_brainer/document/persistance.rb +5 -9
  23. data/lib/no_brainer/document/readonly.rb +7 -0
  24. data/lib/no_brainer/document/serialization.rb +4 -0
  25. data/lib/no_brainer/document/types.rb +62 -13
  26. data/lib/no_brainer/document/uniqueness.rb +99 -0
  27. data/lib/no_brainer/document/validation.rb +8 -24
  28. data/lib/no_brainer/document.rb +3 -1
  29. data/lib/no_brainer/error.rb +9 -9
  30. data/lib/no_brainer/index_manager.rb +4 -2
  31. data/lib/no_brainer/loader.rb +1 -1
  32. data/lib/no_brainer/query_runner/logger.rb +31 -19
  33. data/lib/no_brainer/query_runner/missing_index.rb +15 -3
  34. data/lib/no_brainer/query_runner/{connection.rb → reconnect.rb} +16 -5
  35. data/lib/no_brainer/query_runner/table_on_demand.rb +14 -25
  36. data/lib/no_brainer/query_runner/write_error.rb +5 -8
  37. data/lib/no_brainer/query_runner.rb +2 -2
  38. data/lib/no_brainer/rql.rb +25 -0
  39. data/lib/nobrainer.rb +5 -6
  40. metadata +70 -69
  41. data/lib/no_brainer/util.rb +0 -23
@@ -5,17 +5,23 @@ require 'digest/md5'
5
5
  module NoBrainer::Document::Id
6
6
  extend ActiveSupport::Concern
7
7
 
8
- included do
9
- self.field :id, :type => String, :default => ->{ NoBrainer::Document::Id.generate }, :readonly => true
8
+ DEFAULT_PK_NAME = :id
9
+
10
+ def pk_value
11
+ __send__(self.class.pk_name)
12
+ end
13
+
14
+ def pk_value=(value)
15
+ __send__("#{self.class.pk_name}=", value)
10
16
  end
11
17
 
12
18
  def ==(other)
13
19
  return super unless self.class == other.class
14
- !id.nil? && id == other.id
20
+ !pk_value.nil? && pk_value == other.pk_value
15
21
  end
16
22
  alias_method :eql?, :==
17
23
 
18
- delegate :hash, :to => :id
24
+ delegate :hash, :to => :pk_value
19
25
 
20
26
  # The following code is inspired by the mongo-ruby-driver
21
27
 
@@ -46,4 +52,43 @@ module NoBrainer::Document::Id
46
52
 
47
53
  oid.unpack("C12").map {|e| v=e.to_s(16); v.size == 1 ? "0#{v}" : v }.join
48
54
  end
55
+
56
+ module ClassMethods
57
+ def define_default_pk
58
+ class_variable_set(:@@pk_name, nil)
59
+ field NoBrainer::Document::Id::DEFAULT_PK_NAME, :primary_key => :default,
60
+ :type => String, :default => ->{ NoBrainer::Document::Id.generate }
61
+ end
62
+
63
+ def define_pk(attr)
64
+ if fields[pk_name].try(:[], :primary_key) == :default
65
+ remove_field(pk_name, :set_default_pk => false)
66
+ end
67
+ class_variable_set(:@@pk_name, attr)
68
+ end
69
+
70
+ def pk_name
71
+ class_variable_get(:@@pk_name)
72
+ end
73
+
74
+ def _field(attr, options={})
75
+ super
76
+ define_pk(attr) if options[:primary_key]
77
+ end
78
+
79
+ def field(attr, options={})
80
+ if options[:primary_key]
81
+ options = options.merge(:readonly => true) if options[:readonly].nil?
82
+ options = options.merge(:index => true)
83
+ end
84
+ super
85
+ end
86
+
87
+ def _remove_field(attr, options={})
88
+ super
89
+ if fields[attr][:primary_key] && options[:set_default_pk] != false
90
+ define_default_pk
91
+ end
92
+ end
93
+ end
49
94
  end
@@ -5,7 +5,6 @@ module NoBrainer::Document::Index
5
5
  included do
6
6
  cattr_accessor :indexes, :instance_accessor => false
7
7
  self.indexes = {}
8
- self.index :id
9
8
  end
10
9
 
11
10
  module ClassMethods
@@ -57,6 +56,11 @@ module NoBrainer::Document::Index
57
56
  end
58
57
  end
59
58
 
59
+ def _remove_field(attr, options={})
60
+ super
61
+ remove_index(attr) if fields[attr][:index]
62
+ end
63
+
60
64
  def perform_create_index(index_name, options={})
61
65
  index_name = index_name.to_sym
62
66
  index_args = self.indexes[index_name]
@@ -68,7 +72,7 @@ module NoBrainer::Document::Index
68
72
  end
69
73
 
70
74
  NoBrainer.run(self.rql_table.index_create(index_name, index_args[:options], &index_proc))
71
- NoBrainer.run(self.rql_table.index_wait(index_name)) if options[:wait]
75
+ wait_for_index(index_name) unless options[:wait] == false
72
76
  STDERR.puts "Created index #{self}.#{index_name}" if options[:verbose]
73
77
  end
74
78
 
@@ -79,7 +83,7 @@ module NoBrainer::Document::Index
79
83
 
80
84
  def perform_update_indexes(options={})
81
85
  current_indexes = NoBrainer.run(self.rql_table.index_list).map(&:to_sym)
82
- wanted_indexes = self.indexes.keys - [:id] # XXX Primary key?
86
+ wanted_indexes = self.indexes.keys - [self.pk_name]
83
87
 
84
88
  (current_indexes - wanted_indexes).each do |index_name|
85
89
  perform_drop_index(index_name, options)
@@ -90,5 +94,10 @@ module NoBrainer::Document::Index
90
94
  end
91
95
  end
92
96
  alias_method :update_indexes, :perform_update_indexes
97
+
98
+ def wait_for_index(index_name=nil, options={})
99
+ args = [index_name].compact
100
+ NoBrainer.run(self.rql_table.index_wait(*args))
101
+ end
93
102
  end
94
103
  end
@@ -1,11 +1,6 @@
1
1
  module NoBrainer::Document::Persistance
2
2
  extend ActiveSupport::Concern
3
3
 
4
- included do
5
- extend ActiveModel::Callbacks
6
- define_model_callbacks :create, :update, :save, :destroy, :terminator => 'false'
7
- end
8
-
9
4
  def _initialize(attrs={}, options={})
10
5
  @new_record = !options[:from_db]
11
6
  super
@@ -24,7 +19,8 @@ module NoBrainer::Document::Persistance
24
19
  end
25
20
 
26
21
  def reload(options={})
27
- attrs = selector.raw.first!
22
+ attrs = NoBrainer.run { self.class.selector_for(pk_value) }
23
+ raise NoBrainer::Error::DocumentNotFound, "#{self.class} #{self.class.pk_name}: #{pk_value} not found" unless attrs
28
24
  instance_variables.each { |ivar| remove_instance_variable(ivar) } unless options[:keep_ivars]
29
25
  initialize(attrs, :pristine => true, :from_db => true)
30
26
  self
@@ -33,13 +29,13 @@ module NoBrainer::Document::Persistance
33
29
  def _create(options={})
34
30
  return false if options[:validate] && !valid?
35
31
  keys = self.class.insert_all(@_attributes)
36
- self.id ||= keys.first
32
+ self.pk_value ||= keys.first
37
33
  @new_record = false
38
34
  true
39
35
  end
40
36
 
41
37
  def _update(attrs)
42
- selector.update_all(attrs)
38
+ NoBrainer.run { selector.update(attrs) }
43
39
  end
44
40
 
45
41
  def _update_only_changed_attrs(options={})
@@ -78,7 +74,7 @@ module NoBrainer::Document::Persistance
78
74
 
79
75
  def delete
80
76
  unless @destroyed
81
- selector.delete_all
77
+ NoBrainer.run { selector.delete }
82
78
  @destroyed = true
83
79
  end
84
80
  @_attributes.freeze
@@ -15,5 +15,12 @@ module NoBrainer::Document::Readonly
15
15
  end
16
16
  end
17
17
  end
18
+
19
+ def _remove_field(attr, options={})
20
+ super
21
+ inject_in_layer :readonly do
22
+ remove_method("#{attr}=") if method_defined?("#{attr}=")
23
+ end
24
+ end
18
25
  end
19
26
  end
@@ -5,4 +5,8 @@ module NoBrainer::Document::Serialization
5
5
  include ActiveModel::Serializers::JSON
6
6
 
7
7
  included { self.include_root_in_json = false }
8
+
9
+ def read_attribute_for_serialization(*a, &b)
10
+ read_attribute(*a, &b)
11
+ end
8
12
  end
@@ -1,7 +1,7 @@
1
1
  module NoBrainer::Document::Types
2
2
  extend ActiveSupport::Concern
3
3
 
4
- module CastingRules
4
+ module CastUserToDB
5
5
  extend self
6
6
  InvalidType = NoBrainer::Error::InvalidType
7
7
 
@@ -60,14 +60,23 @@ module NoBrainer::Document::Types
60
60
  end
61
61
 
62
62
  def lookup(type)
63
- CastingRules.method(type.to_s)
63
+ public_method(type.to_s)
64
64
  rescue NameError
65
65
  proc { raise InvalidType }
66
66
  end
67
+ end
67
68
 
68
- def cast(value, type, type_cast_method)
69
- return value if value.nil? || type.nil? || value.is_a?(type)
70
- type_cast_method.call(value)
69
+ module CastDBToUser
70
+ extend self
71
+
72
+ def Symbol(value)
73
+ value.to_sym rescue value
74
+ end
75
+
76
+ def lookup(type)
77
+ public_method(type.to_s)
78
+ rescue NameError
79
+ nil
71
80
  end
72
81
  end
73
82
 
@@ -82,6 +91,11 @@ module NoBrainer::Document::Types
82
91
  end
83
92
  end
84
93
  before_validation :add_type_errors
94
+
95
+ # Fast access for db->user cast methods for performance when reading from
96
+ # the database.
97
+ singleton_class.send(:attr_accessor, :cast_db_to_user_fields)
98
+ self.cast_db_to_user_fields = Set.new
85
99
  end
86
100
 
87
101
  def add_type_errors
@@ -91,12 +105,27 @@ module NoBrainer::Document::Types
91
105
  end
92
106
  end
93
107
 
108
+ def assign_attributes(attrs, options={})
109
+ super
110
+ if options[:from_db]
111
+ self.class.cast_db_to_user_fields.each do |attr|
112
+ field_def = self.class.fields[attr]
113
+ type = field_def[:type]
114
+ value = @_attributes[attr.to_s]
115
+ unless value.nil? || value.is_a?(type)
116
+ @_attributes[attr.to_s] = field_def[:cast_db_to_user].call(value)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
94
122
  module ClassMethods
95
- def cast_value_for(attr, value)
96
- attr = attr.to_sym
97
- field_def = fields[attr]
98
- return value unless field_def && field_def[:type]
99
- NoBrainer::Document::Types::CastingRules.cast(value, field_def[:type], field_def[:type_cast_method])
123
+ def cast_user_to_db_for(attr, value)
124
+ field_def = fields[attr.to_sym]
125
+ return value if !field_def
126
+ type = field_def[:type]
127
+ return value if value.nil? || type.nil? || value.is_a?(type)
128
+ field_def[:cast_user_to_db].call(value)
100
129
  rescue NoBrainer::Error::InvalidType => error
101
130
  error.type = field_def[:type]
102
131
  error.value = value
@@ -104,13 +133,24 @@ module NoBrainer::Document::Types
104
133
  raise error
105
134
  end
106
135
 
136
+ def inherited(subclass)
137
+ super
138
+ subclass.cast_db_to_user_fields = self.cast_db_to_user_fields.dup
139
+ end
140
+
107
141
  def _field(attr, options={})
108
142
  super
109
143
 
144
+ if options[:cast_db_to_user]
145
+ ([self] + descendants).each do |klass|
146
+ klass.cast_db_to_user_fields << attr
147
+ end
148
+ end
149
+
110
150
  inject_in_layer :types do
111
151
  define_method("#{attr}=") do |value|
112
152
  begin
113
- value = self.class.cast_value_for(attr, value)
153
+ value = self.class.cast_user_to_db_for(attr, value)
114
154
  @pending_type_errors.try(:delete, attr)
115
155
  rescue NoBrainer::Error::InvalidType => error
116
156
  @pending_type_errors ||= {}
@@ -125,10 +165,19 @@ module NoBrainer::Document::Types
125
165
 
126
166
  def field(attr, options={})
127
167
  if options[:type]
128
- type_cast_method = NoBrainer::Document::Types::CastingRules.lookup(options[:type])
129
- options = options.merge(:type_cast_method => type_cast_method)
168
+ options = options.merge(
169
+ :cast_user_to_db => NoBrainer::Document::Types::CastUserToDB.lookup(options[:type]),
170
+ :cast_db_to_user => NoBrainer::Document::Types::CastDBToUser.lookup(options[:type]))
130
171
  end
131
172
  super
132
173
  end
174
+
175
+ def _remove_field(attr, options={})
176
+ super
177
+ inject_in_layer :types do
178
+ remove_method("#{attr}=")
179
+ remove_method("#{attr}?") if method_defined?("#{attr}?")
180
+ end
181
+ end
133
182
  end
134
183
  end
@@ -0,0 +1,99 @@
1
+ module NoBrainer::Document::Uniqueness
2
+ extend ActiveSupport::Concern
3
+
4
+ def _create(options={})
5
+ lock_unique_fields
6
+ super
7
+ ensure
8
+ unlock_unique_fields
9
+ end
10
+
11
+ def _update_only_changed_attrs(options={})
12
+ lock_unique_fields
13
+ super
14
+ ensure
15
+ unlock_unique_fields
16
+ end
17
+
18
+ def _lock_key_from_field(field)
19
+ value = read_attribute(field).to_s
20
+ ['nobrainer', self.class.database_name || NoBrainer.connection.parsed_uri[:db],
21
+ self.class.table_name, field, value.empty? ? 'nil' : value].join(':')
22
+ end
23
+
24
+ def lock_unique_fields
25
+ return unless NoBrainer::Config.distributed_lock_class && !self.class.unique_validators.empty?
26
+
27
+ self.class.unique_validators
28
+ .map { |validator| validator.attributes.map { |attr| [attr, validator] } }
29
+ .flatten(1)
30
+ .select { |f, validator| validator.should_validate_uniquess_of?(self, f) }
31
+ .map { |f, options| _lock_key_from_field(f) }
32
+ .sort
33
+ .uniq
34
+ .each do |key|
35
+ lock = NoBrainer::Config.distributed_lock_class.new(key)
36
+ lock.lock
37
+ @locked_unique_fields ||= []
38
+ @locked_unique_fields << lock
39
+ end
40
+ end
41
+
42
+ def unlock_unique_fields
43
+ return unless @locked_unique_fields
44
+ @locked_unique_fields.pop.unlock until @locked_unique_fields.empty?
45
+ end
46
+
47
+ included do
48
+ singleton_class.send(:attr_accessor, :unique_validators)
49
+ self.unique_validators = []
50
+ end
51
+
52
+ module ClassMethods
53
+ def validates_uniqueness_of(*attr_names)
54
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
55
+ end
56
+
57
+ def inherited(subclass)
58
+ super
59
+ subclass.unique_validators = self.unique_validators.dup
60
+ end
61
+
62
+ end
63
+
64
+ class UniquenessValidator < ActiveModel::EachValidator
65
+ attr_accessor :scope
66
+
67
+ def initialize(options={})
68
+ super
69
+ klass = options[:class]
70
+ self.scope = [*options[:scope]]
71
+ ([klass] + klass.descendants).each do |_klass|
72
+ _klass.unique_validators << self
73
+ end
74
+ end
75
+
76
+ def should_validate_uniquess_of?(doc, field)
77
+ (scope + [field]).any? { |f| doc.__send__("#{f}_changed?") }
78
+ end
79
+
80
+ def validate_each(doc, attr, value)
81
+ return true unless should_validate_uniquess_of?(doc, attr)
82
+
83
+ criteria = doc.root_class.unscoped.where(attr => value)
84
+ criteria = apply_scopes(criteria, doc)
85
+ criteria = exclude_doc(criteria, doc) if doc.persisted?
86
+ is_unique = criteria.count == 0
87
+ doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless is_unique
88
+ is_unique
89
+ end
90
+
91
+ def apply_scopes(criteria, doc)
92
+ criteria.where(scope.map { |k| {k => doc.read_attribute(k)} })
93
+ end
94
+
95
+ def exclude_doc(criteria, doc)
96
+ criteria.where(doc.class.pk_name.ne => doc.pk_value)
97
+ end
98
+ end
99
+ end
@@ -3,6 +3,12 @@ module NoBrainer::Document::Validation
3
3
  include ActiveModel::Validations
4
4
  include ActiveModel::Validations::Callbacks
5
5
 
6
+ included do
7
+ # We don't want before_validation returning false to halt the chain.
8
+ define_callbacks :validation, :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name],
9
+ :terminator => proc { false }
10
+ end
11
+
6
12
  def valid?(context=nil)
7
13
  super(context || (new_record? ? :create : :update))
8
14
  end
@@ -10,31 +16,9 @@ module NoBrainer::Document::Validation
10
16
  module ClassMethods
11
17
  def _field(attr, options={})
12
18
  super
13
- validates(attr, { :presence => true }) if options[:required]
19
+ validates(attr, { :presence => options[:required] }) if options.has_key?(:required)
20
+ validates(attr, { :uniqueness => options[:unique] }) if options.has_key?(:unique)
14
21
  validates(attr, options[:validates]) if options[:validates]
15
22
  end
16
-
17
- def validates_uniqueness_of(*attr_names)
18
- validates_with UniquenessValidator, _merge_attributes(attr_names)
19
- end
20
- end
21
-
22
- class UniquenessValidator < ActiveModel::EachValidator
23
- def validate_each(doc, attr, value)
24
- criteria = doc.root_class.unscoped.where(attr => value)
25
- criteria = apply_scopes(criteria, doc)
26
- criteria = exclude_doc(criteria, doc) if doc.persisted?
27
- is_unique = criteria.count == 0
28
- doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless is_unique
29
- is_unique
30
- end
31
-
32
- def apply_scopes(criteria, doc)
33
- criteria.where([*options[:scope]].map { |k| {k => doc.read_attribute(k)} })
34
- end
35
-
36
- def exclude_doc(criteria, doc)
37
- criteria.where(:id.ne => doc.id)
38
- end
39
23
  end
40
24
  end
@@ -5,10 +5,12 @@ module NoBrainer::Document
5
5
  extend NoBrainer::Autoload
6
6
 
7
7
  autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Readonly, :Validation, :Types,
8
- :Persistance, :Callbacks, :Dirty, :Id, :Association, :Serialization,
8
+ :Persistance, :Uniqueness, :Callbacks, :Dirty, :Id, :Association, :Serialization,
9
9
  :Criteria, :Polymorphic, :Index
10
10
 
11
11
  autoload :DynamicAttributes, :Timestamps
12
12
 
13
+ included { define_default_pk }
14
+
13
15
  singleton_class.delegate :all, :to => Core
14
16
  end
@@ -1,13 +1,13 @@
1
1
  module NoBrainer::Error
2
- class Connection < RuntimeError; end
3
- class DocumentNotFound < RuntimeError; end
4
- class DocumentNotSaved < RuntimeError; end
5
- class ChildrenExist < RuntimeError; end
6
- class CannotUseIndex < RuntimeError; end
7
- class MissingIndex < RuntimeError; end
8
- class InvalidType < RuntimeError; end
9
- class AssociationNotSaved < RuntimeError; end
10
- class ReadonlyField < RuntimeError; end
2
+ class Connection < RuntimeError; end
3
+ class DocumentNotFound < RuntimeError; end
4
+ class DocumentNotPersisted < RuntimeError; end
5
+ class ChildrenExist < RuntimeError; end
6
+ class CannotUseIndex < RuntimeError; end
7
+ class MissingIndex < RuntimeError; end
8
+ class InvalidType < RuntimeError; end
9
+ class AssociationNotPersisted < RuntimeError; end
10
+ class ReadonlyField < RuntimeError; end
11
11
 
12
12
  class DocumentInvalid < RuntimeError
13
13
  attr_accessor :instance
@@ -1,6 +1,8 @@
1
1
  module NoBrainer::IndexManager
2
2
  def self.update_indexes(options={})
3
- Rails.application.eager_load! if defined?(Rails)
4
- NoBrainer::Document.all.each { |model| model.perform_update_indexes(options) }
3
+ NoBrainer::Document.all.each { |model| model.perform_update_indexes(options.merge(:wait => false)) }
4
+ unless options[:wait] == false
5
+ NoBrainer::Document.all.each { |model| model.wait_for_index(nil) }
6
+ end
5
7
  end
6
8
  end
@@ -1,5 +1,5 @@
1
1
  module NoBrainer::Loader
2
2
  def self.cleanup
3
- NoBrainer::Document.all.clear
3
+ NoBrainer::Document::Core._all.clear
4
4
  end
5
5
  end
@@ -1,29 +1,41 @@
1
1
  class NoBrainer::QueryRunner::Logger < NoBrainer::QueryRunner::Middleware
2
2
  def call(env)
3
3
  start_time = Time.now
4
- res = @runner.call(env)
5
- if NoBrainer.logger and NoBrainer.logger.debug?
6
- duration = Time.now - start_time
7
- msg = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
4
+ @runner.call(env).tap { log_query(env, start_time) }
5
+ rescue Exception => e
6
+ log_query(env, start_time, e) rescue nil
7
+ raise e
8
+ end
9
+
10
+ private
8
11
 
9
- # Need to fix the rethinkdb gem.
10
- if msg =~ /Erroneous_Portion_Constructed/
11
- msg = "r.the_rethinkdb_gem_is_flipping_out_with_Erroneous_Portion_Constructed"
12
- end
12
+ def log_query(env, start_time, exception=nil)
13
+ return unless NoBrainer.logger.debug?
13
14
 
14
- msg = "(#{env[:db_name]}) #{msg}" if env[:db_name]
15
- msg = "[#{(duration * 1000.0).round(1)}ms] #{msg}"
15
+ duration = Time.now - start_time
16
16
 
17
- if NoBrainer::Config.colorize_logger
18
- case NoBrainer::Util.rql_type(env[:query])
19
- when :write then msg = "\e[1;31m#{msg}\e[0m" # red
20
- when :read then msg = "\e[1;32m#{msg}\e[0m" # green
21
- when :management then msg = "\e[1;33m#{msg}\e[0m" # yellow
22
- end
23
- end
17
+ msg_duration = (duration * 1000.0).round(1).to_s
18
+ msg_duration = " " * [0, 5 - msg_duration.size].max + msg_duration
19
+ msg_duration = "[#{msg_duration}ms] "
24
20
 
25
- NoBrainer.logger.debug(msg)
21
+ msg_db = "[#{env[:db_name]}] " if env[:db_name]
22
+ msg_query = env[:query].inspect.gsub(/\n/, '').gsub(/ +/, ' ')
23
+ msg_exception = " #{exception.class} #{exception.message.split("\n").first}" if exception
24
+ msg_last = nil
25
+
26
+ if NoBrainer::Config.colorize_logger
27
+ query_color = case NoBrainer::RQL.type_of(env[:query])
28
+ when :write then "\e[1;31m" # red
29
+ when :read then "\e[1;32m" # green
30
+ when :management then "\e[1;33m" # yellow
31
+ end
32
+ msg_duration = [query_color, msg_duration].join
33
+ msg_db = ["\e[0;34m", msg_db, query_color].join if msg_db
34
+ msg_exception = ["\e[0;31m", msg_exception].join if msg_exception
35
+ msg_last = "\e[0m"
26
36
  end
27
- res
37
+
38
+ msg = [msg_duration, msg_db, msg_query, msg_exception, msg_last].join
39
+ NoBrainer.logger.debug(msg)
28
40
  end
29
41
  end
@@ -2,9 +2,21 @@ class NoBrainer::QueryRunner::MissingIndex < NoBrainer::QueryRunner::Middleware
2
2
  def call(env)
3
3
  @runner.call(env)
4
4
  rescue RethinkDB::RqlRuntimeError => e
5
- if e.message =~ /^Index `(.+)` was not found\.$/
6
- raise NoBrainer::Error::MissingIndex.new("Please run \"rake db:update_indexes\" to create the index `#{$1}`\n" +
7
- "--> Read http://nobrainer.io/docs/indexes for more information.")
5
+ if e.message =~ /^Index `(.+)` was not found on table `(.+)\.(.+)`\.$/
6
+ index_name = $1
7
+ database_name = $2
8
+ table_name = $3
9
+
10
+ klass = NoBrainer::Document.all.select { |m| m.table_name == table_name }.first
11
+ if klass && klass.pk_name.to_s == index_name
12
+ err_msg = "Please run update the primary key `#{index_name}` in the table `#{database_name}.#{table_name}`."
13
+ else
14
+ err_msg = "Please run \"rake db:update_indexes\" to create the index `#{index_name}`"
15
+ err_msg += " in the table `#{database_name}.#{table_name}`."
16
+ err_msg += "\n--> Read http://nobrainer.io/docs/indexes for more information."
17
+ end
18
+
19
+ raise NoBrainer::Error::MissingIndex.new(err_msg)
8
20
  end
9
21
  raise
10
22
  end
@@ -1,23 +1,23 @@
1
- class NoBrainer::QueryRunner::Connection < NoBrainer::QueryRunner::Middleware
1
+ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
2
2
  def call(env)
3
3
  @runner.call(env)
4
4
  rescue StandardError => e
5
5
  # TODO test that thing
6
6
  if is_connection_error_exception?(e)
7
- retry if reconnect
7
+ retry if reconnect(e)
8
8
  end
9
9
  raise
10
10
  end
11
11
 
12
12
  private
13
13
 
14
- def reconnect
14
+ def reconnect(e)
15
15
  # FIXME thread safety? perhaps we need to use a connection pool
16
16
  # XXX Possibly dangerous, as we could reexecute a non idempotent operation
17
17
  # Check the semantics of the db
18
18
  NoBrainer::Config.max_reconnection_tries.times do
19
19
  begin
20
- NoBrainer.logger.try(:warn, "Lost connection to #{NoBrainer::Config.rethinkdb_url}, retrying...")
20
+ warn_reconnect(e)
21
21
  sleep 1
22
22
  NoBrainer.connection.reconnect(:noreply_wait => false)
23
23
  return true
@@ -35,10 +35,21 @@ class NoBrainer::QueryRunner::Connection < NoBrainer::QueryRunner::Middleware
35
35
  Errno::ECONNRESET, Errno::ETIMEDOUT, IOError
36
36
  true
37
37
  when RethinkDB::RqlRuntimeError
38
- e.message =~ /cannot perform (read|write): No master available/ ||
38
+ e.message =~ /No master available/ ||
39
+ e.message =~ /Master .* not available/ ||
39
40
  e.message =~ /Error: Connection Closed/
40
41
  else
41
42
  false
42
43
  end
43
44
  end
45
+
46
+ def warn_reconnect(e)
47
+ if e.is_a?(RethinkDB::RqlRuntimeError)
48
+ e_msg = e.message.split("\n").first
49
+ msg = "Server #{NoBrainer::Config.rethinkdb_url} not ready - #{e_msg}, retrying..."
50
+ else
51
+ msg = "Connection issue with #{NoBrainer::Config.rethinkdb_url} - #{e}, retrying..."
52
+ end
53
+ NoBrainer.logger.try(:warn, msg)
54
+ end
44
55
  end