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.
- data/lib/entity_store/entity.rb +2 -0
- data/lib/entity_store/event_bus.rb +51 -22
- data/lib/entity_store/event_data_object.rb +1 -1
- data/lib/entity_store/external_store.rb +16 -6
- data/lib/entity_store/mongo_entity_store.rb +3 -3
- data/lib/entity_store/store.rb +9 -4
- data/lib/entity_store/version.rb +1 -1
- data/lib/entity_store.rb +2 -21
- data/spec/entity_store/event_bus_spec.rb +34 -6
- data/spec/entity_store/external_store_spec.rb +69 -43
- data/spec/entity_store/store_spec.rb +3 -2
- data/spec/entity_store_spec.rb +1 -8
- data/spec/spec_helper.rb +12 -0
- metadata +18 -2
data/lib/entity_store/entity.rb
CHANGED
@@ -1,36 +1,65 @@
|
|
1
1
|
module EntityStore
|
2
2
|
class EventBus
|
3
|
-
|
4
|
-
def publish(entity_type, event)
|
5
|
-
publish_externally entity_type, event
|
3
|
+
include Hatchet
|
6
4
|
|
7
|
-
|
5
|
+
def publish(entity_type, event)
|
6
|
+
publish_externally entity_type, event
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def subscribers_to(event_name)
|
21
|
+
subscribers.select { |s| s.instance_methods.include?(event_name.to_sym) }
|
22
|
+
end
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
def subscribers
|
25
|
+
EntityStore.event_subscribers
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
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
|
@@ -29,13 +29,23 @@ module EntityStore
|
|
29
29
|
)
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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 = {
|
38
|
-
|
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
|
-
|
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
|
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? }
|
data/lib/entity_store/store.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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|
|
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
|
data/lib/entity_store/version.rb
CHANGED
data/lib/entity_store.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module EntityStore
|
2
|
-
|
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
|
-
|
25
|
-
|
25
|
+
@event_bus.stub(:subscribers).and_return([DummySubscriber, @subscriber_class2])
|
26
|
+
@event_bus.stub(:publish_externally)
|
26
27
|
end
|
27
28
|
|
28
|
-
subject {
|
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
|
-
|
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
|
-
|
48
|
+
@event_bus.stub(:external_store) { @external_store }
|
48
49
|
end
|
49
50
|
|
50
|
-
subject {
|
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
|
-
@
|
40
|
-
@
|
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
|
-
|
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
|
-
|
56
|
-
|
51
|
+
context "when time passed as since" do
|
52
|
+
before(:each) do
|
53
|
+
@since = @reference_time
|
57
54
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
@
|
72
|
+
@type = "DummyEventTwo"
|
73
|
+
@results = subject
|
66
74
|
end
|
67
|
-
|
68
|
-
|
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
|
-
|
76
|
-
@
|
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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
72
|
+
@event_bus.should_receive(:publish).with(@entity.type, e)
|
72
73
|
end
|
73
74
|
subject
|
74
75
|
end
|
data/spec/entity_store_spec.rb
CHANGED
@@ -2,14 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe EntityStore do
|
4
4
|
describe ".setup" do
|
5
|
-
|
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
|
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-
|
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: []
|