aggro 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -0
  3. data/.travis.yml +15 -0
  4. data/Gemfile +9 -0
  5. data/README.md +5 -1
  6. data/Rakefile +10 -0
  7. data/aggro.gemspec +8 -1
  8. data/lib/aggro.rb +191 -7
  9. data/lib/aggro/abstract_store.rb +12 -0
  10. data/lib/aggro/aggregate.rb +98 -0
  11. data/lib/aggro/aggregate_ref.rb +68 -6
  12. data/lib/aggro/attribute_dsl.rb +96 -0
  13. data/lib/aggro/binding_dsl.rb +45 -0
  14. data/lib/aggro/block_helper.rb +14 -0
  15. data/lib/aggro/channel.rb +37 -0
  16. data/lib/aggro/client.rb +12 -0
  17. data/lib/aggro/cluster_config.rb +57 -0
  18. data/lib/aggro/command.rb +16 -0
  19. data/lib/aggro/concurrent_actor.rb +26 -0
  20. data/lib/aggro/event_bus.rb +94 -0
  21. data/lib/aggro/event_dsl.rb +53 -0
  22. data/lib/aggro/event_proxy.rb +23 -0
  23. data/lib/aggro/event_serializer.rb +14 -0
  24. data/lib/aggro/file_store.rb +97 -0
  25. data/lib/aggro/file_store/reader.rb +21 -0
  26. data/lib/aggro/file_store/writer.rb +27 -0
  27. data/lib/aggro/handler/command.rb +60 -0
  28. data/lib/aggro/handler/create_aggregate.rb +42 -0
  29. data/lib/aggro/handler/get_events.rb +30 -0
  30. data/lib/aggro/handler/query.rb +60 -0
  31. data/lib/aggro/handler/start_saga.rb +56 -0
  32. data/lib/aggro/local_node.rb +28 -0
  33. data/lib/aggro/locator.rb +32 -0
  34. data/lib/aggro/message/ask.rb +16 -0
  35. data/lib/aggro/message/command.rb +36 -0
  36. data/lib/aggro/message/create_aggregate.rb +16 -0
  37. data/lib/aggro/message/endpoint.rb +16 -0
  38. data/lib/aggro/message/events.rb +24 -0
  39. data/lib/aggro/message/get_events.rb +16 -0
  40. data/lib/aggro/message/heartbeat.rb +16 -0
  41. data/lib/aggro/message/invalid_target.rb +20 -0
  42. data/lib/aggro/message/ok.rb +20 -0
  43. data/lib/aggro/message/publisher_endpoint_inquiry.rb +16 -0
  44. data/lib/aggro/message/query.rb +36 -0
  45. data/lib/aggro/message/result.rb +16 -0
  46. data/lib/aggro/message/start_saga.rb +28 -0
  47. data/lib/aggro/message/unhandled_operation.rb +20 -0
  48. data/lib/aggro/message/unknown_operation.rb +20 -0
  49. data/lib/aggro/message_parser.rb +10 -0
  50. data/lib/aggro/message_router.rb +26 -0
  51. data/lib/aggro/nanomsg_transport.rb +31 -0
  52. data/lib/aggro/nanomsg_transport/client.rb +35 -0
  53. data/lib/aggro/nanomsg_transport/connection.rb +98 -0
  54. data/lib/aggro/nanomsg_transport/publish.rb +17 -0
  55. data/lib/aggro/nanomsg_transport/publisher.rb +37 -0
  56. data/lib/aggro/nanomsg_transport/raw_reply.rb +18 -0
  57. data/lib/aggro/nanomsg_transport/raw_request.rb +18 -0
  58. data/lib/aggro/nanomsg_transport/reply.rb +17 -0
  59. data/lib/aggro/nanomsg_transport/request.rb +17 -0
  60. data/lib/aggro/nanomsg_transport/server.rb +84 -0
  61. data/lib/aggro/nanomsg_transport/socket_error.rb +20 -0
  62. data/lib/aggro/nanomsg_transport/subscribe.rb +27 -0
  63. data/lib/aggro/nanomsg_transport/subscriber.rb +82 -0
  64. data/lib/aggro/node.rb +29 -0
  65. data/lib/aggro/node_list.rb +39 -0
  66. data/lib/aggro/projection.rb +13 -0
  67. data/lib/aggro/query.rb +11 -0
  68. data/lib/aggro/saga.rb +94 -0
  69. data/lib/aggro/saga_runner.rb +87 -0
  70. data/lib/aggro/saga_runner/start_saga.rb +12 -0
  71. data/lib/aggro/saga_status.rb +29 -0
  72. data/lib/aggro/server.rb +88 -0
  73. data/lib/aggro/subscriber.rb +48 -0
  74. data/lib/aggro/subscription.rb +41 -0
  75. data/lib/aggro/transform/boolean.rb +16 -0
  76. data/lib/aggro/transform/email.rb +26 -0
  77. data/lib/aggro/transform/id.rb +34 -0
  78. data/lib/aggro/transform/integer.rb +22 -0
  79. data/lib/aggro/transform/money.rb +22 -0
  80. data/lib/aggro/transform/noop.rb +16 -0
  81. data/lib/aggro/transform/string.rb +16 -0
  82. data/lib/aggro/transform/time_interval.rb +24 -0
  83. data/lib/aggro/version.rb +1 -1
  84. data/spec/lib/aggro/abstract_store_spec.rb +15 -0
  85. data/spec/lib/aggro/aggregate_ref_spec.rb +63 -12
  86. data/spec/lib/aggro/aggregate_spec.rb +207 -0
  87. data/spec/lib/aggro/channel_spec.rb +87 -0
  88. data/spec/lib/aggro/client_spec.rb +26 -0
  89. data/spec/lib/aggro/cluster_config_spec.rb +33 -0
  90. data/spec/lib/aggro/command_spec.rb +52 -0
  91. data/spec/lib/aggro/concurrent_actor_spec.rb +44 -0
  92. data/spec/lib/aggro/event_bus_spec.rb +20 -0
  93. data/spec/lib/aggro/event_serializer_spec.rb +28 -0
  94. data/spec/lib/aggro/file_store/reader_spec.rb +32 -0
  95. data/spec/lib/aggro/file_store/writer_spec.rb +67 -0
  96. data/spec/lib/aggro/file_store_spec.rb +51 -0
  97. data/spec/lib/aggro/handler/command_spec.rb +78 -0
  98. data/spec/lib/aggro/handler/create_aggregate_spec.rb +64 -0
  99. data/spec/lib/aggro/handler/get_events_handler_spec.rb +45 -0
  100. data/spec/lib/aggro/handler/query_spec.rb +78 -0
  101. data/spec/lib/aggro/handler/start_saga_spec.rb +64 -0
  102. data/spec/lib/aggro/local_node_spec.rb +52 -0
  103. data/spec/lib/aggro/locator_spec.rb +61 -0
  104. data/spec/lib/aggro/message/ask_spec.rb +23 -0
  105. data/spec/lib/aggro/message/command_spec.rb +50 -0
  106. data/spec/lib/aggro/message/create_aggregate_spec.rb +28 -0
  107. data/spec/lib/aggro/message/endpoint_spec.rb +23 -0
  108. data/spec/lib/aggro/message/events_spec.rb +37 -0
  109. data/spec/lib/aggro/message/get_events_spec.rb +33 -0
  110. data/spec/lib/aggro/message/heartbeat_spec.rb +23 -0
  111. data/spec/lib/aggro/message/invalid_target_spec.rb +28 -0
  112. data/spec/lib/aggro/message/ok_spec.rb +27 -0
  113. data/spec/lib/aggro/message/publisher_endpoint_inquiry_spec.rb +23 -0
  114. data/spec/lib/aggro/message/query_spec.rb +50 -0
  115. data/spec/lib/aggro/message/start_saga_spec.rb +37 -0
  116. data/spec/lib/aggro/message/unhandled_operation_spec.rb +28 -0
  117. data/spec/lib/aggro/message/unknown_operation_spec.rb +28 -0
  118. data/spec/lib/aggro/message_parser_spec.rb +16 -0
  119. data/spec/lib/aggro/message_router_spec.rb +35 -0
  120. data/spec/lib/aggro/nanomsg_transport/socket_error_spec.rb +21 -0
  121. data/spec/lib/aggro/nanomsg_transport_spec.rb +37 -0
  122. data/spec/lib/aggro/node_list_spec.rb +38 -0
  123. data/spec/lib/aggro/node_spec.rb +44 -0
  124. data/spec/lib/aggro/projection_spec.rb +22 -0
  125. data/spec/lib/aggro/query_spec.rb +47 -0
  126. data/spec/lib/aggro/saga_runner_spec.rb +84 -0
  127. data/spec/lib/aggro/saga_spec.rb +126 -0
  128. data/spec/lib/aggro/saga_status_spec.rb +56 -0
  129. data/spec/lib/aggro/server_spec.rb +118 -0
  130. data/spec/lib/aggro/subscriber_spec.rb +59 -0
  131. data/spec/lib/aggro/subscription_spec.rb +50 -0
  132. data/spec/lib/aggro/transform/boolean_spec.rb +23 -0
  133. data/spec/lib/aggro/transform/email_spec.rb +13 -0
  134. data/spec/lib/aggro/transform/id_spec.rb +70 -0
  135. data/spec/lib/aggro/transform/integer_spec.rb +30 -0
  136. data/spec/lib/aggro/transform/money_spec.rb +34 -0
  137. data/spec/lib/aggro/transform/string_spec.rb +15 -0
  138. data/spec/lib/aggro/transform/time_interval_spec.rb +29 -0
  139. data/spec/lib/aggro_spec.rb +63 -19
  140. data/spec/spec_helper.rb +21 -2
  141. metadata +283 -3
