promiscuous 0.91.0 → 0.92.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/promiscuous.rb +1 -1
  3. data/lib/promiscuous/amqp.rb +1 -1
  4. data/lib/promiscuous/amqp/fake.rb +0 -3
  5. data/lib/promiscuous/amqp/file.rb +81 -0
  6. data/lib/promiscuous/amqp/null.rb +0 -3
  7. data/lib/promiscuous/cli.rb +35 -25
  8. data/lib/promiscuous/config.rb +8 -3
  9. data/lib/promiscuous/error.rb +1 -2
  10. data/lib/promiscuous/key.rb +1 -12
  11. data/lib/promiscuous/mongoid.rb +7 -0
  12. data/lib/promiscuous/publisher/context/base.rb +4 -4
  13. data/lib/promiscuous/publisher/context/middleware.rb +2 -23
  14. data/lib/promiscuous/publisher/model/ephemeral.rb +5 -1
  15. data/lib/promiscuous/publisher/model/mock.rb +9 -7
  16. data/lib/promiscuous/publisher/model/mongoid.rb +3 -1
  17. data/lib/promiscuous/publisher/operation.rb +1 -1
  18. data/lib/promiscuous/publisher/operation/atomic.rb +44 -32
  19. data/lib/promiscuous/publisher/operation/base.rb +14 -9
  20. data/lib/promiscuous/publisher/operation/ephemeral.rb +14 -0
  21. data/lib/promiscuous/publisher/operation/mongoid.rb +4 -12
  22. data/lib/promiscuous/subscriber/message_processor/base.rb +17 -1
  23. data/lib/promiscuous/subscriber/message_processor/regular.rb +94 -48
  24. data/lib/promiscuous/subscriber/model/active_record.rb +25 -0
  25. data/lib/promiscuous/subscriber/model/base.rb +17 -13
  26. data/lib/promiscuous/subscriber/model/mongoid.rb +20 -1
  27. data/lib/promiscuous/subscriber/model/observer.rb +4 -0
  28. data/lib/promiscuous/subscriber/operation/base.rb +14 -16
  29. data/lib/promiscuous/subscriber/operation/bootstrap.rb +7 -1
  30. data/lib/promiscuous/subscriber/operation/regular.rb +6 -0
  31. data/lib/promiscuous/subscriber/worker.rb +6 -2
  32. data/lib/promiscuous/subscriber/worker/eventual_destroyer.rb +85 -0
  33. data/lib/promiscuous/subscriber/worker/message.rb +9 -15
  34. data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +24 -78
  35. data/lib/promiscuous/subscriber/worker/runner.rb +6 -2
  36. data/lib/promiscuous/subscriber/worker/stats.rb +11 -7
  37. data/lib/promiscuous/version.rb +1 -1
  38. metadata +66 -63
  39. data/lib/promiscuous/error/already_processed.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b84ccb396c096ef9d2164d4400356606f90c3954
4
- data.tar.gz: 31da27f6d1163623df3014dc15a0641d6e93c0c3
3
+ metadata.gz: d772f9b9786ef7ee6787030113bf9ec5934717d5
4
+ data.tar.gz: 163c9cb61d4b359f1e6766effef9baccdfd8c229
5
5
  SHA512:
6
- metadata.gz: 2cec057b926337d6940a8d3144b88ccc3dcf200a35dc74d88f69bf4ad388d034b6e68a04dd2f270f5c581382bef3ae5985b6e4ad9e7d9cfe44babf6cdacabd02
7
- data.tar.gz: d84a82ecdcb22041d72c08e8c9fb443e77c2c9a74c49589a6d209079df7055f1adc484ca12b32d6f75f9a04e8f66bcfbaefeff020eb47b38c6fecbe3e1bc0c0b
6
+ metadata.gz: 6cacc43f226549868f963abbad29e7ba7249a1bbc1c4eb3e0b8014275935ca37e7455a73b48e36d7c0c56906308ab334c104bbf136a7cd2c7345ee976161eead
7
+ data.tar.gz: 6f59e694ed2c89e2ebe9a6c73995c4b6e4987ee1f43d171f55f3783b9698c4974aff8ef684d22dd8e652956d943081b952f8bb313d492016629b9564a1447727
data/lib/promiscuous.rb CHANGED
@@ -13,7 +13,7 @@ module Promiscuous
13
13
  require_for 'rails', 'promiscuous/railtie'
