nobrainer 0.27.0 → 0.28.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +36 -8
  3. data/lib/no_brainer/connection.rb +16 -19
  4. data/lib/no_brainer/connection_manager.rb +10 -10
  5. data/lib/no_brainer/criteria.rb +1 -1
  6. data/lib/no_brainer/criteria/eager_load.rb +1 -1
  7. data/lib/no_brainer/criteria/find.rb +1 -1
  8. data/lib/no_brainer/criteria/first.rb +2 -2
  9. data/lib/no_brainer/criteria/first_or_create.rb +32 -19
  10. data/lib/no_brainer/criteria/join.rb +62 -0
  11. data/lib/no_brainer/criteria/where.rb +25 -14
  12. data/lib/no_brainer/document.rb +1 -1
  13. data/lib/no_brainer/document/association/belongs_to.rb +4 -3
  14. data/lib/no_brainer/document/association/eager_loader.rb +26 -25
  15. data/lib/no_brainer/document/association/has_many.rb +3 -2
  16. data/lib/no_brainer/document/association/has_many_through.rb +1 -2
  17. data/lib/no_brainer/document/association/has_one.rb +4 -0
  18. data/lib/no_brainer/document/atomic_ops.rb +31 -4
  19. data/lib/no_brainer/document/attributes.rb +12 -9
  20. data/lib/no_brainer/document/core.rb +18 -18
  21. data/lib/no_brainer/document/criteria.rb +3 -2
  22. data/lib/no_brainer/document/dirty.rb +3 -3
  23. data/lib/no_brainer/document/index.rb +3 -3
  24. data/lib/no_brainer/document/index/index.rb +5 -5
  25. data/lib/no_brainer/document/index/meta_store.rb +1 -1
  26. data/lib/no_brainer/document/index/synchronizer.rb +5 -17
  27. data/lib/no_brainer/document/missing_attributes.rb +7 -2
  28. data/lib/no_brainer/document/primary_key.rb +14 -8
  29. data/lib/no_brainer/document/table_config.rb +118 -0
  30. data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
  31. data/lib/no_brainer/document/timestamps.rb +4 -0
  32. data/lib/no_brainer/document/validation/core.rb +1 -1
  33. data/lib/no_brainer/document/validation/uniqueness.rb +1 -1
  34. data/lib/no_brainer/lock.rb +4 -4
  35. data/lib/no_brainer/profiler/logger.rb +1 -1
  36. data/lib/no_brainer/query_runner/database_on_demand.rb +1 -1
  37. data/lib/no_brainer/query_runner/reconnect.rb +37 -21
  38. data/lib/no_brainer/query_runner/table_on_demand.rb +12 -5
  39. data/lib/no_brainer/railtie/database.rake +14 -4
  40. data/lib/no_brainer/rql.rb +3 -2
  41. data/lib/no_brainer/symbol_decoration.rb +1 -1
  42. data/lib/no_brainer/system.rb +17 -0
  43. data/lib/no_brainer/system/cluster_config.rb +5 -0
  44. data/lib/no_brainer/system/db_config.rb +5 -0
  45. data/lib/no_brainer/system/document.rb +24 -0
  46. data/lib/no_brainer/system/issue.rb +10 -0
  47. data/lib/no_brainer/system/job.rb +10 -0
  48. data/lib/no_brainer/system/log.rb +11 -0
  49. data/lib/no_brainer/system/server_config.rb +7 -0
  50. data/lib/no_brainer/system/server_status.rb +9 -0
  51. data/lib/no_brainer/system/stat.rb +11 -0
  52. data/lib/no_brainer/system/table_config.rb +10 -0
  53. data/lib/no_brainer/system/table_status.rb +8 -0
  54. data/lib/nobrainer.rb +7 -6
  55. data/lib/rails/generators/templates/nobrainer.rb +11 -2
  56. metadata +17 -3
  57. data/lib/no_brainer/document/store_in.rb +0 -33
@@ -6,9 +6,14 @@ module NoBrainer::Document::MissingAttributes
6
6
  end
7
7
 
8
8
  def assign_attributes(attrs, options={})
9
+ # there is one and only one key :pluck or :without to missing_attributes
9
10
  if options[:missing_attributes]
10
- # there is one and only one key :pluck or :without to missing_attributes
11
- @missing_attributes = options[:missing_attributes]
11
+ # TODO XXX this whole thing is gross.
12
+ # if @missing_attributes is already there, it's because we are doing a
13
+ # incremental reload. clear_missing_field will do the work of recognizing
14
+ # which fields are here or not.
15
+ @missing_attributes ||= options[:missing_attributes]
16
+
12
17
  assert_access_field(self.class.pk_name, "The primary key is not accessible. Use .raw or")
