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.
- checksums.yaml +4 -4
- data/lib/no_brainer/config.rb +36 -8
- data/lib/no_brainer/connection.rb +16 -19
- data/lib/no_brainer/connection_manager.rb +10 -10
- data/lib/no_brainer/criteria.rb +1 -1
- data/lib/no_brainer/criteria/eager_load.rb +1 -1
- data/lib/no_brainer/criteria/find.rb +1 -1
- data/lib/no_brainer/criteria/first.rb +2 -2
- data/lib/no_brainer/criteria/first_or_create.rb +32 -19
- data/lib/no_brainer/criteria/join.rb +62 -0
- data/lib/no_brainer/criteria/where.rb +25 -14
- data/lib/no_brainer/document.rb +1 -1
- data/lib/no_brainer/document/association/belongs_to.rb +4 -3
- data/lib/no_brainer/document/association/eager_loader.rb +26 -25
- data/lib/no_brainer/document/association/has_many.rb +3 -2
- data/lib/no_brainer/document/association/has_many_through.rb +1 -2
- data/lib/no_brainer/document/association/has_one.rb +4 -0
- data/lib/no_brainer/document/atomic_ops.rb +31 -4
- data/lib/no_brainer/document/attributes.rb +12 -9
- data/lib/no_brainer/document/core.rb +18 -18
- data/lib/no_brainer/document/criteria.rb +3 -2
- data/lib/no_brainer/document/dirty.rb +3 -3
- data/lib/no_brainer/document/index.rb +3 -3
- data/lib/no_brainer/document/index/index.rb +5 -5
- data/lib/no_brainer/document/index/meta_store.rb +1 -1
- data/lib/no_brainer/document/index/synchronizer.rb +5 -17
- data/lib/no_brainer/document/missing_attributes.rb +7 -2
- data/lib/no_brainer/document/primary_key.rb +14 -8
- data/lib/no_brainer/document/table_config.rb +118 -0
- data/lib/no_brainer/document/table_config/synchronizer.rb +21 -0
- data/lib/no_brainer/document/timestamps.rb +4 -0
- data/lib/no_brainer/document/validation/core.rb +1 -1
- data/lib/no_brainer/document/validation/uniqueness.rb +1 -1
- data/lib/no_brainer/lock.rb +4 -4
- data/lib/no_brainer/profiler/logger.rb +1 -1
- data/lib/no_brainer/query_runner/database_on_demand.rb +1 -1
- data/lib/no_brainer/query_runner/reconnect.rb +37 -21
- data/lib/no_brainer/query_runner/table_on_demand.rb +12 -5
- data/lib/no_brainer/railtie/database.rake +14 -4
- data/lib/no_brainer/rql.rb +3 -2
- data/lib/no_brainer/symbol_decoration.rb +1 -1
- data/lib/no_brainer/system.rb +17 -0
- data/lib/no_brainer/system/cluster_config.rb +5 -0
- data/lib/no_brainer/system/db_config.rb +5 -0
- data/lib/no_brainer/system/document.rb +24 -0
- data/lib/no_brainer/system/issue.rb +10 -0
- data/lib/no_brainer/system/job.rb +10 -0
- data/lib/no_brainer/system/log.rb +11 -0
- data/lib/no_brainer/system/server_config.rb +7 -0
- data/lib/no_brainer/system/server_status.rb +9 -0
- data/lib/no_brainer/system/stat.rb +11 -0
- data/lib/no_brainer/system/table_config.rb +10 -0
- data/lib/no_brainer/system/table_status.rb +8 -0
- data/lib/nobrainer.rb +7 -6
- data/lib/rails/generators/templates/nobrainer.rb +11 -2
- metadata +17 -3
- 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
|
-
#
|
11
|
-
@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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
@@ -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.
|
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]
|
data/lib/no_brainer/lock.rb
CHANGED
@@ -3,7 +3,7 @@ require 'digest/sha1'
|
|
3
3
|
class NoBrainer::Lock
|
4
4
|
include NoBrainer::Document
|
5
5
|
|
6
|
-
|
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.
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
22
|
-
context[:
|
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
|
-
|
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 =~ /
|
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
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
20
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
13
|
-
|
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 :
|
22
|
-
task :setup => [:
|
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]
|
data/lib/no_brainer/rql.rb
CHANGED
@@ -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,
|
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.
|