14
14
  require_for 'resque', 'promiscuous/resque'
15
15
  require_for 'sidekiq', 'promiscuous/sidekiq'
16
-
16
+ require_for 'mongoid', 'promiscuous/mongoid'
17
17
 
18
18
  extend Promiscuous::Autoload
19
19
  autoload :Common, :Publisher, :Subscriber, :Observer, :Worker, :Ephemeral,
@@ -1,6 +1,6 @@
1
1
  module Promiscuous::AMQP
2
2
  extend Promiscuous::Autoload
3
- autoload :HotBunnies, :Bunny, :Null, :Fake
3
+ autoload :HotBunnies, :Bunny, :Null, :Fake, :File
4
4
 
5
5
  LIVE_EXCHANGE = 'promiscuous'
6
6
  BOOTSTRAP_EXCHANGE = 'promiscuous.bootstrap'
@@ -38,9 +38,6 @@ class Promiscuous::AMQP::Fake
38
38
  msg && JSON.parse(msg[:payload])
39
39
  end
40
40
 
41
- def open_queue(options={}, &block)
42
- end
43
-
44
41
  module Subscriber
45
42
  def subscribe(options={}, &block)
46
43
  end
@@ -0,0 +1,81 @@
1
+ class Promiscuous::AMQP::File
2
+ def connect
3
+ end
4
+
5
+ def disconnect
6
+ end
7
+
8
+ def connected?
9
+ true
10
+ end
11
+
12
+ def new_connection(options={})
13
+ end
14
+
15
+ def publish(options={})
16
+ options[:on_confirm].try(:call)
17
+ raise NotImplemented
18
+ end
19
+
20
+ module Subscriber
21
+ attr_accessor :lock, :prefetch_wait, :num_pending
22
+
23
+ def subscribe(options={}, &block)
24
+ file_name, worker_index, num_workers = Promiscuous::Config.subscriber_amqp_url.split(':')
25
+
26
+ worker_index = worker_index.to_i
27
+ num_workers = num_workers.to_i
28
+
29
+ file = File.open(file_name, 'r')
30
+
31
+ @prefetch = Promiscuous::Config.prefetch
32
+ @num_pending = 0
33
+ @lock = Mutex.new
34
+ @prefetch_wait = ConditionVariable.new
35
+
36
+ @thread = Thread.new do
37
+ file.each_with_index do |line, i|
38
+ if num_workers > 0
39
+ next if ((i+worker_index) % num_workers) != 0
40
+ end
41
+
42
+ return if @stop
43
+
44
+ @lock.synchronize do
45
+ @prefetch_wait.wait(@lock) until @num_pending < @prefetch
46
+ @num_pending += 1
47
+ end
48
+
49
+ block.call(Metadata.new(self), line.chomp)
50
+ end
51
+
52
+ @lock.synchronize do
53
+ @prefetch_wait.wait(@lock) until @num_pending == 0
54
+ end
55
+
56
+ # will shutdown the CLI gracefully
57
+ Process.kill("SIGTERM", Process.pid)
58
+ end
59
+ end
60
+
61
+ def recover
62
+ end
63
+
64
+ def disconnect
65
+ @stop = true
66
+ end
67
+
68
+ class Metadata
69
+ def initialize(sub)
70
+ @sub = sub
71
+ end
72
+
73
+ def ack
74
+ @sub.lock.synchronize do
75
+ @sub.num_pending -= 1
76
+ @sub.prefetch_wait.signal
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -12,7 +12,4 @@ class Promiscuous::AMQP::Null
12
12
  def publish(options={})
13
13
  options[:on_confirm].try(:call)
14
14
  end
15
-
16
- def open_queue(options={}, &block)
17
- end
18
15
  end
@@ -3,22 +3,28 @@ class Promiscuous::CLI
3
3
 
4
4
  def trap_debug_signals
5
5
  Signal.trap 'SIGUSR2' do
