active_projection 0.5.0.rc3 → 0.5.0.rc4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a3300767c58d38551a2844322cece40fcfdbd2df
4
- data.tar.gz: 0c7d4bae4ef6276563e64e15d0a2f7a05cc6dff4
3
+ metadata.gz: b9efdd612a791bf02b6a1f3f809ffffca7c7841b
4
+ data.tar.gz: 8596080a556b2d9639534923c00589cb55cc9235
5
5
  SHA512:
6
- metadata.gz: 4a9dd96e797d670fc8b11e53202f12a786258a9e266cd6b988d7568a3a297a4ae552ce2a66d79170a1bc7156ff10f934317e6c760c1f805001edb7153693138e
7
- data.tar.gz: fd00e8748edfe21a30fcf6f548c499d8b0d071e15ed46dea43002c1413dc1dd9d51f6c61ad5e3e696713c09caae73df3252030915795b2a8018988bb782477f4
6
+ metadata.gz: 11fe3536a9b6318bbd5a34970cf2a949c5c0c9ab13f5c0e2436c03d19e1183289737567de9b4c96dcbddd026c0b69710a2ff9321e126eb3ac4ba64c5c9954629
7
+ data.tar.gz: f32df5da5a87329fe6e5c47a9027788460bf28c56b78245eb0bd9fc7843ac26c86d3dbcdaeb474dbba7807b036f98cc23a88c391660ed0d3a0c7b743cd64569e
@@ -0,0 +1,55 @@
1
+ module ActiveProjection
2
+ #
3
+ # This implements a fully cached projection repository.
4
+ #
5
+ # notice: NOT thread safe!
6
+ class CachedProjectionRepository
7
+ def self.last_ids
8
+ projections.values.map(&:last_id)
9
+ end
10
+
11
+ def self.get_last_id(id)
12
+ projections[id].last_id
13
+ end
14
+
15
+ def self.set_last_id(id, last_id)
16
+ projections[id].update! last_id: last_id
17
+ end
18
+
19
+ def self.set_broken(id)
20
+ projections[id].update! solid: false
21
+ end
22
+
23
+ def self.get_all_broken
24
+ projections.values.reject(&:solid).map(&:class_name)
25
+ end
26
+
27
+ def self.solid?(id)
28
+ projections[id].solid
29
+ end
30
+
31
+ def self.ensure_exists(projection_class)
32
+ Projection.transaction do
33
+ projection = Projection.find_or_create_by! class_name: projection_class
34
+ projections[projection.id] = projection
35
+ end
36
+ end
37
+
38
+ def self.ensure_solid(projection_class)
39
+ Projection.transaction do
40
+ projection = Projection.find_or_initialize_by class_name: projection_class
41
+ projection.update! solid: true
42
+ projections[projection.id] = projection
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def self.initialize
49
+ end
50
+
51
+ cattr_accessor :projections do
52
+ {}
53
+ end
54
+ end
55
+ end
@@ -1,51 +1,45 @@
1
1
  module ActiveProjection
2
2
  class ProjectionRepository
3
-
4
3
  def self.last_ids
5
4
  Projection.all.to_a.map { |p| p.last_id }
6
5
  end
7
6
 
8
7
  def self.get_last_id(id)
9
- @@last_id[id] ||= Projection.find(id).last_id
8
+ Projection.find(id).last_id
10
9
  end
11
10
 
12
11
  def self.set_last_id(id, last_id)
13
12
  Projection.find(id).update! last_id: last_id
14
- @@last_id[id] += 1
15
13
  end
16
14
 
17
15
  def self.set_broken(id)
18
- @@broken[id] = false
19
16
  Projection.find(id).update! solid: false
20
17
  end
21
18
 
22
19
  def self.get_all_broken
23
- broken_projections = []
24
- Projection.all.to_a.each do |p|
25
- broken_projections << p.class_name unless p.solid?
26
- end
27
- broken_projections
20
+ Projection.where(solid: false).to_a.map { |p| p.class_name }
28
21
  end