13
18
  assert_access_field(:_type, "The subclass type is not accessible. Use .raw or") if self.class.is_polymorphic
14
19
  end
@@ -3,6 +3,7 @@ module NoBrainer::Document::PrimaryKey
3
3
  autoload :Generator
4
4
 
5
5
  extend ActiveSupport::Concern
6
+ include ActiveModel::Conversion
6
7
 
7
8
  DEFAULT_PK_NAME = :id
8
9
 
@@ -27,6 +28,11 @@ module NoBrainer::Document::PrimaryKey
27
28
  "#{self.class.table_name}/#{pk_value}"
28
29
  end
29
30
 
31
+ def to_key
32
+ # ActiveModel::Conversion
33
+ [pk_value]
34
+ end
35
+
30
36
  module ClassMethods
31
37
  def define_default_pk
32
38
  class_variable_set(:@@pk_name, nil)
@@ -51,19 +57,19 @@ module NoBrainer::Document::PrimaryKey
51
57
  end
52
58
 
53
59
  def field(attr, options={})
54
- if options[:primary_key]
60
+ if attr.to_sym == pk_name || options[:primary_key]
55
61
  options = options.merge(:readonly => true) if options[:readonly].nil?
56
62
  options = options.merge(:index => true)
57
63
 
58
- if options[:default].nil?
59
- # TODO Maybe we should let the user configure the pk generator
60
- default_pk_generator = NoBrainer::Document::PrimaryKey::Generator
61
- if options[:type].in?([default_pk_generator.field_type, nil])
62
- options[:type] = default_pk_generator.field_type
63
- options[:default] = ->{ default_pk_generator.generate }
64
- end
64
+ # TODO Maybe we should let the user configure the pk generator
65
+ pk_generator = NoBrainer::Document::PrimaryKey::Generator
66
+
67
+ if options[:type].in?([pk_generator.field_type, nil]) && !options.key?(:default)
68
+ options[:type] = pk_generator.field_type
69
+ options[:default] = ->{ pk_generator.generate }
65
70
  end
66
71
  end
72
+
67
73
  super
68
74
  end
69
75
 