6
- Thread.list.each do |thread|
7
- print_status '----[ Threads ]----' + '-' * (100-19)
8
- if thread.backtrace
9
- print_status "Thread #{thread} #{thread['label']}"
10
- print_status thread.backtrace.join("\n")
11
- else
12
- print_status "Thread #{thread} #{thread['label']} -- no backtrace"
6
+ # Using a thread because we cannot acquire mutexes in a trap context in
7
+ # ruby 2.0
8
+ Thread.new do
9
+ Thread.list.each do |thread|
10
+ next if Thread.current == thread
11
+
12
+ print_status '----[ Threads ]----' + '-' * (100-19)
13
+ if thread.backtrace
14
+ print_status "Thread #{thread} #{thread['label']}"
15
+ print_status thread.backtrace.join("\n")
16
+ else
17
+ print_status "Thread #{thread} #{thread['label']} -- no backtrace"
18
+ end
13
19
  end
14
- end
15
20
 
16
- if @worker && @worker.respond_to?(:message_synchronizer)
17
- if blocked_messages = @worker.message_synchronizer.try(:blocked_messages)
18
- print_status '----[ Pending Dependencies ]----' + '-' * (100-32)
19
- blocked_messages.reverse_each { |msg| print_status msg }
21
+ if @worker && @worker.respond_to?(:message_synchronizer)
22
+ if blocked_messages = @worker.message_synchronizer.try(:blocked_messages)
23
+ print_status '----[ Pending Dependencies ]----' + '-' * (100-32)
24
+ blocked_messages.reverse_each { |msg| print_status msg }
25
+ end
26
+ print_status '-' * 100
20
27
  end
21
- print_status '-' * 100
22
28
  end
23
29
  end
24
30
  end
@@ -26,14 +32,18 @@ class Promiscuous::CLI
26
32
  def trap_exit_signals
27
33
  %w(SIGTERM SIGINT).each do |signal|
28
34
  Signal.trap(signal) do
29
- print_status "Exiting..."
30
- if @stop
31
- @worker.try(:show_stop_status)
32
- else
33
- @stop = true
34
- @worker.try(:stop)
35
- @worker = nil
36
- end
35
+ # Using a thread because we cannot acquire mutexes in a trap context in
36
+ # ruby 2.0
37
+ Thread.new do
38
+ print_status "Exiting..."
39
+ if @stop
40
+ @worker.try(:show_stop_status)
41
+ else
42
+ @stop = true
43
+ @worker.try(:stop)
44
+ @worker = nil
45
+ end
46
+ end.join
37
47
  end
38
48
  end
39
49
  end
@@ -144,12 +154,12 @@ class Promiscuous::CLI
144
154
  Promiscuous::Config.no_deps = true
145
155
  end
146
156
 
147
- opts.on "-l", "--require FILE", "File to require to load your app. Don't worry about it with rails" do |file|
148
- options[:require] = file
157
+ opts.on "-x", "--ignore-exceptions", "Ignore exceptions and continue to process messages" do
158
+ Promiscuous::Config.ignore_exceptions = true
149
159
  end
150
160
 
151
- opts.on "-r", "--recovery", "Run in recovery mode" do
152
- Promiscuous::Config.recovery = true
161
+ opts.on "-l", "--require FILE", "File to require to load your app. Don't worry about it with rails" do |file|
162
+ options[:require] = file
153
163
  end
154
164
 
155
165
  opts.on "-p", "--prefetch [NUM]", "Number of messages to prefetch" do |prefetch|
@@ -3,9 +3,10 @@ module Promiscuous::Config
3
3
  :publisher_amqp_url, :subscriber_amqp_url, :publisher_exchange,
4
4
  :subscriber_exchanges, :queue_name, :queue_options, :redis_url,
5
5
  :redis_urls, :redis_stats_url, :stats_interval,
6
- :socket_timeout, :heartbeat, :no_deps, :hash_size, :recovery,
6
+ :socket_timeout, :heartbeat, :no_deps, :hash_size,
7
7
  :prefetch, :recovery_timeout, :logger, :subscriber_threads,
8
- :version_field, :error_notifier, :recovery_on_boot
8
+ :version_field, :error_notifier, :recovery_on_boot,
9
+ :on_stats, :ignore_exceptions, :consistency, :max_retries, :generation
9
10
 
10
11
  def self.backend=(value)