29
22
 
30
23
  def self.solid?(id)
31
- @@broken[id] ||= Projection.find(id).solid
24
+ Projection.find(id).solid
32
25
  end
33
26
 
34
- def self.create_or_get(projection_class)
35
- projection = Projection.where(class_name: projection_class).first
36
- if projection.nil?
37
- projection = Projection.create! class_name: projection_class, last_id: 0, solid: true
38
- else
27
+ def self.ensure_exists(projection_class)
28
+ Projection.transaction do
29
+ Projection.find_or_create_by! class_name: projection_class
30
+ end
31
+ end
32
+
33
+ def self.ensure_solid(projection_class)
34
+ Projection.transaction do
35
+ projection = Projection.find_or_initialize_by class_name: projection_class
39
36
  projection.update! solid: true
40
37
  end
41
- projection
42
38
  end
43
39
 
44
40
  private
41
+
45
42
  def self.initialize
46
43
  end
47
-
48
- @@last_id = {}
49
- @@broken = {}
50
44
  end
51
- end
45
+ end
@@ -2,8 +2,8 @@ class CreateProjections < ActiveRecord::Migration
2
2
  def change
3
3
  create_table :projections do |t|
4
4
  t.string :class_name
5
- t.integer :last_id
6
- t.boolean :solid
5
+ t.integer :last_id, default: 0
6
+ t.boolean :solid, default: true
7
7
  end
8
8
  add_index :projections, :class_name
9
9
  end
@@ -3,40 +3,7 @@ module ActiveProjection
3
3
  include ActiveEvent::Support::Autoload
4
4
  private
5
5
  def self.dir_names
6
- %W(app/models)
7
- end
8
-
9
- def self.reload
10
- ActiveEvent::Support::Autoloader.reload_from pdirs
11
- super
12
- end
13
-
14
- def self.worker_config=(config)
15
- set_pdirs config
16
- ActiveEvent::Support::Autoloader.load_from pdirs
17
- end
18
-
19
- def self.watchable_dirs
20
- watchable_dirs = super
21
- watchable_dirs['app/projections'] = [:rb]
22
- watchable_dirs
23
- end
24
-
25
- private
26
- def self.set_pdirs(config)
27
- @pdirs = []
28
- projections_dir = "#{config[:path]}/app/projections/**/*.rb"
29
- if config[:count] == 1
30
- @pdirs << projections_dir
31
- else
32
- Dir[*projections_dir].each_with_index.map { |item, i| i % config[:count] == config[:number] ? item : nil }.compact.each do |dir|
33
- @pdirs << dir
34
- end
35
- end
36
- end
37
-
38
- def self.pdirs
39
- @pdirs ||= []
6
+ %W(app/models app/projections)
40
7
  end
41
8
  end
42
- end
9
+ end
@@ -1,77 +1,139 @@
1
1
  require 'singleton'
2
2
  require 'bunny'
3
+
3
4
  module ActiveProjection
4
5
  class EventClient
5
6
  include Singleton
6
7
 
7
8
  def self.start(options)
8
- instance.options = options
9
- instance.start
9
+ instance.configure(options).start
10
+ end
11
+
12
+ def configure(options)
13
+ raise 'Unsupported! Cannot configure running client' if running
14
+ self.options = options
15
+ self
10
16
  end
11
17
 
12
18
  def start
13
- event_connection.start
14
- sync_projections
15
- listen_for_events
16
- request_missing_events
17
- event_channel.work_pool.join
19
+ run_once do
20
+ prepare
21
+ sync_projections
22
+ listen_for_events
23
+ listen_for_replayed_events
24
+ request_missing_events
25
+ event_channel.work_pool.join
26
+ end
18
27
  rescue Interrupt
19
28
  LOGGER.info 'Catching Interrupt'
20
29
  rescue Exception => e
21
30
  LOGGER.error e.message
22
31
  LOGGER.error e.backtrace.join("\n")
23
- raise e
32
+ raise
24
33
  end
