entity_store 0.0.15 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,7 @@
1
1
  module EntityStore
2
2
  module Entity
3
+ include Hatchet
4
+
3
5
  attr_accessor :id
4
6
 
5
7
  def self.included(klass)
@@ -1,36 +1,65 @@
1
1
  module EntityStore
2
2
  class EventBus
3
- class << self
4
- def publish(entity_type, event)
5
- publish_externally entity_type, event
3
+ include Hatchet
6
4
 
7
- EntityStore.logger.debug { "publishing #{event.inspect}" }
5
+ def publish(entity_type, event)
6
+ publish_externally entity_type, event
8
7
 
9
- subscribers_to(event.receiver_name).each do |s|
10
- begin
11
- s.new.send(event.receiver_name, event)
12
- EntityStore.logger.debug { "called #{s.name}##{event.receiver_name} with #{event.inspect}" }
13
- rescue => e
14
- EntityStore.logger.error { "#{e.message} when calling #{s.name}##{event.receiver_name} with #{event.inspect}" }
15
- end
8
+ logger.debug { "publishing #{event.inspect}" }
9
+
10
+ subscribers_to(event.receiver_name).each do |s|
11
+ begin
12
+ s.new.send(event.receiver_name, event)
13
+ logger.debug { "called #{s.name}##{event.receiver_name} with #{event.inspect}" }
14
+ rescue => e
15
+ logger.error "#{e.message} when calling #{s.name}##{event.receiver_name} with #{event.inspect}", e
16
16
  end
17
17
  end
18
+ end
18
19
 
19
- def subscribers_to(event_name)
20
- subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
21
- end
20
+ def subscribers_to(event_name)
21
+ subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
22
+ end
22
23
 
23
- def subscribers
24
- EntityStore.event_subscribers
25
- end
24
+ def subscribers
25
+ EntityStore.event_subscribers
26
+ end
26
27
 
27
- def publish_externally(entity_type, event)
28
- external_store.add_event(entity_type, event)
29
- end
28
+ def publish_externally(entity_type, event)
29
+ external_store.add_event(entity_type, event)
30
+ end
31
+
32
+ def external_store
33
+ @_external_store ||= ExternalStore.new
34
+ end
30
35
 
31
- def external_store
32
- @_external_store ||= ExternalStore.new
36
+ # Public - replay events of a given type to a given subscriber
37
+ #
38
+ # since - Time reference point
39
+ # type - String type name of event
40
+ # subscriber - Class of the subscriber to replay events to
41
+ #
42
+ # Returns nothing
43
+ def replay(since, type, subscriber)
44
+ max_items = 100
45
+ event_data_objects = external_store.get_events(since, type, max_items)
46
+
47
+ while event_data_objects.count > 0 do
48
+ event_data_objects.each do |event_data_object|
49
+ begin
50
+ event = get_type_constant(event_data_object.type).new(event_data_object.attrs)
51
+ subscriber.new.send(event.receiver_name, event)
52
+ logger.info { "replayed #{event.inspect} to #{subscriber.name}##{event.receiver_name}" }
53
+ rescue => e
54
+ logger.error "#{e.message} when replaying #{event_data_object.inspect} to #{subscriber}", e
55
+ end
56
+ end
57
+ event_data_objects = external_store.get_events(event_data_objects.last.id, type, max_items)
33
58
  end
34
59
  end
60
+
61
+ def get_type_constant(type_name)
62
+ type_name.split('::').inject(Object) {|obj, name| obj.const_get(name) }
63
+ end
35
64
  end
36
65
  end
@@ -7,7 +7,7 @@ module EntityStore
7
7
  end
8
8
 
9
9
  def id
10
- attrs['_id']
10
+ attrs['_id'].to_s
11
11
  end
12
12
 
13
13
  def entity_type
@@ -29,13 +29,23 @@ module EntityStore
29
29
  )
30
30
  end
31
31
 