@@ -0,0 +1,118 @@
1
+ require 'rethinkdb'
2
+
3
+ module NoBrainer::Document::TableConfig
4
+ extend ActiveSupport::Concern
5
+ extend NoBrainer::Autoload
6
+
7
+ autoload :Synchronizer
8
+
9
+ VALID_TABLE_CONFIG_OPTIONS = [:name, :durability, :shards, :replicas, :primary_replica_tag, :write_acks]
10
+
11
+ included do
12
+ cattr_accessor :table_config_options, :instance_accessor => false
13
+ self.table_config_options = {}
14
+ end
15
+
16
+ module ClassMethods
17
+ def store_in(options)
18
+ if options[:table]
19
+ STDERR.puts "[NoBrainer] `store_in(table: ...)' has been removed. Use `table_config(name: ...)' instead."
20
+ options[:name] = options.delete(:table)
21
+ end
22
+
23
+ if options[:database] || options[:db]
24
+ raise "`store_in(db: ...)' has been removed. Use `run_with(db: ...)' instead."
25
+ end
26
+
27
+ table_config(options)
28
+ end
29
+
30
+ def _set_table_config(options)
31
+ raise "table_config() must be used at the parent class, not a subclass" unless is_root_class?
32
+
33
+ options.assert_valid_keys(*VALID_TABLE_CONFIG_OPTIONS)
34
+ self.table_config_options.merge!(options)
35
+ end
36
+
37
+ def table_name
38
+ name = table_config_options[:name]
39
+ name = name.call if name.is_a?(Proc)
40
+ (name || root_class.name.tableize.gsub('/', '__')).to_s
41
+ end
42
+
43
+ def rql_table
44
+ RethinkDB::RQL.new.table(table_name)
45
+ end
46
+
47
+ def table_config(options={})
48
+ return _set_table_config(options) unless options.empty?
49
+ NoBrainer::System::TableConfig.new_from_db(NoBrainer.run { rql_table.config })
50
+ end
51
+
52
+ def table_status
53
+ NoBrainer::System::TableConfig.new_from_db(NoBrainer.run { rql_table.status })
54
+ end
55
+
56
+ def table_stats
57
+ NoBrainer::System::Stats.where(:db => NoBrainer.current_db, :table => table_name).to_a
58
+ end
59
+
60
+ def rebalance
61
+ NoBrainer.run { rql_table.rebalance }
62
+ true
63
+ end
64
+
65
+ def table_wait
66
+ NoBrainer.run { rql_table.wait }
67
+ end
68
+
69
+ def table_create_options
70
+ NoBrainer::Config.table_options
71
+ .merge(table_config_options)
72
+ .merge(:name => table_name)
73
+ .merge(:primary_key => lookup_field_alias(pk_name))
74
+ .reverse_merge(:durability => 'hard')
75
+ .reduce({}) { |h,(k,v)| h[k] = v.is_a?(Symbol) ? v.to_s : v; h } # symbols -> strings
76
+ end
77
+
78
+ def sync_table_config(options={})
79
+ c = table_create_options
80
+ table_config.update!(c.slice(:durability, :primary_key, :write_acks))
81
+ NoBrainer.run { rql_table.reconfigure(c.slice(:shards, :replicas, :primary_replica_tag)) }
82
+ true
83
+ end
84
+
85
+ def sync_indexes(options={})
86
+ NoBrainer::Document::Index::Synchronizer.new(self).sync_indexes(options)
87
+ end
88
+
89
+ def sync_schema(options={})
90
+ sync_table_config(options)
91
+ sync_indexes(options)
92
+ end
93
+ end
94
+
95
+ class << self
96
+ def sync_table_config(options={})
97
+ models = NoBrainer::Document.all(:types => [:user, :nobrainer])
98
+ NoBrainer::Document::TableConfig::Synchronizer.new(models).sync_table_config(options)
99
+ end
100
+
101
+ def sync_indexes(options={})
102
+ # nobrainer models don't have indexes
103
+ models = NoBrainer::Document.all(:types => [:user])
104
+ NoBrainer::Document::Index::Synchronizer.new(models).sync_indexes(options)
105
+ end
106
+
107
+ def sync_schema(options={})
108
+ sync_table_config(options)
109
+ sync_indexes(options)
110
+ end
111
+
112
+ def rebalance(options={})
113
+ models = NoBrainer::Document.all(:types => [:user])
114
+ models.each(&:rebalance)
115
+ true
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,21 @@
1
+ class NoBrainer::Document::TableConfig::Synchronizer
2
+ def initialize(models)
3
+ @models = models
4
+ end
5
+
6
+ def sync_table_config(options={})
7
+ # XXX A bit funny since we might touch the lock table...
8
+ lock = NoBrainer::Lock.new('nobrainer:sync_table_config')
9
+
10
+ lock.synchronize do
11
+ @models.each(&:sync_table_config)
12
+ end
13
+
14
+ unless options[:wait] == false
15
+ # Waiting on all models due to possible races
16
+ @models.each(&:table_wait)
17
+ end
18
+
19
+ true
20
+ end
21
+ end
@@ -21,4 +21,8 @@ module NoBrainer::Document::Timestamps
21
21
  def cache_key
22
22
  "#{super}#{updated_at.try(:strftime, "-%s%L")}"
23
23
  end
24
+
25
+ def touch
26
+ update!(:updated_at => Time.now)
27
+ end
24
28
  end
@@ -32,7 +32,7 @@ module NoBrainer::Document::Validation::Core
32
32
 
33
33
  shorthands = SHORTHANDS
34
34
  shorthands = shorthands.merge(:required => :not_null) if options[:type] == NoBrainer::Boolean
35
- shorthands.each { |k,v| validates(attr, v => options[k]) if options.has_key?(k) }
35
+ shorthands.each { |k,v| validates(attr, v => options[k]) if options.key?(k) }
36
36
 
37
37
  validates(attr, options[:validates]) if options[:validates]
38
38
  validates(attr, :length => { :minimum => options[:min_length] }) if options[:min_length]
@@ -17,7 +17,7 @@ module NoBrainer::Document::Validation::Uniqueness
17
17
  end
18
18
 
19
19
  def unlock_unique_fields
20
- @locked_keys_for_uniqueness.to_h.values.each(&:unlock)
20
+ (@locked_keys_for_uniqueness || {}).values.each(&:unlock)
21
21
  @locked_keys_for_uniqueness = {}
22
22
  end
23
23
 
@@ -3,7 +3,7 @@ require 'digest/sha1'
3
3
  class NoBrainer::Lock
4
4
  include NoBrainer::Document
5
5
 
6
- store_in :table => 'nobrainer_locks'
6
+ table_config :name => 'nobrainer_locks'
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.
@@ -106,13 +106,13 @@ class NoBrainer::Lock
106
106
  end
