aggro 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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