32
- def get_events(opts={})
33
- query = {}
34
- query['_id'] = { '$gt' => opts[:after] } if opts[:after]
35
- query['_type'] = opts[:type] if opts[:type]
32
+ # Public - get events since a Time or ID
33
+ #
34
+ # since - Time or String id to filter events from
35
+ # type - String optionally filter the event type to return (default=nil)
36
+ # max_items - Fixnum max items to return (default=100)
37
+ #
38
+ # Returns Enumerable EventDataObject
39
+ def get_events(since, type=nil, max_items=100)
40
+ since_id = since.is_a?(Time) ? BSON::ObjectId.from_time(since) : BSON::ObjectId.from_string(since)
41
+
42
+ query = { '_id' => { '$gt' => since_id } }
43
+ query['_type'] = type if type
36
44
 
37
- options = {:sort => [['_id', -1]]}
38
- options[:limit] = opts[:limit] || 100
45
+ options = {
46
+ :sort => [['_id', Mongo::ASCENDING]],
47
+ :limit => max_items
48
+ }
39
49
 
40
50
  collection.find(query, options).collect { |e| EventDataObject.new(e)}
41
51
  end
@@ -4,6 +4,7 @@ require 'uri'
4
4
  module EntityStore
5
5
  class MongoEntityStore
6
6
  include Mongo
7
+ include Hatchet
7
8
 
8
9
  def open_connection
9
10
  @db ||= open_store
@@ -79,7 +80,7 @@ module EntityStore
79
80
  begin
80
81
  event.apply(entity)
81
82
  rescue => e
82
- EntityStore.logger.error { "Failed to apply #{event.class.name} #{event.attributes} to #{id} with #{e.inspect}" }
83
+ logger.error "Failed to apply #{event.class.name} #{event.attributes} to #{id} with #{e.inspect}", e
83
84
  end
84
85
  entity.version = event.entity_version
85
86
  end
@@ -107,8 +108,7 @@ module EntityStore
107
108
  begin
108
109
  get_type_constant(attrs['_type']).new(attrs)
109
110
  rescue => e
110
- logger = Logger.new(STDERR)
111
- logger.error "Error loading type #{attrs['_type']}"
111
+ logger.error "Error loading type #{attrs['_type']}", e
112
112
  nil
113
113
  end
114
114
  end.select { |e| !e.nil? }
@@ -1,5 +1,7 @@
1
1
  module EntityStore
2
2
  class Store
3
+ include Hatchet
4
+
3
5
  def storage_client
4
6
  @_storage_client ||= MongoEntityStore.new
5
7
  end
@@ -9,7 +11,7 @@ module EntityStore
9
11
  add_events(entity)
10
12
  entity
11
13
  rescue => e
12
- EntityStore.logger.error { "Store#add error: #{e.inspect} - #{entity.inspect}" }
14
+ logger.error { "Store#add error: #{e.inspect} - #{entity.inspect}" }
13
15
  raise e
14
16
  end
15
17
 
@@ -33,12 +35,12 @@ module EntityStore
33
35
  end
34
36
  entity
35
37
  rescue => e
36
- EntityStore.logger.error { "Store#do_save error: #{e.inspect} - #{entity.inspect}" }
38
+ logger.error { "Store#do_save error: #{e.inspect} - #{entity.inspect}" }
37
39
  raise e
38
40
  end
39
41
 
40
42
  def snapshot_entity(entity)
41
- EntityStore.logger.info { "Store#snapshot_entity : Snapshotting #{entity.id}"}
43
+ logger.info { "Store#snapshot_entity : Snapshotting #{entity.id}"}
42
44
  storage_client.snapshot_entity(entity)
43
45
  end
44
46
 
@@ -52,7 +54,7 @@ module EntityStore
52
54
  e.entity_version = entity.version
53
55
  storage_client.add_event(e)
54
56
  end
55
- entity.pending_events.each {|e| EventBus.publish(entity.type, e) }
57
+ entity.pending_events.each {|e| event_bus.publish(entity.type, e) }
56
58
  entity.clear_pending_events
57
59
  end