107
107
  end
108
108
 
109
+ def save?(*); raise NotImplementedError; end
110
+ def delete(*); raise NotImplementedError; end
111
+
109
112
  private
110
113
 
111
114
  def set_expiration(options)
112
115
  expire = NoBrainer::Config.lock_options.merge(options)[:expire]
113
116
  self.expires_at = RethinkDB::RQL.new.now + expire
114
117
  end
115
-
116
- def save?; raise; end
117
- def delete; raise; end
118
118
  end
@@ -6,7 +6,7 @@ class NoBrainer::Profiler::Logger
6
6
 
7
7
  level = env[:exception] ? Logger::ERROR :
8
8
  not_indexed ? Logger::INFO : Logger::DEBUG
9
- return if NoBrainer.logger.nil? || NoBrainer.logger.level > level
9
+ return if NoBrainer.logger.level > level
10
10
 
11
11
  msg_duration = (env[:duration] * 1000.0).round(1).to_s
12
12
  msg_duration = " " * [0, 6 - msg_duration.size].max + msg_duration
@@ -21,7 +21,7 @@ class NoBrainer::QueryRunner::DatabaseOnDemand < NoBrainer::QueryRunner::Middlew
21
21
  end
22
22
  env[:last_auto_create_database] = db_name
23
23
 
24
- NoBrainer.db_create(db_name)
24
+ NoBrainer.run { |r| r.db_create(db_name) }
25
25
  rescue RuntimeError => e
26
26
  # We might have raced with another db_create
27
27
  raise unless e.message =~ /Database `#{db_name}` already exists/
@@ -2,28 +2,44 @@ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
2
2
  def call(env)
3
3
  @runner.call(env)
4
4
  rescue StandardError => e
5
- context ||= { :retries => NoBrainer::Config.max_retries_on_connection_failure }
6
5
  if is_connection_error_exception?(e)
7
- if NoBrainer::Config.max_retries_on_connection_failure == 0
8
- NoBrainer.disconnect
9
- else
10
- # XXX Possibly dangerous, as we could reexecute a non idempotent operation
11
- # Check the semantics of the db
12
- retry if reconnect(e, context)
13
- end
6
+ context ||= {}
7
+ # XXX Possibly dangerous, as we could reexecute a non idempotent operation
8
+ retry if reconnect(e, context)
14
9
  end
15
10
  raise
16
11
  end
17
12
 
18
13
  private
19
14
 
15
+ def max_tries
16
+ NoBrainer::Config.max_retries_on_connection_failure
17
+ end
18
+
20
19
  def reconnect(e, context)
21
- return false if context[:retries].zero?
22
- context[:retries] -= 1
20
+ context[:connection_retries] ||= max_tries
21
+ context[:previous_connection] ||= NoBrainer.connection
22
+ NoBrainer.disconnect
23
+
24
+ unless context[:lost_connection_logged]
25
+ context[:lost_connection_logged] = true
26
+
27
+ msg = server_not_ready?(e) ? "Server %s not ready: %s" : "Connection issue with %s: %s"
28
+ NoBrainer.logger.warn(msg % [context[:previous_connection].try(:uri), exception_msg(e)])
29
+ end
30
+
31
+ if context[:connection_retries].zero?
32
+ NoBrainer.logger.info("Retry limit exceeded (#{max_tries}). Giving up.")
33
+ return false
34
+ end
35
+ context[:connection_retries] -= 1
23
36
 
24
- warn_reconnect(e)
25
37
  sleep 1
26
- NoBrainer.connection.reconnect(:noreply_wait => false)
38
+
39
+ c = NoBrainer.connection
40
+ NoBrainer.logger.info("Connecting to #{c.uri}... (last error: #{exception_msg(e)})")
41
+ c.connect
42
+
27
43
  true
28
44
  rescue StandardError => e
29
45
  retry if is_connection_error_exception?(e)
@@ -36,20 +52,20 @@ class NoBrainer::QueryRunner::Reconnect < NoBrainer::QueryRunner::Middleware
36
52
  Errno::ECONNRESET, Errno::ETIMEDOUT, IOError
37
53
  true
38
54
  when RethinkDB::RqlRuntimeError
39
- e.message =~ /Primary .* not available/ ||
55
+ e.message =~ /lost contact/ ||
56
+ e.message =~ /(P|p)rimary .* not available/||
40
57
  e.message =~ /Connection.*closed/
41
58
  else
42
59
  false
43
60
  end