11
12
  @@backend = value
@@ -53,7 +54,6 @@ module Promiscuous::Config
53
54
  self.heartbeat ||= 60
54
55
  self.no_deps ||= false
55
56
  self.hash_size ||= 2**20 # one million keys ~ 200Mb.
56
- self.recovery ||= false
57
57
  self.prefetch ||= self.bootstrap ? 10000000 : 1000
58
58
  self.recovery_timeout ||= 10
59
59
  self.logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDERR).tap { |l| l.level = Logger::WARN }
@@ -61,6 +61,11 @@ module Promiscuous::Config
61
61
  self.error_notifier ||= proc {}
62
62
  self.version_field ||= '_v'
63
63
  self.recovery_on_boot = true if self.recovery_on_boot.nil?
64
+ self.on_stats ||= proc { |rate, latency| }
65
+ self.ignore_exceptions ||= false
66
+ self.consistency ||= :eventual
67
+ self.max_retries ||= 10
68
+ self.generation ||= 1
64
69
  end
65
70
 
66
71
  def self.configure(&block)
@@ -1,6 +1,5 @@
1
1
  module Promiscuous::Error
2
2
  extend Promiscuous::Autoload
3
3
  autoload :Base, :Connection, :Publisher, :Subscriber, :Recovery,
4
- :Dependency, :MissingContext, :AlreadyProcessed,
5
- :LockUnavailable, :LostLock
4
+ :Dependency, :MissingContext, :LockUnavailable, :LostLock
6
5
  end
@@ -1,21 +1,10 @@
1
1
  class Promiscuous::Key
2
- def initialize(role, nodes=[], no_join=nil)
2
+ def initialize(role, nodes=[])
3
3
  @role = role
4
4
  @nodes = nodes
5
- @no_join = no_join
6
5
  end
7
6
 
8
7
  def join(*nodes)
9
- # --- backward compatiblity code ---
10
- # TODO remove code
11
- if nodes == ['global', nil, nil]
12
- return self.class.new(@role, @nodes + nodes, :no_join)
13
- end
14
- if @no_join
15
- return self.class.new(@role, @nodes)
16
- end
17
- # --- backward compatiblity code ---
18
-
19
8
  self.class.new(@role, @nodes + nodes)
20
9
  end
21
10
 
@@ -0,0 +1,7 @@
1
+ class Moped::BSON::ObjectId
2
+ # No {"$oid": "123"}, it's horrible.
3
+ # TODO Document this shit.
4
+ def to_json(*args)
5
+ "\"#{to_s}\""
6
+ end
7
+ end
@@ -24,11 +24,11 @@ class Promiscuous::Publisher::Context::Base
24
24
  end
25
25
  end
26
26
 
27
- attr_accessor :name, :read_operations, :extra_dependencies, :current_user_id
27
+ attr_accessor :name, :read_operations, :extra_dependencies, :current_user
28
28
 
29
- def initialize(*args)
30
- @name = args[0].try(:to_s) || 'anonymous'
31
- @current_user_id = args[1]
29
+ def initialize(name=nil, options={})
30
+ @name = name.try(:to_s) || 'anonymous'
31
+ @current_user = options[:current_user]
32
32
  @read_operations = []
33
33
  @extra_dependencies = []
34
34
  @transaction_managers = {}
@@ -4,12 +4,8 @@ class Promiscuous::Publisher::Context::Middleware < Promiscuous::Publisher::Cont
4
4
 
5
5
  def process_action(*args)
6
6
  full_name = "#{self.class.controller_path}/#{self.action_name}"
7
- current_user_id = self.respond_to?(:current_user) ? self.current_user.try(:id) : nil
8
- Promiscuous::Publisher::Context::Middleware.with_context(full_name, current_user_id) { super }
9
- end
10
-
11
- def render(*args)
12
- Promiscuous::Publisher::Context::Middleware.without_context { super }
7
+ current_user = self.current_user if self.respond_to?(:current_user)
8
+ Promiscuous::Publisher::Context::Middleware.with_context(full_name, :current_user => current_user) { super }
13
9
  end
14
10
  end
15
11
 