25
34
 
26
- def sync_projections
27
- ProjectionTypeRegistry.sync_projections
35
+ private
36
+
37
+ attr_accessor :options
38
+ attr_accessor :running # true once start was called
39
+ attr_accessor :current # true after missing events are processed
40
+ attr_accessor :delay_queue # stores events while processing missing events
41
+
42
+ def run_once
43
+ raise 'Unsupported! Connot start a running client' if running
44
+ self.running = true
45
+ yield
46
+ ensure
47
+ self.running = false
28
48
  end
29
49
 
30
- def listen_for_events
31
- subscribe_to event_queue do |delivery_info, properties, body|
32
- event_received properties, body
33
- send_browser_notification properties.headers[:id]
50
+ def prepare
51
+ self.delay_queue = []
52
+ init_database_connection
53
+ event_connection.start
54
+ end
55
+
56
+ def init_database_connection
57
+ ActiveRecord::Base.establish_connection options[:projection_database]
58
+ end
59
+
60
+ def sync_projections
61
+ server_projections.each do |projection|
62
+ CachedProjectionRepository.ensure_solid(projection.class.name)
34
63
  end
35
64
  end
36
65
 
37
- def request_missing_events
38
- listen_for_replayed_events
39
- send_request_for min_last_id
66
+ def server_projections
67
+ @server_projections ||= ProjectionTypeRegistry.projections.drop(WORKER_NUMBER).each_slice(WORKER_COUNT).map(&:first)
68
+ end
69
+
70
+ def listen_for_events
71
+ event_queue.subscribe do |delivery_info, properties, body|
72
+ if current
73
+ event_received properties, body
74
+ else
75
+ delay_queue << [properties, body]
76
+ end
77
+ end
40
78
  end
41
79
 
42
80
  def listen_for_replayed_events
43
- subscribe_to replay_queue do |delivery_info, properties, body|
44
- event_received properties, body unless replay_done? body
81
+ replay_queue.subscribe do |delivery_info, properties, body|
82
+ if 'replay_done' == body
83
+ replay_done
84
+ else
85
+ event_received properties, body
86
+ end
45
87
  end
46
88
  end
47
89
 
48
- def send_browser_notification(id)
49
- server_side_events_exchange.publish id.to_s
90
+ def request_missing_events
91
+ send_request_for(CachedProjectionRepository.last_ids.min || 0)
92
+ end
93
+
94
+ def send_projection_notification(event_id, projection, error = nil)
95
+ message = {event: event_id, projection: projection.class.name}
96
+ message.merge error: error.message, backtrace: error.backtrace if error
97
+ server_side_events_exchange.publish message.to_json
50
98
  end
51
99
 
52
100
  def send_request_for(id)
53
101
  resend_request_exchange.publish id.to_s, routing_key: 'resend_request'
54
102
  end
55
103
 
56
- def replay_done?(body)
57
- if 'replay_done' == body
58
- LOGGER.debug 'All replayed events received'
59
- broken_projections = ProjectionRepository.get_all_broken
60
- LOGGER.error "These projections are still broken: #{broken_projections.join(", ")}" unless broken_projections.empty?
61
- replay_queue.unbind(resend_exchange)
62
- return true
63
- end
64
- false
104
+ def replay_done
105
+ LOGGER.debug 'All replayed events received'
106
+ broken_projections = CachedProjectionRepository.get_all_broken
107
+ LOGGER.error "These projections are still broken: #{broken_projections.join(", ")}" unless broken_projections.empty?
108
+ replay_queue.unbind(resend_exchange)
109
+ self.current = true
110
+ flush_delay_queue
111
+ end
112
+
113
+ def flush_delay_queue
114
+ delay_queue.each { |properties, body| event_received properties, body }
115
+ self.delay_queue = []
65
116
  end
66
117
 
67
118
  def event_received(properties, body)
68
119
  RELOADER.execute_if_updated
69
120
  LOGGER.debug "Received #{properties.type} with #{body}"