58
60
 
@@ -77,5 +79,8 @@ module EntityStore
77
79
  @_storage_client = nil
78
80
  end
79
81
 
82
+ def event_bus
83
+ @_event_bus ||= EventBus.new
84
+ end
80
85
  end
81
86
  end
@@ -1,3 +1,3 @@
1
1
  module EntityStore
2
- VERSION = "0.0.15".freeze
2
+ VERSION = "0.1.0".freeze
3
3
  end
data/lib/entity_store.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module EntityStore
2
- require 'logger'
2
+
3
+ require 'hatchet'
3
4
  require 'entity_store/entity'
4
5
  require 'entity_store/entity_value'
5
6
  require 'entity_store/event'
@@ -47,26 +48,6 @@ module EntityStore
47
48
  @_snapshot_threshold = value
48
49
  end
49
50
 
50
- def log_level
51
- @_log_level ||= Logger::INFO
52
- end
53
-
54
- def log_level=(value)
55
- @_log_level = value
56
- end
57
-
58
- def logger
59
- unless @_logger
60
- @_logger = Logger.new(STDOUT)
61
- @_logger.progname = "Entity_Store"
62
- @_logger.level = log_level
63
- end
64
- @_logger
65
- end
66
-
67
- def logger=(value)
68
- @_logger = value
69
- end
70
51
  end
71
52
 
72
53
 
@@ -15,17 +15,18 @@ describe EventBus do
15
15
  before(:each) do
16
16
  @entity_type = random_string
17
17
  @event = DummyEvent.new(:name => random_string)
18
+ @event_bus = EventBus.new
18
19
  end
19
20
  describe ".publish" do
20
21
  before(:each) do
21
22
  @subscriber = mock("Subscriber", :dummy_event => true)
22
23
  DummySubscriber.stub(:new) { @subscriber }
23
24
  @subscriber_class2 = mock("SubscriberClass", :instance_methods => ['bilge'], :name => "SubscriberClass")
24
- EventBus.stub(:subscribers).and_return([DummySubscriber, @subscriber_class2])
25
- EventBus.stub(:publish_externally)
25
+ @event_bus.stub(:subscribers).and_return([DummySubscriber, @subscriber_class2])
26
+ @event_bus.stub(:publish_externally)
26
27
  end
27
28
 
28
- subject { EventBus.publish(@entity_type, @event) }
29
+ subject { @event_bus.publish(@entity_type, @event) }
29
30
 
30
31
  it "calls the receiver method on the subscriber" do
31
32
  @subscriber.should_receive(:dummy_event).with(@event)
@@ -36,7 +37,7 @@ describe EventBus do
36
37
  subject
37
38
  end
38
39
  it "publishes event to the external event push" do
39
- EventBus.should_receive(:publish_externally).with(@entity_type, @event)
40
+ @event_bus.should_receive(:publish_externally).with(@entity_type, @event)
40
41
  subject
41
42
  end
42
43
  end
@@ -44,14 +45,41 @@ describe EventBus do
44
45
  describe ".publish_externally" do
45
46
  before(:each) do
46
47
  @external_store = mock(ExternalStore)
47
- EventBus.stub(:external_store) { @external_store }
48
+ @event_bus.stub(:external_store) { @external_store }
48
49
  end
49
50
 
50
- subject { EventBus.publish_externally @entity_type, @event }
51
+ subject { @event_bus.publish_externally @entity_type, @event }
51
52
 
52
53
  it "should publish to the external store" do
53
54
  @external_store.should_receive(:add_event).with(@entity_type, @event)
54
55
  subject
55
56
  end
56
57
  end