@@ -26,23 +22,6 @@ class Promiscuous::Publisher::Context::Middleware < Promiscuous::Publisher::Cont
26
22
  Promiscuous.disabled = old_disabled
27
23
  end
28
24
 
29
- def self.without_context
30
- # This is different from the method without_promiscuous in convenience.rb
31
- # That's used for render() and things that are *not* supposed to write.
32
- # We actually force promiscuous to instrument queries, and make sure that
33
- # we don't do any writes we shouldn't.
34
- old_disabled, Promiscuous.disabled = Promiscuous.disabled?, false
35
- old_current, self.current = self.current, nil
36
- yield
37
- rescue Exception => e
38
- $promiscuous_last_exception = e if e.is_a? Promiscuous::Error::Base
39
- pretty_print_exception(e) unless e.is_a? ActionView::MissingTemplate
40
- raise e
41
- ensure
42
- self.current = old_current
43
- Promiscuous.disabled = old_disabled
44
- end
45
-
46
25
  def self.pretty_print_exception(e)
47
26
  return if $promiscuous_pretty_print_exception_once == :disable || ENV['RAILS_ENV'] == 'production'
48
27
  return if e.is_a?(SystemExit)
@@ -33,13 +33,17 @@ module Promiscuous::Publisher::Model::Ephemeral
33
33
  operation = :update unless self.new_record
34
34
  operation = :destroy if self.destroyed
35
35
 
36
- Promiscuous::Publisher::Operation::Atomic.new(:instance => self, :operation => operation).execute {}
36
+ save_operation(operation)
37
37
 
38
38
  self.new_record = false
39
39
  true
40
40
  end
41
41
  alias :save! :save
42
42
 
43
+ def save_operation(operation)
44
+ Promiscuous::Publisher::Operation::Ephemeral.new(:instance => self, :operation => operation).execute
45
+ end
46
+
43
47
  def update_attributes(attrs)
44
48
  attrs.each { |attr, value| __send__("#{attr}=", value) }
45
49
  save
@@ -22,15 +22,17 @@ module Promiscuous::Publisher::Model::Mock
22
22
  end
23
23
  end
24
24
 
25
- class PromiscuousMethods
26
- include Promiscuous::Publisher::Model::Base::PromiscuousMethodsBase
27
- include Promiscuous::Publisher::Model::Ephemeral::PromiscuousMethodsEphemeral
25
+ def save_operation(operation)
26
+ payload = nil
28
27
 
29
- def sync(options={}, &block)
30
- payload[:operations] = [self.payload.merge(:operation => options[:operation] || :update)]
31
- payload[:app] = self.class.mock_options[:from]
32
- Promiscuous::Subscriber::Worker::Message.new(MultiJson.dump(payload)).process
28
+ Promiscuous::Publisher::Context::Middleware.with_context("mocking #{self.class}") do
29
+ op = Promiscuous::Publisher::Operation::Ephemeral.new(:instance => self, :operation => operation)
30
+ # TODO FIX the mocks to populate app name, also we need to hook before the
31
+ # json dump.
32
+ payload = op.generate_payload
33
33
  end
34
+
35
+ Promiscuous::Subscriber::Worker::Message.new(payload).process
34
36
  end
35
37
 
36
38
  module ClassMethods
@@ -23,7 +23,9 @@ module Promiscuous::Publisher::Model::Mongoid
23
23
 
24
24
  def sync(options={}, &block)
25
25
  raise "Use promiscuous.sync on the parent instance" if @instance.embedded?
26
- super
26
+
27
+ # We can use the ephemeral because both are mongoid and ephemerals are atomic operations.
28
+ Promiscuous::Publisher::Operation::Ephemeral.new(:instance => @instance, :operation => :update).execute
27
29
  end
28
30
 
29
31
  def attribute(attr)
@@ -1,4 +1,4 @@
1
1
  module Promiscuous::Publisher::Operation
2
2
  extend Promiscuous::Autoload
3
- autoload :Base, :Transaction, :Atomic, :NonPersistent, :ProxyForQuery
3
+ autoload :Base, :Transaction, :Atomic, :NonPersistent, :ProxyForQuery, :Ephemeral
4
4
  end
@@ -35,10 +35,47 @@ class Promiscuous::Publisher::Operation::Atomic < Promiscuous::Publisher::Operat
35
35
  end