44
61
  end
45
62
 
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)
63
+ def exception_msg(e)
64
+ e.is_a?(RethinkDB::RqlRuntimeError) ? e.message.split("\n").first : e.to_s
65
+ end
66
+
67
+ def server_not_ready?(e)
68
+ e.message =~ /lost contact/ ||
69
+ e.message =~ /(P|p)rimary .* not available/
54
70
  end
55
71
  end
@@ -16,9 +16,8 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
16
16
  private
17
17
 
18
18
  def auto_create_table(env, db_name, table_name)
19
- model ||= NoBrainer::Document::Core._all.select { |m| m.table_name == table_name }.first
20
- model ||= NoBrainer::Document::Core._all_nobrainer.select { |m| m.table_name == table_name }.first
21
-
19
+ model = NoBrainer::Document.all(:types => [:user, :nobrainer])
20
+ .detect { |m| m.table_name == table_name }
22
21
  if model.nil?
23
22
  raise "Auto table creation is not working for `#{db_name}.#{table_name}` -- Can't find the corresponding model."
24
23
  end
@@ -28,8 +27,16 @@ class NoBrainer::QueryRunner::TableOnDemand < NoBrainer::QueryRunner::Middleware
28
27
  end
29
28
  env[:last_auto_create_table] = [db_name, table_name]
30
29
 
31
- NoBrainer.run_with(:db => db_name) do
32
- NoBrainer.table_create(table_name, :primary_key => model.lookup_field_alias(model.pk_name))
30
+ create_options = model.table_create_options
31
+
32
+ NoBrainer.run(:db => db_name) do |r|
33
+ r.table_create(table_name, create_options.reject { |k,_| k.in? [:name, :write_acks] })
34
+ end
35
+
36
+ if create_options[:write_acks] && create_options[:write_acks] != 'single'
37
+ NoBrainer.run(:db => db_name) do |r|
38
+ r.table(table_name).config().update(:write_acks => create_options[:write_acks])
39
+ end
33
40
  end
34
41
  rescue RuntimeError => e
35
42
  # We might have raced with another table create
@@ -9,8 +9,18 @@ namespace :nobrainer do
9
9
  NoBrainer.sync_indexes(:verbose => true)
10
10
  end
11
11
 
12
- task :sync_indexes_quiet => :environment do
13
- NoBrainer.sync_indexes
12
+ desc 'Synchronize table configuration'
13
+ task :sync_table_config => :environment do
14
+ NoBrainer.sync_table_config(:verbose => true)
15
+ end
16
+
17
+ desc 'Synchronize indexes and table configuration'
18
+ task :sync_schema => :environment do
19
+ NoBrainer.sync_schema(:verbose => true)
20
+ end
21
+
22
+ task :sync_schema_quiet => :environment do
23
+ NoBrainer.sync_schema
14
24
  end
15
25
 
16
26
  desc 'Load seed data from db/seeds.rb'
@@ -18,8 +28,8 @@ namespace :nobrainer do
18
28
  Rails.application.load_seed
19
29
  end
20
30
 
21
- desc 'Equivalent to :sync_indexes_quiet + :seed'
22
- task :setup => [:sync_indexes_quiet, :seed]
31
+ desc 'Equivalent to :sync_schema_quiet + :seed'
32
+ task :setup => [:sync_schema_quiet, :seed]
23
33
 
24
34
  desc 'Equivalent to :drop + :setup'
25
35
  task :reset => [:drop, :setup]
@@ -21,8 +21,9 @@ module NoBrainer::RQL
21
21
  case rql.is_a?(RethinkDB::RQL) && rql.body.is_a?(Array) && rql.body.first
22
22
  when UPDATE, DELETE, REPLACE, INSERT
23
23
  :write
24
- when DB_CREATE, DB_DROP, DB_LIST, TABLE_CREATE, TABLE_DROP, TABLE_LIST, SYNC,
25
- INDEX_CREATE, INDEX_DROP, INDEX_LIST, INDEX_STATUS, INDEX_WAIT
24
+ when DB_CREATE, DB_DROP, DB_LIST, TABLE_CREATE, TABLE_DROP, TABLE_LIST,
25
+ INDEX_CREATE, INDEX_DROP, INDEX_LIST, INDEX_STATUS, INDEX_WAIT, INDEX_RENAME,
26
+ CONFIG, STATUS, WAIT, RECONFIGURE, REBALANCE, SYNC
26
27
  :management
27
28
  else
28
29
  # XXX Not necessarily correct, but we'll be happy for logging colors.