70
- ProjectionTypeRegistry.process properties.headers.deep_symbolize_keys!, build_event(properties.type, JSON.parse(body))
71
- end
72
-
73
- def build_event(type, data)
74
- Object.const_get(type).new(data.deep_symbolize_keys)
121
+ headers = properties.headers.deep_symbolize_keys!
122
+ event = ActiveEvent::EventType.create_instance properties.type, JSON.parse(body).deep_symbolize_keys!
123
+ process_event headers, event
124
+ end
125
+
126
+ def process_event(headers, event)
127
+ server_projections.select { |p| p.evaluate headers }.each do |projection|
128
+ begin
129
+ ActiveRecord::Base.transaction do
130
+ projection.invoke event, headers
131
+ end
132
+ send_projection_notification headers[:id], projection
133
+ rescue Exception => e
134
+ send_projection_notification headers[:id], projection, e
135
+ end
136
+ end
75
137
  end
76
138
 
77
139
  def replay_queue
@@ -82,14 +144,6 @@ module ActiveProjection
82
144
  @event_queue ||= event_channel.queue('', auto_delete: true).bind(event_exchange)
83
145
  end
84
146
 
85
- def min_last_id
86
- ProjectionRepository.last_ids.min || 0
87
- end
88
-
89
- def subscribe_to(queue, &block)
90
- queue.subscribe(&block)
91
- end
92
-
93
147
  def event_connection
94
148
  @event_server ||= Bunny.new URI::Generic.build(options[:event_connection]).to_s
95
149
  end
@@ -113,11 +167,5 @@ module ActiveProjection
113
167
  def server_side_events_exchange
114
168
  @server_side_events_exchange ||= event_channel.fanout "server_side_#{options[:event_exchange]}"
115
169
  end
116
-
117
- def options
118
- @options
119
- end
120
-
121
- attr_writer :options
122
170
  end
123
- end
171
+ end
@@ -52,12 +52,11 @@ module ActiveProjection
52
52
  rescue Exception => e
53
53
  LOGGER.error "[#{self.class.name}]: error processing #{event_type}[#{event_id}]\n#{e.message}\n#{e.backtrace}"
54
54
  set_broken
55
+ raise
55
56
  end
56
57
  end
57
- if solid?
58
- update_last_id event_id
59
- LOGGER.debug "[#{self.class.name}]: sucessfully processed #{event_type}[#{event_id}]"
60
- end
58
+ update_last_id event_id
59
+ LOGGER.debug "[#{self.class.name}]: sucessfully processed #{event_type}[#{event_id}]"
61
60
  end
62
61
 
63
62
  private
@@ -69,7 +68,7 @@ module ActiveProjection
69
68
  end
70
69
 
71
70
  def projection_id
72
- @projection_id ||= ProjectionRepository.create_or_get(self.class.name).id
71
+ @projection_id ||= ProjectionRepository.ensure_exists(self.class.name).id
73
72
  end
74
73
 
75
74
  def solid?
@@ -4,45 +4,25 @@ module ActiveProjection
4
4
  class ProjectionTypeRegistry
5
5
  include Singleton
6
6
 
7
+ # register a new projection class
8
+ #
9
+ # The best way to create a new projection is using the ProjectionType module
10
+ # This module automatically registers each class
7
11
  def self.register(projection)
8
12
  self.registry << projection
9
13
  end
10
14
 
11
- def self.process(headers, event)
12
- instance.process headers, event
15
+ # @return an enumerable with all projections
16
+ def self.projections
17
+ instance.projections.each
13
18
  end
14
19
 
15
- def process(headers, event)
16
- projections.each do |projection|
17
- ActiveRecord::Base.transaction do
18
- projection.invoke(event, headers) if projection.evaluate headers
19
- end
20
- end
21
- end
22
-
23
- def self.sync_projections
24
- instance.sync_projections
25
- end
26
-
27
- def sync_projections
28
- projections.each do |projection|
29
- ProjectionRepository.create_or_get(projection.class.name)
30
- end
20
+ def projections
21
+ @projections ||= self.class.registry.freeze.map { |projection_class| projection_class.new }.freeze
31
22
  end
