nobrainer 0.27.0 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
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.