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