58
+
59
+ describe "#replay" do
60
+ before(:each) do
61
+ @since = random_time
62
+ @type = 'DummyEvent'
63
+ @subscriber = mock("Subscriber", :dummy_event => true)
64
+ DummySubscriber.stub(:new) { @subscriber }
65
+
66
+ @external_store = mock(ExternalStore)
67
+ @id = random_object_id
68
+ @external_store.stub(:get_events) { |since| since == @id ? [] : [
69
+ EventDataObject.new('_id' => @id, '_type' => DummyEvent.name, 'name' => random_string)
70
+ ]}
71
+ @event_bus.stub(:external_store) { @external_store }
72
+ end
73
+
74
+ subject { @event_bus.replay(@since, @type, DummySubscriber) }
75
+
76
+ it "gets the events for that period" do
77
+ @external_store.should_receive(:get_events).with(@since, @type, 100)
78
+ subject
79
+ end
80
+ it "publishes them to the subscriber" do
81
+ @subscriber.should_receive(:dummy_event).with(an_instance_of(DummyEvent))
82
+ subject
83
+ end
84
+ end
57
85
  end
@@ -36,63 +36,89 @@ describe ExternalStore do
36
36
 
37
37
  describe "#get_events" do
38
38
  before(:each) do
39
- @entity_type = random_string
40
- @events = [
41
- DummyEvent.new(:name => random_string, :entity_id => random_object_id),
42
- DummyEventTwo.new(:name => random_string, :entity_id => random_object_id),
43
- DummyEvent.new(:name => random_string, :entity_id => random_object_id),
44
- DummyEventTwo.new(:name => random_string, :entity_id => random_object_id),
45
- DummyEvent.new(:name => random_string, :entity_id => random_object_id)
46
- ]
47
-
48
- @events.each { |e| @store.add_event(@entity_type, e)}
49
- end
50
-
51
- context "when no options" do
39
+ @reference_time = Time.now
40
+ @ids = (-2..2).collect { |i| BSON::ObjectId.from_time(@reference_time + i) }
52
41
 
53
- subject { @store.get_events }
42
+ @store.collection.insert({'_id' => @ids[0], '_type' => 'DummyEvent'})
43
+ @store.collection.insert({'_id' => @ids[1], '_type' => 'DummyEventTwo'})
44
+ @store.collection.insert({'_id' => @ids[2], '_type' => 'DummyEvent'})
45
+ @store.collection.insert({'_id' => @ids[3], '_type' => 'DummyEventTwo'})
46
+ @store.collection.insert({'_id' => @ids[4], '_type' => 'DummyEvent'})
47
+ end
48
+
49
+ subject { @store.get_events @since, @type }
54
50
 
55
- it "returns all of the events" do
56
- subject.count.should eq(@events.count)
51
+ context "when time passed as since" do
52
+ before(:each) do
53
+ @since = @reference_time
57
54
  end
58
- end
59
-
60
- context "when options passed" do
61
- subject { @store.get_events(@options) }
62
-
63
- context "when limit option passed" do
55
+ context "when no type filter" do
56
+ before(:each) do
57
+ @type = nil
58
+ @results = subject
59
+ end
60
+ it "returns two records" do
61
+ @results.count.should eq(2)
62
+ end
63
+ it "it returns the 4th item first" do
64
+ @results.first.id.should eq(@ids[3].to_s)
65
+ end
66
+ it "it returns the 5th item second" do
67
+ @results[1].id.should eq(@ids[4].to_s)
68
+ end
69
+ end
70
+ context "when type filter 'DummyEventTwo' passed" do
64
71
  before(:each) do
65
- @options = {:limit => 3}
72
+ @type = "DummyEventTwo"
73
+ @results = subject
66
74
  end
67
-
68
- it "returns limited records records" do
69
- subject.count.should eq(@options[:limit])
75
+ it "returns 1 record" do
76
+ @results.count.should eq(1)
70
77
  end
78
+ it "returns the 4th item" do
79
+ @results.first.id.should eq(@ids[3].to_s)
80
+ end
81
+ end
82
+ end
83
+
84
+ context "when id passed as since" do
85
+ before(:each) do
86
+ @since = @ids[1].to_s
71
87
  end
72
-
73
- context "when after index passed" do
88
+ context "when no type filter passed" do
74
89
  before(:each) do
