thunderwire 0.2.0
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.
- checksums.yaml +7 -0
- data/README.md +3 -0
- data/lib/thunderwire/background_task.rb +11 -0
- data/lib/thunderwire/handler.rb +17 -0
- data/lib/thunderwire/message_broker.rb +16 -0
- data/lib/thunderwire/message_brokers/rabbitmq/consumer.rb +70 -0
- data/lib/thunderwire/message_brokers/rabbitmq/handlers/direct.rb +69 -0
- data/lib/thunderwire/message_brokers/rabbitmq/outbox.rb +68 -0
- data/lib/thunderwire/message_brokers/rabbitmq/publishers/outbox.rb +29 -0
- data/lib/thunderwire/protocol.rb +13 -0
- data/lib/thunderwire/protocols/http/request.rb +54 -0
- data/lib/thunderwire/protocols/http/server.rb +27 -0
- data/lib/thunderwire/protocols/http/wires/operation/receiver.rb +56 -0
- data/lib/thunderwire/protocols/http/wires/operation/sender.rb +50 -0
- data/lib/thunderwire/protocols/http/wires/operation.rb +14 -0
- data/lib/thunderwire/server.rb +39 -0
- data/lib/thunderwire/version.rb +5 -0
- data/lib/thunderwire/wire.rb +21 -0
- data/lib/thunderwire.rb +41 -0
- metadata +152 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 985b8c17c616f7c9a76e56313dc46e186875b479483ffa426c0745a9255f6514
|
|
4
|
+
data.tar.gz: 02a422c11d63251542d90466d52d2cf56fe292e6437d60d5b65b66373fdb3654
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 76635e57f908b2594521bcdd9965c5295a10b76649d0116713d7b26a7c05ba8d2b544a843c16264bcfc12f8a5450ce0dc2d9284f9994cf558b43a2353510e4e0
|
|
7
|
+
data.tar.gz: 609fdc9e8f8325ed53cebe16afe84d36d9c4579ff4f407998e74d87676e3708fb6bafca977c7be146598f2dfa8cc7511a8fd55f02cbf96fe55bc24c7d99003b8
|
data/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
class Handler
|
|
3
|
+
class << self
|
|
4
|
+
# WARN: AI MADE
|
|
5
|
+
def get!(broker, name)
|
|
6
|
+
file = File.join(__dir__, 'message_brokers', broker.name.split('::').last.underscore, 'handlers',
|
|
7
|
+
"#{name.underscore}.rb")
|
|
8
|
+
raise(Error, 'This handler does not exists') unless File.exist?(file)
|
|
9
|
+
|
|
10
|
+
require file
|
|
11
|
+
|
|
12
|
+
broker::Handlers.const_get(name.camelize)
|
|
13
|
+
end
|
|
14
|
+
# WARN: ---
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
class MessageBroker
|
|
3
|
+
class << self
|
|
4
|
+
# WARN: AI MADE
|
|
5
|
+
def get!(message_broker)
|
|
6
|
+
files = Dir[File.join(__dir__, 'message_brokers', message_broker.to_s, '*.rb')]
|
|
7
|
+
raise(Error, 'This broker does not exists') if files.empty?
|
|
8
|
+
|
|
9
|
+
files.sort.each { |file| require file }
|
|
10
|
+
|
|
11
|
+
ThunderWire::MessageBrokers.const_get(message_broker.to_s.camelize)
|
|
12
|
+
end
|
|
13
|
+
# WARN: ---
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bunny'
|
|
4
|
+
|
|
5
|
+
# WARN: AI MADE
|
|
6
|
+
module ThunderWire
|
|
7
|
+
module MessageBrokers
|
|
8
|
+
module Rabbitmq
|
|
9
|
+
# Consumes the queue of the chunk: declares it with one binding per event the chunk subscribes to, and hands each
|
|
10
|
+
# message to the handler. A message requeued past the delivery limit, or rejected, goes to the dead exchange.
|
|
11
|
+
class Consumer < BackgroundTask
|
|
12
|
+
def initialize(url:, queue:, keys:, handler:, exchange: 'thunderbolt.events', dead_exchange: 'thunderbolt.dead',
|
|
13
|
+
prefetch: 10, concurrency: 5, delivery_limit: 5)
|
|
14
|
+
@url = url
|
|
15
|
+
@queue_name = queue
|
|
16
|
+
@keys = keys
|
|
17
|
+
@handler = handler
|
|
18
|
+
@exchange_name = exchange
|
|
19
|
+
@dead_exchange_name = dead_exchange
|
|
20
|
+
@prefetch = prefetch
|
|
21
|
+
@concurrency = concurrency
|
|
22
|
+
@delivery_limit = delivery_limit
|
|
23
|
+
@stopped = Queue.new
|
|
24
|
+
|
|
25
|
+
super()
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start
|
|
29
|
+
@connection = Bunny.new(@url).start
|
|
30
|
+
channel = @connection.create_channel(nil, @concurrency)
|
|
31
|
+
channel.prefetch(@prefetch)
|
|
32
|
+
|
|
33
|
+
events = channel.exchange(@exchange_name, type: :topic, durable: true)
|
|
34
|
+
dead = channel.exchange(@dead_exchange_name, type: :fanout, durable: true)
|
|
35
|
+
channel.queue(@dead_exchange_name, durable: true, arguments: { 'x-queue-type' => 'quorum' }).bind(dead)
|
|
36
|
+
|
|
37
|
+
queue = channel.queue(@queue_name, durable: true, arguments: {
|
|
38
|
+
'x-queue-type' => 'quorum',
|
|
39
|
+
'x-dead-letter-exchange' => @dead_exchange_name,
|
|
40
|
+
'x-delivery-limit' => @delivery_limit
|
|
41
|
+
})
|
|
42
|
+
@keys.each { |key| queue.bind(events, routing_key: key) }
|
|
43
|
+
|
|
44
|
+
consumer = queue.subscribe(manual_ack: true) do |delivery, _properties, body|
|
|
45
|
+
case @handler.handle(delivery.routing_key, body)
|
|
46
|
+
when :ack then channel.ack(delivery.delivery_tag)
|
|
47
|
+
when :requeue then channel.nack(delivery.delivery_tag, false, true)
|
|
48
|
+
else channel.reject(delivery.delivery_tag, false)
|
|
49
|
+
end
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
Console.error(self, e)
|
|
52
|
+
channel.nack(delivery.delivery_tag, false, true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@stopped.pop
|
|
56
|
+
|
|
57
|
+
consumer.cancel
|
|
58
|
+
channel.work_pool.join(30)
|
|
59
|
+
ensure
|
|
60
|
+
@connection&.close
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Stops taking messages; the ones in progress finish, and the ones not acked go back to the queue.
|
|
64
|
+
def stop
|
|
65
|
+
@stopped << true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
# WARN: AI MADE
|
|
6
|
+
module ThunderWire
|
|
7
|
+
module MessageBrokers
|
|
8
|
+
module Rabbitmq
|
|
9
|
+
module Handlers
|
|
10
|
+
# Runs the subscriber of the event inside the consumer, and answers what the consumer does with the message.
|
|
11
|
+
# The keys of the payload the subscriber does not declare are left out; the ones it declares and the payload
|
|
12
|
+
# leaves out arrive as nil.
|
|
13
|
+
#
|
|
14
|
+
# A problem with the message itself (no subscriber, body that is not JSON, param of the wrong type) is logged
|
|
15
|
+
# and the message acked: delivering it again gives the same result. Any other failure of the subscriber is
|
|
16
|
+
# logged and the message requeued.
|
|
17
|
+
#
|
|
18
|
+
# FIX: no event is ever rejected: the ones with a problem of their own are acked, never sent to the dead
|
|
19
|
+
# exchange.
|
|
20
|
+
class Direct
|
|
21
|
+
def initialize(exporter)
|
|
22
|
+
@exporter = exporter
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.configurations
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle(routing_key, body)
|
|
30
|
+
unless @exporter.exports?(routing_key)
|
|
31
|
+
Console.error(self, 'Event Without Subscriber', routing_key:)
|
|
32
|
+
|
|
33
|
+
return :ack
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
params = JSON.parse(body, symbolize_names: true)
|
|
37
|
+
declared = @exporter.get(routing_key).base_parameter.configuration.keys
|
|
38
|
+
unknown = params.keys - declared
|
|
39
|
+
missing = declared - params.keys
|
|
40
|
+
|
|
41
|
+
if unknown.any? || missing.any?
|
|
42
|
+
Console.error(self, 'Event Payload Off Contract', routing_key:, unknown:, missing:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
response = @exporter.call(routing_key, params: params.slice(*declared))
|
|
46
|
+
|
|
47
|
+
if response.success?
|
|
48
|
+
Console.info(self, 'Event Handled', routing_key:)
|
|
49
|
+
|
|
50
|
+
:ack
|
|
51
|
+
elsif response.error.is_a?(ThunderOperation::Exceptions::ParameterError)
|
|
52
|
+
Console.error(self, 'Event Payload Off Contract', routing_key:, error: response.error)
|
|
53
|
+
|
|
54
|
+
:ack
|
|
55
|
+
else
|
|
56
|
+
Console.error(self, 'Event Failed', routing_key:, error: response.error)
|
|
57
|
+
|
|
58
|
+
:requeue
|
|
59
|
+
end
|
|
60
|
+
rescue JSON::ParserError => e
|
|
61
|
+
Console.error(self, 'Event Body Is Not JSON', routing_key:, error: e)
|
|
62
|
+
|
|
63
|
+
:ack
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bunny'
|
|
4
|
+
|
|
5
|
+
module ThunderWire
|
|
6
|
+
module MessageBrokers
|
|
7
|
+
module Rabbitmq
|
|
8
|
+
class Outbox < BackgroundTask
|
|
9
|
+
def initialize(url:, chunk:, exchange: 'thunderbolt.events', batch_size: 100, idle: 0.5)
|
|
10
|
+
@url = url
|
|
11
|
+
@chunk = chunk
|
|
12
|
+
@exchange_name = exchange
|
|
13
|
+
@batch_size = batch_size
|
|
14
|
+
@idle = idle
|
|
15
|
+
@running = false
|
|
16
|
+
|
|
17
|
+
super()
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# WARN: AI MADE
|
|
21
|
+
def start
|
|
22
|
+
@running = true
|
|
23
|
+
@connection = Bunny.new(@url).start
|
|
24
|
+
channel = @connection.create_channel
|
|
25
|
+
channel.confirm_select
|
|
26
|
+
exchange = channel.exchange(@exchange_name, type: :topic, durable: true)
|
|
27
|
+
|
|
28
|
+
while @running
|
|
29
|
+
begin
|
|
30
|
+
sent = ThunderChunk::Outbox.transaction do
|
|
31
|
+
messages = ThunderChunk::Outbox.pending.limit(@batch_size).lock('FOR UPDATE SKIP LOCKED').to_a
|
|
32
|
+
|
|
33
|
+
messages.each do |message|
|
|
34
|
+
routing_key = "#{@chunk}/#{message.routing_key}"
|
|
35
|
+
|
|
36
|
+
Console.info(self, 'Event Published', message_id: message.message_id,
|
|
37
|
+
routing_key:,
|
|
38
|
+
payload: message.payload)
|
|
39
|
+
exchange.publish(message.payload.to_json, routing_key:, message_id: message.message_id,
|
|
40
|
+
persistent: true, content_type: 'application/json')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise Error, 'publish not confirmed by the broker' if messages.any? && !channel.wait_for_confirms
|
|
44
|
+
|
|
45
|
+
if messages.any?
|
|
46
|
+
ThunderChunk::Outbox.where(id: messages.map(&:id)).update_all(published_at: Time.current)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
messages.size
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sleep @idle if sent.zero?
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
Console.error(self, e)
|
|
55
|
+
sleep 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
ensure
|
|
59
|
+
@connection&.close
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stop
|
|
63
|
+
@running = false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module ThunderWire
|
|
6
|
+
module MessageBrokers
|
|
7
|
+
module Rabbitmq
|
|
8
|
+
module Publishers
|
|
9
|
+
class Outbox
|
|
10
|
+
def initialize(table_name: 'thunder_outbox')
|
|
11
|
+
ThunderChunk::Outbox.table_name = table_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.configurations
|
|
15
|
+
{ table_name: 'messaging.outbox.table_name' }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def publish(key, params)
|
|
19
|
+
ThunderChunk::Outbox.create!(
|
|
20
|
+
message_id: SecureRandom.uuid,
|
|
21
|
+
routing_key: key,
|
|
22
|
+
payload: params
|
|
23
|
+
).message_id
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
class Protocol
|
|
3
|
+
class << self
|
|
4
|
+
def get!(protocol)
|
|
5
|
+
Dir[File.join(__dir__, 'protocols', protocol.to_s, '*.rb')].sort.each { |file| require file }
|
|
6
|
+
|
|
7
|
+
ThunderWire::Protocols.const_get(protocol.camelize).tap do |v|
|
|
8
|
+
raise(Error, 'This protocol does not exists') unless v
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderWire
|
|
4
|
+
module Protocols
|
|
5
|
+
module Http
|
|
6
|
+
# The Rack request as the adapters read it. The body stream can be consumed only once, so it is read on the
|
|
7
|
+
# first call and kept; the parsed JSON and query are kept too.
|
|
8
|
+
class Request < ::Protocol::Rack::Request
|
|
9
|
+
ENV_KEY = 'thunderwire.request'
|
|
10
|
+
|
|
11
|
+
class InvalidJSON < StandardError; end
|
|
12
|
+
|
|
13
|
+
attr_reader :env
|
|
14
|
+
|
|
15
|
+
def self.[](env)
|
|
16
|
+
env[ENV_KEY] ||= new(env)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def body
|
|
20
|
+
return @raw_body if defined?(@raw_body)
|
|
21
|
+
|
|
22
|
+
@raw_body = super&.join.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parsed_body
|
|
26
|
+
return @parsed_body if defined?(@parsed_body)
|
|
27
|
+
return @parsed_body = body unless json?
|
|
28
|
+
|
|
29
|
+
@parsed_body = body.empty? ? {} : JSON.parse(body)
|
|
30
|
+
rescue JSON::ParserError => e
|
|
31
|
+
raise InvalidJSON, e.message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def query_string = @env['QUERY_STRING'].to_s
|
|
35
|
+
|
|
36
|
+
def query
|
|
37
|
+
@query ||= ::Rack::Utils.parse_nested_query(query_string)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def content_type = @env['CONTENT_TYPE']
|
|
41
|
+
|
|
42
|
+
def content_length = @env['CONTENT_LENGTH']&.to_i
|
|
43
|
+
|
|
44
|
+
def media_type = content_type&.split(';')&.first&.strip&.downcase
|
|
45
|
+
|
|
46
|
+
def json? = media_type == 'application/json'
|
|
47
|
+
|
|
48
|
+
def post? = method == 'POST'
|
|
49
|
+
|
|
50
|
+
def remote_ip = @env['REMOTE_ADDR']
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
module Protocols
|
|
3
|
+
module Http
|
|
4
|
+
class Server < ThunderWire::Server
|
|
5
|
+
attr_reader :async_server
|
|
6
|
+
|
|
7
|
+
def create_task(ip, port)
|
|
8
|
+
endpoint = Async::HTTP::Endpoint.for('http', ip, port:)
|
|
9
|
+
|
|
10
|
+
@async_server = Async::HTTP::Server.new(
|
|
11
|
+
::Protocol::Rack::Adapter.new(
|
|
12
|
+
lambda do |request|
|
|
13
|
+
# Console.info(self, 'Http Request Received', **request)
|
|
14
|
+
@wire.call(ThunderWire::Protocols::Http::Request[request]).tap do |(status, headers, body)|
|
|
15
|
+
# Console.info(self, 'Http Response Sent', status:, headers:, body:)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
),
|
|
19
|
+
endpoint
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@async_server.run
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
module Protocols
|
|
3
|
+
module Http
|
|
4
|
+
module Wires
|
|
5
|
+
module Operation
|
|
6
|
+
class Receiver
|
|
7
|
+
def initialize(chunk)
|
|
8
|
+
@chunk = chunk
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# FIX: Imrpove the code
|
|
12
|
+
def call(request)
|
|
13
|
+
key = path_to_key(request.path)
|
|
14
|
+
exporter = @chunk.exporter.get(:operation)
|
|
15
|
+
|
|
16
|
+
request.post? || raise(ProtocolError, "#{request.method} is not allowed; use POST")
|
|
17
|
+
exporter.exports?(key) || raise(ProtocolError, "No operation at #{request.path}")
|
|
18
|
+
request.json? || raise(ProtocolError, 'The body must be application/json')
|
|
19
|
+
|
|
20
|
+
params = request.parsed_body
|
|
21
|
+
params.is_a?(Hash) || raise(ProtocolError, 'The body must be a JSON object')
|
|
22
|
+
|
|
23
|
+
Console.info(self, 'Http Request Received', params:)
|
|
24
|
+
|
|
25
|
+
result = exporter.call(key, params: params.deep_symbolize_keys)
|
|
26
|
+
|
|
27
|
+
Console.info(self, 'Http Response Sent', result: result.to_h)
|
|
28
|
+
|
|
29
|
+
unless result.payload.nil? || result.payload.is_a?(Hash) || result.payload.is_a?(Array)
|
|
30
|
+
raise Error,
|
|
31
|
+
'result.payload must be a hash or array'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
[200, {}, [result.to_json]]
|
|
35
|
+
rescue ProtocolError, Request::InvalidJSON => e
|
|
36
|
+
[400, {}, [{ error: { type: 'ProtocolError', message: e.message } }.to_json]]
|
|
37
|
+
rescue Error => e
|
|
38
|
+
[500, {}, [{ error: { type: 'InternalError', message: e.message } }.to_json]]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# /_operations/books/show or /library/_operations/books/show -> 'books.show'. The chunk is known by the
|
|
42
|
+
# url; a prefix with its name is accepted for a router (ingress) that keeps it in the path.
|
|
43
|
+
def path_to_key(path)
|
|
44
|
+
segments = path.split('/').reject(&:empty?)
|
|
45
|
+
segments.shift if segments.first == @chunk.identifier.to_s
|
|
46
|
+
segment, *path_array = segments
|
|
47
|
+
return unless segment == '_operations' && path_array.any?
|
|
48
|
+
|
|
49
|
+
path_array.join('.')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
module Protocols
|
|
3
|
+
module Http
|
|
4
|
+
module Wires
|
|
5
|
+
module Operation
|
|
6
|
+
class Sender
|
|
7
|
+
def initialize(chunk_identifier, base_url)
|
|
8
|
+
@chunk_identifier = chunk_identifier
|
|
9
|
+
@endpoint = Async::HTTP::Endpoint.parse(base_url)
|
|
10
|
+
@client = Async::HTTP::Client.new(@endpoint)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# FIX: Imrpove the code
|
|
14
|
+
def call(key, params)
|
|
15
|
+
Sync do
|
|
16
|
+
Console.info(self, 'Http Request Sent', chunk: @chunk_identifier, params:)
|
|
17
|
+
|
|
18
|
+
answer = @client.post(
|
|
19
|
+
"#{@endpoint.path.chomp('/')}/_operations/#{key.tr('.', '/')}",
|
|
20
|
+
[['content-type', 'application/json']],
|
|
21
|
+
params.to_json
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
body = JSON.parse(answer.read)
|
|
25
|
+
failure = body['failure_data']
|
|
26
|
+
|
|
27
|
+
Console.info(self, 'Http Response Received', chunk: @chunk_identifier, status: answer.status,
|
|
28
|
+
body:)
|
|
29
|
+
|
|
30
|
+
error = if failure
|
|
31
|
+
ThunderChunk::Operations::Remote::RemoteResponseFailure.new(
|
|
32
|
+
failure['failure_message'],
|
|
33
|
+
error_class: failure['error_class'],
|
|
34
|
+
extras: failure['failure_extras'],
|
|
35
|
+
trail: failure['trail']
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ThunderOperation::Response.new(
|
|
40
|
+
payload: body['payload'], was_success: body['was_success'], error:,
|
|
41
|
+
status: body['status'], failed_step: body['failed_step'], operation_name: body['operation_name']
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
class Server
|
|
3
|
+
def initialize(ip, port, wire)
|
|
4
|
+
@ip = ip
|
|
5
|
+
@port = port
|
|
6
|
+
@wire = wire
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def up
|
|
10
|
+
Signal.trap('TERM') { Thread.main.raise(Interrupt) }
|
|
11
|
+
|
|
12
|
+
Sync do
|
|
13
|
+
@server_task = create_task(@ip, @port)
|
|
14
|
+
|
|
15
|
+
Console.info(self, 'Server Started!')
|
|
16
|
+
|
|
17
|
+
@server_task.wait
|
|
18
|
+
rescue Interrupt
|
|
19
|
+
# graceful shutdown handled by ensure
|
|
20
|
+
ensure
|
|
21
|
+
down
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def down
|
|
26
|
+
@server_task&.stop
|
|
27
|
+
|
|
28
|
+
Console.info(self, 'Server Stopped!')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def build(chunk, protocol_module, wire_klass, ip, port)
|
|
33
|
+
wire = wire_klass::Receiver.new(chunk)
|
|
34
|
+
|
|
35
|
+
protocol_module::Server.new(ip, port, wire)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module ThunderWire
|
|
2
|
+
# Extended by each wire module (Protocols::Http::Wires::Operation), which holds that protocol's Receiver and Sender
|
|
3
|
+
# for one kind of export.
|
|
4
|
+
module Wire
|
|
5
|
+
def self.get!(protocol, name)
|
|
6
|
+
require "thunderwire/protocols/#{protocol.name.split('::').last.underscore}/wires/#{name.underscore}"
|
|
7
|
+
|
|
8
|
+
protocol::Wires.const_get(name.camelize).tap do |v|
|
|
9
|
+
raise(Error, 'This wire does not exists') unless v
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def exporter
|
|
14
|
+
wire_name = name.split('::').last
|
|
15
|
+
|
|
16
|
+
require "thunderchunk/exporters/#{wire_name.underscore.pluralize}"
|
|
17
|
+
|
|
18
|
+
ThunderChunk::Exporters.const_get(wire_name.pluralize)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/thunderwire.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
|
5
|
+
require 'active_support/core_ext/hash/keys'
|
|
6
|
+
require 'async'
|
|
7
|
+
require 'async/http/server'
|
|
8
|
+
require 'async/http/endpoint'
|
|
9
|
+
require 'async/http/client'
|
|
10
|
+
require 'protocol/rack'
|
|
11
|
+
require 'rack/utils'
|
|
12
|
+
require 'json'
|
|
13
|
+
require 'thunderchunk'
|
|
14
|
+
|
|
15
|
+
require_relative 'thunderwire/version'
|
|
16
|
+
require_relative 'thunderwire/server'
|
|
17
|
+
require_relative 'thunderwire/wire'
|
|
18
|
+
require_relative 'thunderwire/protocol'
|
|
19
|
+
require_relative 'thunderwire/background_task'
|
|
20
|
+
require_relative 'thunderwire/message_broker'
|
|
21
|
+
require_relative 'thunderwire/handler'
|
|
22
|
+
|
|
23
|
+
module ThunderWire
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
|
|
26
|
+
class ProtocolError < Error; end
|
|
27
|
+
|
|
28
|
+
# WARN: AI MADE
|
|
29
|
+
class << self
|
|
30
|
+
def sender(protocol, wire)
|
|
31
|
+
Wire.get!(Protocol.get!(protocol.to_s), wire.to_s)::Sender
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def publisher(broker, publisher)
|
|
35
|
+
require "thunderwire/message_brokers/#{broker}/publishers/#{publisher}"
|
|
36
|
+
|
|
37
|
+
MessageBrokers.const_get(broker.to_s.camelize)::Publishers.const_get(publisher.to_s.camelize)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
# WARN: ---
|
|
41
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: thunderwire
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Albert Alef
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: thunderchunk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.2.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.2.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: async
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: async-http
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.103'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.103'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: bunny
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: protocol-rack
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.22'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.22'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rack
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '3.1'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.1'
|
|
110
|
+
executables: []
|
|
111
|
+
extensions: []
|
|
112
|
+
extra_rdoc_files: []
|
|
113
|
+
files:
|
|
114
|
+
- README.md
|
|
115
|
+
- lib/thunderwire.rb
|
|
116
|
+
- lib/thunderwire/background_task.rb
|
|
117
|
+
- lib/thunderwire/handler.rb
|
|
118
|
+
- lib/thunderwire/message_broker.rb
|
|
119
|
+
- lib/thunderwire/message_brokers/rabbitmq/consumer.rb
|
|
120
|
+
- lib/thunderwire/message_brokers/rabbitmq/handlers/direct.rb
|
|
121
|
+
- lib/thunderwire/message_brokers/rabbitmq/outbox.rb
|
|
122
|
+
- lib/thunderwire/message_brokers/rabbitmq/publishers/outbox.rb
|
|
123
|
+
- lib/thunderwire/protocol.rb
|
|
124
|
+
- lib/thunderwire/protocols/http/request.rb
|
|
125
|
+
- lib/thunderwire/protocols/http/server.rb
|
|
126
|
+
- lib/thunderwire/protocols/http/wires/operation.rb
|
|
127
|
+
- lib/thunderwire/protocols/http/wires/operation/receiver.rb
|
|
128
|
+
- lib/thunderwire/protocols/http/wires/operation/sender.rb
|
|
129
|
+
- lib/thunderwire/server.rb
|
|
130
|
+
- lib/thunderwire/version.rb
|
|
131
|
+
- lib/thunderwire/wire.rb
|
|
132
|
+
licenses: []
|
|
133
|
+
metadata:
|
|
134
|
+
rubygems_mfa_required: 'true'
|
|
135
|
+
rdoc_options: []
|
|
136
|
+
require_paths:
|
|
137
|
+
- lib
|
|
138
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '3.4'
|
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '0'
|
|
148
|
+
requirements: []
|
|
149
|
+
rubygems_version: 3.6.9
|
|
150
|
+
specification_version: 4
|
|
151
|
+
summary: External of the thunderbolt ecosystem
|
|
152
|
+
test_files: []
|