32
23
 
33
24
  private
34
25
 
35
- cattr_accessor :registry
36
- attr_accessor :projections
37
-
38
- def initialize
39
- self.projections = []
40
- self.class.registry.freeze.each do |projection|
41
- projections << projection.new
42
- end
43
- projections.freeze
44
- end
45
-
46
- self.registry = []
26
+ cattr_accessor(:registry) { [] }
47
27
  end
48
- end
28
+ end
@@ -12,7 +12,6 @@ module ActiveProjection
12
12
  end
13
13
 
14
14
  def run
15
- ActiveRecord::Base.establish_connection options[:projection_database]
16
15
  EventClient.start options
17
16
  end
18
17
 
@@ -1,7 +1,7 @@
1
1
  module ActiveProjection
2
2
  # Returns the version of the currently loaded ActiveProjection as a Gem::Version
3
3
  def self.version
4
- Gem::Version.new '0.5.0.rc3'
4
+ Gem::Version.new '0.5.0.rc4'
5
5
  end
6
6
 
7
7
  module VERSION #:nodoc:
@@ -13,4 +13,5 @@ module ActiveProjection
13
13
 
14
14
  autoload :Projection, (File.expand_path '../../app/models/active_projection/projection', __FILE__)
15
15
  autoload :ProjectionRepository, (File.expand_path '../../app/models/active_projection/projection_repository', __FILE__)
16
+ autoload :CachedProjectionRepository, (File.expand_path '../../app/models/active_projection/cached_projection_repository', __FILE__)
16
17
  end
@@ -22,7 +22,7 @@ describe ActiveProjection::ProjectionTypeRegistry do
22
22
  end
23
23
 
24
24
  it 'synchronizes the known projections with the db' do
25
- expect(ActiveProjection::ProjectionRepository).to receive(:create_or_get).with('TestProjection')
25
+ expect(ActiveProjection::ProjectionRepository).to receive(:ensure_exists).with('TestProjection')
26
26
  ActiveProjection::ProjectionTypeRegistry.sync_projections
27
27
  end
28
28
  end
@@ -7,14 +7,14 @@ describe ActiveProjection::ProjectionRepository do
7
7
  FactoryGirl.create :projection, id: i, class_name: "TestProjection#{i}", last_id: 5 - i
8
8
  end
9
9
  end
10
- describe 'create_or_get' do
10
+ describe 'ensure_exists' do
11
11
  it 'creates a projection, if classname is not present' do
12
- expect(ActiveProjection::ProjectionRepository.create_or_get('TestProjection5').id).to eq 5
12
+ expect(ActiveProjection::ProjectionRepository.ensure_exists('TestProjection5').id).to eq 5
13
13
  expect(ActiveProjection::Projection.all.to_a.length).to eq 6
14
14
  end
15
15
 
16
16
  it 'does not create a projection, if classname is present' do
17
- expect(ActiveProjection::ProjectionRepository.create_or_get('TestProjection3').id).to eq 3
17
+ expect(ActiveProjection::ProjectionRepository.ensure_exists('TestProjection3').id).to eq 3
18
18
  expect(ActiveProjection::Projection.all.to_a.length).to eq 5
19
19
  end
20
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_projection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0.rc3
4
+ version: 0.5.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - HicknHack Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-24 00:00:00.000000000 Z
11
+ date: 2014-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_event
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.5.0.rc3
19
+ version: 0.5.0.rc4
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.5.0.rc3
26
+ version: 0.5.0.rc4
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -159,6 +159,7 @@ executables: []
159
159
  extensions: []
160
160
  extra_rdoc_files: []
161
161
  files:
162
+ - app/models/active_projection/cached_projection_repository.rb
162
163
  - app/models/active_projection/projection.rb
163
164
  - app/models/active_projection/projection_repository.rb
164
165
  - db/migrate/01_create_projections.rb