75
- items = @store.get_events(:limit => 3)
76
- @options = {:after => items[2].id}
90
+ @type = nil
91
+ @results = subject
92
+ end
93
+ it "returns 3 records" do
94
+ @results.count.should eq(3)
95
+ end
96
+ it "it returns the 3rd item first" do
97
+ @results.first.id.should eq(@ids[2].to_s)
77
98
  end
78
-
79
- it "returns limited records records" do
80
- subject.count.should eq(2)
99
+ it "it returns the 4th item second" do
100
+ @results[1].id.should eq(@ids[3].to_s)
81
101
  end
82
- end
83
-
84
- context "when type passed" do
102
+ it "it returns the 5th item second" do
103
+ @results[2].id.should eq(@ids[4].to_s)
104
+ end
105
+ end
106
+ context "when type filter 'DummyEvent' passed" do
85
107
  before(:each) do
86
- @options = {:type => @events[2].class.name}
108
+ @type = "DummyEvent"
109
+ @results = subject
110
+ end
111
+ it "returns 2 records" do
112
+ @results.count.should eq(2)
87
113
  end
88
-
89
- it "returns type records records" do
90
- subject.count.should eq(3)
114
+ it "returns the 3rd item" do
115
+ @results.first.id.should eq(@ids[2].to_s)
91
116
  end
92
- end
93
-
117
+ it "returns the 5th item" do
118
+ @results[1].id.should eq(@ids[4].to_s)
119
+ end
120
+ end
94
121
  end
95
-
96
122
  end
97
123
 
98
124
  end
@@ -43,7 +43,8 @@ describe Store do
43
43
  @storage_client = mock("StorageClient", :add_event => true)
44
44
  @store = Store.new
45
45
  @store.stub(:storage_client) { @storage_client }
46
- EventBus.stub(:publish)
46
+ @event_bus = mock(EventBus, :publish => true)
47
+ @store.stub(:event_bus) { @event_bus}
47
48
  end
48
49
 
49
50
  subject { @store.add_events(@entity) }
@@ -68,7 +69,7 @@ describe Store do
68
69
  end
69
70
  it "publishes each event to the EventBus" do
70
71
  @entity.pending_events.each do |e|
71
- EventBus.should_receive(:publish).with(@entity.type, e)
72
+ @event_bus.should_receive(:publish).with(@entity.type, e)
72
73
  end
73
74
  subject
74
75
  end
@@ -2,14 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe EntityStore do
4
4
  describe ".setup" do
5
- before(:each) do
6
- EntityStore.setup do |config|
7
- config.log_level = Logger::WARN
8
- end
9
- end
10
- it "has a log_level of WARN" do
11
- EntityStore.log_level.should eq(Logger::WARN)
12
- end
5
+
13
6
  end
14
7
 
15
8
  end
data/spec/spec_helper.rb CHANGED
@@ -6,6 +6,18 @@ RSpec.configure do |config|
6
6
  config.color_enabled = true
7
7
  end
8
8
 
9
+ Hatchet.configure do |config|
10
+ # Reset the logging configuration
11
+ config.reset!
12
+ config.level :error
13
+ # Use the format without time, etc so we don't duplicate it
14
+ config.formatter = Hatchet::SimpleFormatter.new
15
+ # Set up a STDOUT appender
16
+ config.appenders << Hatchet::LoggerAppender.new do |appender|
17
+ appender.logger = Logger.new(STDOUT)
18
+ end
19
+ end
20
+
9
21
  include EntityStore
10
22
 
11
23
  def random_string
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: entity_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-31 00:00:00.000000000 Z
12
+ date: 2012-11-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongo
@@ -43,6 +43,22 @@ dependencies:
43
43
  - - ~>
44
44
  - !ruby/object:Gem::Version
45
45
  version: '1.6'
46
+ - !ruby/object:Gem::Dependency
47
+ name: hatchet
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.0.20
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.0.20
46
62
  description: Event sourced entity store with a Mongo body
47
63
  email: adam.bird@gmail.com
48
64
  executables: []