@@ -0,0 +1,96 @@
1
+ module Aggro
2
+ # Public: Adds a DSL defining attributes and validations.
3
+ module AttributeDSL
4
+ extend ActiveSupport::Concern
5
+
6
+ include ActiveModel::Model
7
+ include ActiveModel::Validations::Callbacks
8
+
9
+ def initialize(attrs = {})
10
+ if Thread.current[:causation_id] && respond_to?(:causation_id=)
11
+ attrs.merge! causation_id: Thread.current[:causation_id]
12
+ end
13
+
14
+ if Thread.current[:correlation_id] && respond_to?(:correlation_id=)
15
+ attrs.merge! correlation_id: Thread.current[:correlation_id]
16
+ end
17
+
18
+ super
19
+ end
20
+
21
+ def attributes
22
+ self.class.attributes.keys.reduce({}) do |hash, name|
23
+ hash.merge name => send(name)
24
+ end
25
+ end
26
+
27
+ def serialized_attributes
28
+ self.class.attributes.reduce({}) do |hash, (name, transform)|
29
+ hash.merge name => transform.serialize(send(name))
30
+ end
31
+ end
32
+
33
+ class_methods do
34
+ def attributes
35
+ Aggro.class_attributes[name]
36
+ end
37
+
38
+ def attribute(name)
39
+ create_attrs name, Transform::NOOP
40
+ end
41
+
42
+ def boolean(name)
43
+ create_attrs name, Transform::Boolean
44
+ end
45
+
46
+ def email(name)
47
+ create_attrs name, Transform::Email
48
+ end
49
+
50
+ def generate_id(name)
51
+ create_attrs name, Transform::ID.new(generate: true)
52
+ end
53
+
54
+ def id(name)
55
+ create_attrs name, Transform::ID.new
56
+ end
57
+
58
+ def integer(name)
59
+ create_attrs name, Transform::Integer
60
+ end
61
+
62
+ def money(name)
63
+ require 'money'
64
+ require 'monetize'
65
+
66
+ create_attrs name, Transform::Money
67
+ rescue LoadError
68
+ puts '`money` and `monetize` gems must be present to use money type'
69
+ end
70
+
71
+ def string(name)
72
+ create_attrs name, Transform::String
73
+ end
74
+
75
+ def time_interval(name)
76
+ require 'time-interval'
77
+
78
+ create_attrs name, Transform::TimeInterval
79
+ rescue LoadError
80
+ puts '`time-interval` gem must be present to use time_interval type'
81
+ end
82
+
83
+ private
84
+
85
+ def create_attrs(name, transformer)
86
+ attr_reader name
87
+ attributes[name] = transformer
88
+
89
+ define_method("#{name}=") do |value|
90
+ transformed = self.class.attributes[name].deserialize value
91
+ instance_variable_set "@#{name}", transformed
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,45 @@
1
+ module Aggro
2
+ # Public: Adds a DSL creating domain event bindings.
3
+ module BindingDSL
4
+ def bind(ref, filters: default_filters, to: nil, &block)
5
+ if to
6
+ bindings << Aggro.event_bus.subscribe(ref.id, self, to, filters)
7
+ else
8
+ bind_block ref, filters, &block
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def bind_block(ref, filters, namespace = generate_namespace, &block)
15
+ new_methods = BlockHelper.method_definitions(&block)
16
+ event_methods[namespace] = Set.new(new_methods)
17
+
18
+ class_eval(&block)
19
+ move_methods_to_namespace(new_methods, namespace)
20
+
21
+ bindings << Aggro.event_bus.subscribe(ref.id, self, namespace, filters)
22
+ end
23
+
24
+ def bindings
25
+ @bindings ||= []
26
+ end
27
+
28
+ def default_filters
29
+ {}
30
+ end
31
+
32
+ def generate_namespace
33
+ [*('a'..'z')].sample(8).join
34
+ end
35
+
36
+ def move_methods_to_namespace(method_list, namespace)
37
+ class_eval do
38
+ method_list.each do |method|
39
+ alias_method "#{namespace}_#{method}", method
40
+ remove_method method
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ module Aggro
2
+ # Private: Helper fuction for common operations on blocks.
3
+ module BlockHelper
4
+ module_function
5
+
6
+ def method_definitions(&block)
7
+ test_class = Class.new(BasicObject)
8
+ starting_methods = test_class.instance_methods
9
+ test_class.class_eval(&block)
10
+
11
+ test_class.instance_methods - starting_methods
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ module Aggro
2
+ # Private: Provides an interface to communicate with an aggregate or saga.
3
+ # Only loads the target object when needed.
4
+ class Channel < Struct.new(:id, :type)
5
+ def forward_command(command)
6
+ target << command if handles_command?(command)
7
+ end
8
+
9
+ def handles_command?(command)
10
+ target_class.allows? command
11
+ end
12
+
13
+ def handles_query?(query)
14
+ target_class.responds_to? query
15
+ end
16
+
17
+ def run_query(query)
18
+ target.ask query if handles_query? query
19
+ end
20
+
21
+ private
22
+
23
+ def target
24
+ @target ||= begin
25
+ ConcurrentActor.spawn!(
26
+ name: id,
27
+ args: [target_class.new(id)],
28
+ executor: Concurrent.configuration.global_task_pool
29
+ )
30
+ end
31
+ end
32
+
33
+ def target_class
34
+ @target_class ||= ActiveSupport::Inflector.constantize type
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ module Aggro
2
+ # Public: Makes requests against a given endpoint returning parsed responses.
3
+ class Client
4
+ def initialize(endpoint)
5
+ @transport_client = Aggro.transport.client(endpoint)
6
+ end
7
+
8
+ def post(message)
9
+ MessageParser.parse @transport_client.post message
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,57 @@
1
+ module Aggro
2
+ # Public: Stores the current cluster config. Persists to disk after changes.
3
+ class ClusterConfig
4
+ attr_reader :node_name
5
+
6
+ def initialize(path)
7
+ @path = path
8
+
9
+ if File.exist? path
10
+ load_config
11
+ else
12
+ initialize_config
13
+ persist_config
14
+ end
15
+ end
16
+
17
+ def add_node(name, server)
18
+ @nodes = nodes.merge(name => server).freeze
19
+ persist_config
20
+ end
21
+
22
+ def nodes
23
+ @nodes ||= {}.freeze
24
+ end
25
+
26
+ def server_node?
27
+ @is_server_node == true
28
+ end
29
+
30
+ private
31
+
32
+ def initialize_config
33
+ @node_name = SecureRandom.uuid
34
+ @is_server_node = false
35
+ end
36
+
37
+ def load_config
38
+ YAML.load_file(@path).each do |key, value|
39
+ instance_variable_set "@#{key}", value.freeze
40
+ end
41
+ end
42
+
43
+ def persist_config
44
+ File.open @path, 'w' do |file|
45
+ file.write YAML.dump to_h
46
+ end
47
+ end
48
+
49
+ def to_h
50
+ {
51
+ node_name: node_name,
52
+ nodes: nodes,
53
+ is_server_node: server_node?
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ module Aggro
2
+ # Public: Mixin to turn a PORO into an Aggro command.
3
+ module Command
4
+ extend ActiveSupport::Concern
5
+ include AttributeDSL
6
+
7
+ included do
8
+ generate_id :causation_id
9
+ generate_id :correlation_id
10
+ end
11
+
12
+ def to_details
13
+ { name: model_name.name, args: serialized_attributes }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module Aggro
2
+ # Private: Wraps a given target in an concurrent actor.
3
+ class ConcurrentActor < Concurrent::Actor::Context
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ def on_message(message)
9
+ if command? message
10
+ @target.send :apply_command, message
11
+ elsif query? message
12
+ @target.send :run_query, message
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def command?(message)
19
+ message.class.included_modules.include? Command
20
+ end
21
+
22
+ def query?(message)
23
+ message.class.included_modules.include? Query
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,94 @@
1
+ module Aggro
2
+ # Public: Publishes events to any subscribed listeners.
3
+ class EventBus
4
+ def initialize
5
+ @remote_publishers = {}
6
+ end
7
+
8
+ def publish(topic, event)
9
+ Aggro.server.publish Message::Events.new(topic, [event])
10
+
11
+ return unless subscriptions.key? topic
12
+
13
+ subscriptions[topic].each do |subscription|
14
+ subscription.handle_event event
15
+ end
16
+ end
17
+
18
+ def subscribe(topic, subscriber, event_namespace = nil, filters = {})
19
+ subscription = Subscription.new(topic, subscriber, event_namespace,
20
+ filters, 0)
21
+
22
+ catchup_subscriber topic, subscription
23
+
24
+ subscriptions[topic] ||= []
25
+ subscriptions[topic] << subscription
26
+
27
+ subscribe_bus_to_publisher topic
28
+
29
+ subscription
30
+ end
31
+
32
+ def unsubscribe(topic, subscriber)
33
+ subscriptions[topic].delete subscriber
34
+ end
35
+
36
+ def shutdown
37
+ remote_publishers.values.each(&:stop)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :remote_publishers
43
+
44
+ def catchup_local(topic, subscription)
45
+ Aggro.store.read([topic]).first.events.each do |event|
46
+ subscription.handle_event event
47
+ end
48
+ end
49
+
50
+ def catchup_remote(topic, subscription, node)
51
+ message = Message::GetEvents.new(Aggro.local_node.id, topic, 0)
52
+ response = node.client.post message
53
+
54
+ if response.is_a? Message::Events
55
+ response.events.each { |event| subscription.handle_event event }
56
+ else
57
+ fail 'Could not catchup subscriber'
58
+ end
59
+ end
60
+
61
+ def catchup_subscriber(topic, subscription)
62
+ node = Locator.new(topic).primary_node
63
+
64
+ if node.is_a? LocalNode
65
+ catchup_local(topic, subscription)
66
+ else
67
+ catchup_remote(topic, subscription, node)
68
+ end
69
+ end
70
+
71
+ def handle_events(topic, events)
72
+ subscriptions[topic].each do |subscription|
73
+ events.each { |event| subscription.handle_event event }
74
+ end
75
+ end
76
+
77
+ def subscribe_bus_to_publisher(topic)
78
+ node = Locator.new(topic).primary_node
79
+
80
+ return if node.is_a? LocalNode
81
+
82
+ publisher_endpoint = node.publisher_endpoint
83
+ remote_publishers[publisher_endpoint] ||= begin
84
+ Subscriber.new(publisher_endpoint, method(:handle_events)).tap(&:bind)
85
+ end
86
+
87
+ remote_publishers[publisher_endpoint].subscribe_to_topic topic
88
+ end
89
+
90
+ def subscriptions
91
+ @subscriptions ||= {}
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,53 @@
1
+ module Aggro
2
+ # Public: Adds a DSL defining event handlers.
3
+ module EventDSL
4
+ extend ActiveSupport::Concern
5
+
6
+ def handles_event?(event_name, namespace = nil)
7
+ self.class.handles_event?(event_name, namespace) || \
8
+ namespace?(namespace) && event_methods[namespace].include?(event_name)
9
+ end
10
+
11
+ private
12
+
13
+ def event_methods
14
+ @event_methods ||= {}
15
+ end
16
+
17
+ def namespace?(namespace)
18
+ !event_methods[namespace].nil?
19
+ end
20
+
21
+ class_methods do
22
+ def events(namespace = nil, &block)
23
+ new_methods = BlockHelper.method_definitions(&block)
24
+
25
+ event_methods[namespace] ||= Set.new
26
+ new_methods.each { |method| event_methods[namespace] << method }
27
+
28
+ class_eval(&block)
29
+
30
+ class_eval do
31
+ new_methods.each do |method|
32
+ alias_method "#{namespace}_#{method}", method
33
+ remove_method method
34
+ end
35
+ end
36
+ end
37
+
38
+ def handles_event?(event_name, namespace = nil)
39
+ namespace?(namespace) && event_methods[namespace].include?(event_name)
40
+ end
41
+
42
+ private
43
+
44
+ def event_methods
45
+ @event_methods ||= {}
46
+ end
47
+
48
+ def namespace?(namespace)
49
+ !event_methods[namespace].nil?
50
+ end
51
+ end
52
+ end
53
+ end