36
36
  end
37
37
 
38
- def execute_instrumented(query)
39
- raise if @instance.nil? # assert()
38
+ def do_database_query(query)
39
+ case operation
40
+ when :create
41
+ # We don't stash the version in the document as we can't have races
42
+ # on the same document.
43
+ when :update
44
+ increment_version_in_document
45
+ # We are now in the possession of an instance that matches the original
46
+ # selector. We need to make sure the db query will operate on it,
47
+ # instead of the original selector.
48
+ use_id_selector(:use_atomic_version_selector => true)
49
+ # We need to use an atomic versioned selector to make sure that
50
+ # if we lose the lock for a long period of time, we don't mess up
51
+ # the record. Perhaps the operation has been recovered a while ago.
52
+ when :destroy
53
+ use_id_selector
54
+ end
55
+
56
+ # The driver is responsible to set instance to the appropriate value.
57
+ query.call_and_remember_result(:instrumented)
58
+
59
+ if query.failed?
60
+ # If we get an network failure, we should retry later.
61
+ return if recoverable_failure?(query.exception)
62
+ @instance = nil
63
+ end
64
+ end
65
+
66
+ def yell_about_missing_instance
67
+ err = "Cannot find document. Database had a dataloss?. Proceeding anyways. #{@recovery_data}"
68
+ e = Promiscuous::Error::Recovery.new(err)
69
+ Promiscuous.warn "[recovery] #{e}"
70
+ Promiscuous::Config.error_notifier.call(e)
71
+ end
40
72
 
41
- unless self.recovering?
73
+ def execute_instrumented(query)
74
+ if recovering?
75
+ # The DB died or something. We cannot find our instance any more :(
76
+ # this is a problem, but we need to publish.
77
+ yell_about_missing_instance if @instance.nil?
78
+ else
42
79
  generate_read_dependencies
43
80
  acquire_op_lock
44
81
 
@@ -75,32 +112,7 @@ class Promiscuous::Publisher::Operation::Atomic < Promiscuous::Publisher::Operat
75
112
  # documents are missing on our side to be able to resend the destroy
76
113
  # message.
77
114
 
78
- case operation
79
- when :create
80
- # We don't stash the version in the document as we can't have races
81
- # on the same document.
82
- when :update
83
- stash_version_in_document(@committed_write_deps.first.version)
84
- # We are now in the possession of an instance that matches the original
85
- # selector. We need to make sure the db query will operate on it,
86
- # instead of the original selector.
87
- use_id_selector(:use_atomic_version_selector => true)
88
- # We need to use an atomic versioned selector to make sure that
89
- # if we lose the lock for a long period of time, we don't mess up
90
- # the record. Perhaps the operation has been recovered a while ago.
91
- when :destroy
92
- use_id_selector
93
- end
94
-
95
- # The driver is responsible to set instance to the appropriate value.
96
- query.call_and_remember_result(:instrumented)
97
-
98
- if query.failed?
99
- # If we get an network failure, we should retry later.
100
- return if recoverable_failure?(query.exception)
101
- @instance = nil
102
- end
103
-
115
+ do_database_query(query) unless @instance.nil?
104
116
  # We take a timestamp right after the write is performed because latency
105
117
  # measurements are performed on the subscriber.
106
118
  record_timestamp
@@ -141,6 +153,7 @@ class Promiscuous::Publisher::Operation::Atomic < Promiscuous::Publisher::Operat
141
153
  def fetch_instance
142
154
  # This method is overridden to use the original query selector.
143
155
  # Should return nil if the instance is not found.
156
+ @instance.reload if @instance.respond_to?(:reload)
144
157
  @instance
145
158
  end
146
159
 
@@ -148,9 +161,8 @@ class Promiscuous::Publisher::Operation::Atomic < Promiscuous::Publisher::Operat
148
161
  @instance = fetch_instance
149
162
  end
150
163
 
151
- def stash_version_in_document(version)
152
- # Overridden to update the query to set the version field with:
153
- # instance[Promiscuous::Config.version_field] = version
164
+ def increment_version_in_document
165
+ # Overridden to increment version field in the query
154
166
  end
155
167
 
156
168
  def use_id_selector(options={})