magic_pipe 0.1.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +8 -0
  5. data/CHANGELOG.md +10 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +92 -0
  8. data/LICENSE.txt +19 -0
  9. data/README.md +241 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +16 -0
  12. data/bin/setup +8 -0
  13. data/lib/magic_pipe.rb +45 -0
  14. data/lib/magic_pipe/client.rb +41 -0
  15. data/lib/magic_pipe/codecs.rb +24 -0
  16. data/lib/magic_pipe/codecs/base.rb +26 -0
  17. data/lib/magic_pipe/codecs/json.rb +38 -0
  18. data/lib/magic_pipe/codecs/message_pack.rb +81 -0
  19. data/lib/magic_pipe/codecs/thrift.rb +17 -0
  20. data/lib/magic_pipe/codecs/yaml.rb +18 -0
  21. data/lib/magic_pipe/config.rb +103 -0
  22. data/lib/magic_pipe/envelope.rb +29 -0
  23. data/lib/magic_pipe/errors.rb +14 -0
  24. data/lib/magic_pipe/loaders.rb +22 -0
  25. data/lib/magic_pipe/loaders/simple_active_record.rb +54 -0
  26. data/lib/magic_pipe/metrics.rb +54 -0
  27. data/lib/magic_pipe/senders.rb +23 -0
  28. data/lib/magic_pipe/senders/async.rb +76 -0
  29. data/lib/magic_pipe/senders/base.rb +24 -0
  30. data/lib/magic_pipe/senders/metrics_mixin.rb +20 -0
  31. data/lib/magic_pipe/senders/sync.rb +38 -0
  32. data/lib/magic_pipe/transports.rb +26 -0
  33. data/lib/magic_pipe/transports/base.rb +17 -0
  34. data/lib/magic_pipe/transports/debug.rb +17 -0
  35. data/lib/magic_pipe/transports/https.rb +80 -0
  36. data/lib/magic_pipe/transports/kafka.rb +8 -0
  37. data/lib/magic_pipe/transports/log.rb +15 -0
  38. data/lib/magic_pipe/transports/multi.rb +44 -0
  39. data/lib/magic_pipe/transports/sqs.rb +68 -0
  40. data/lib/magic_pipe/version.rb +3 -0
  41. data/magic_pipe.gemspec +44 -0
  42. metadata +257 -0
@@ -0,0 +1,22 @@
1
+ module MagicPipe
2
+ module Loaders
3
+ def self.lookup(type)
4
+ case type
5
+ when :simple_active_record then SimpleActiveRecord
6
+ when Class then type
7
+ else
8
+ raise ConfigurationError, "Unknown MagicPipe::Loaders type: '#{type}'."
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ files = File.expand_path("../loaders/**/*.rb", __FILE__)
15
+ Dir[files].each do |f|
16
+ begin
17
+ require f
18
+ rescue LoadError
19
+ # Some components have extra dependencies
20
+ end
21
+ end
22
+
@@ -0,0 +1,54 @@
1
+ module MagicPipe
2
+ module Loaders
3
+ class SimpleActiveRecord
4
+ def initialize(record, wrapper=nil)
5
+ @record = record
6
+ @wrapper = wrapper
7
+ end
8
+
9
+ attr_reader :record
10
+
11
+ def decompose
12
+ {
13
+ klass: @record.class.to_s,
14
+ id: @record.id,
15
+ wrapper: (@wrapper && @wrapper.to_s),
16
+ }
17
+ end
18
+
19
+
20
+ class << self
21
+ def load(decomposed_data)
22
+ input = symbolize_keys(decomposed_data)
23
+ record_klass_name = input[:klass]
24
+ record_id = input[:id]
25
+ wrapper_klass_name = input[:wrapper]
26
+
27
+ record_klass = load_constant!(record_klass_name, "ActiveRecord model")
28
+ record = record_klass.find(record_id) # let it raise ActiveRecord::RecordNotFound
29
+
30
+ if wrapper_klass_name
31
+ wrapper_klass = load_constant!(wrapper_klass_name, "object serializer")
32
+ wrapper_klass.new(record)
33
+ else
34
+ record
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def load_constant!(name, context)
41
+ Object.const_get(name)
42
+ rescue NameError
43
+ raise MagicPipe::LoaderError.new(name, context)
44
+ end
45
+
46
+ def symbolize_keys(hash)
47
+ hash.map do |k, v|
48
+ [k.to_sym, v]
49
+ end.to_h
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ module MagicPipe
2
+ class Metrics
3
+ def initialize(config)
4
+ @client = config.metrics_client
5
+ @default_tags = build_default_tags(config)
6
+ end
7
+
8
+ attr_reader :client
9
+
10
+ def increment(metric, tags: [])
11
+ @client.increment(metric, tags: all_tags(tags))
12
+ end
13
+
14
+
15
+ private
16
+
17
+
18
+ def all_tags(list)
19
+ @default_tags + list
20
+ end
21
+
22
+ def build_default_tags(config)
23
+ list = [
24
+ "producer:#{config.producer_name.to_s.gsub(" ", "_")}",
25
+ "pipe_instance:#{config.client_name.to_s}",
26
+ "loader:#{config.loader.to_s}",
27
+ "codec:#{config.codec.to_s}",
28
+ "transport:#{transport_tag(config)}",
29
+ "sender:#{config.sender.to_s}",
30
+ ]
31
+ end
32
+
33
+ def transport_tag(config)
34
+ t = config.transport
35
+ if t.is_a?(Array)
36
+ "multi_" + t.map { |s| sanitize_tag_string(s) }.join("-")
37
+ else
38
+ t.to_s
39
+ end
40
+ end
41
+
42
+ def sanitize_tag_string(value)
43
+ value.to_s.tr(":. /", "")
44
+ end
45
+
46
+ def method_missing(name, *args, &block)
47
+ client.public_send(name, *args, &block)
48
+ end
49
+
50
+ def respond_to_missing?(name, include_all)
51
+ client.respond_to?(name, include_all)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,23 @@
1
+ module MagicPipe
2
+ module Senders
3
+ def self.lookup(type)
4
+ case type
5
+ when :sync then Sync
6
+ when :async then Async
7
+ when Class then type
8
+ else
9
+ raise ConfigurationError, "Unknown MagicPipe::Senders type: '#{type}'."
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ files = File.expand_path("../senders/**/*.rb", __FILE__)
16
+ Dir[files].each do |f|
17
+ begin
18
+ require f
19
+ rescue LoadError
20
+ # Some components have extra dependencies
21
+ end
22
+ end
23
+
@@ -0,0 +1,76 @@
1
+ require 'sidekiq'
2
+ require "magic_pipe/senders/base"
3
+ require "magic_pipe/senders/metrics_mixin"
4
+
5
+ module MagicPipe
6
+ module Senders
7
+ class Async < Base
8
+ class Worker
9
+ include Sidekiq::Worker
10
+ include Senders::MetricsMixin
11
+
12
+ def perform(decomposed_object, topic, time, client_name)
13
+ client = MagicPipe.lookup_client(client_name)
14
+ object = client.loader.load(decomposed_object)
15
+ codec = client.codec
16
+
17
+ metadata = {
18
+ topic: topic,
19
+ producer: client.config.producer_name,
20
+ time: time.to_i,
21
+ mime: codec::TYPE
22
+ }
23
+
24
+ envelope = Envelope.new(
25
+ body: object,
26
+ **metadata
27
+ )
28
+
29
+ payload = codec.new(envelope).encode
30
+ client.transport.submit(payload, metadata)
31
+
32
+ track_success(client.metrics, topic)
33
+ rescue => e
34
+ track_failure(client.metrics, topic)
35
+ raise e
36
+ end
37
+ end
38
+
39
+
40
+ SETTINGS = {
41
+ "class" => Worker,
42
+ "retry" => true
43
+ }
44
+
45
+ def call
46
+ enqueue
47
+ end
48
+
49
+ def enqueue
50
+ options = SETTINGS.merge({
51
+ "queue" => queue_name,
52
+ "args" => [
53
+ decomposed_object,
54
+ @topic,
55
+ @time.to_i,
56
+ @config.client_name
57
+ ]
58
+ })
59
+ Sidekiq::Client.push(options)
60
+ end
61
+
62
+
63
+ private
64
+
65
+
66
+ def queue_name
67
+ @config.async_transport_options[:queue]
68
+ end
69
+
70
+ def decomposed_object
71
+ loader = MagicPipe::Loaders.lookup(@config.loader)
72
+ loader.new(@object, @wrapper).decompose
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,24 @@
1
+ module MagicPipe
2
+ module Senders
3
+ class Base
4
+ # object should be something similar
5
+ # to an ActiveModel::Serializer or
6
+ # ActiveRecord object.
7
+ #
8
+ def initialize(object, topic, wrapper, time, codec, transport, config, metrics)
9
+ @object = object
10
+ @topic = topic
11
+ @wrapper = wrapper
12
+ @time = time
13
+ @codec = codec
14
+ @transport = transport
15
+ @config = config
16
+ @metrics = metrics
17
+ end
18
+
19
+ def call
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module MagicPipe
2
+ module Senders
3
+ module MetricsMixin
4
+ def track_success(metrics, topic)
5
+ metrics.increment(
6
+ "magic_pipe.senders.mgs_sent",
7
+ tags: ["topic:#{topic}"]
8
+ )
9
+ end
10
+
11
+ def track_failure(metrics, topic)
12
+ metrics.increment(
13
+ "magic_pipe.senders.failure",
14
+ tags: ["topic:#{topic}"]
15
+ )
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ require "magic_pipe/senders/base"
2
+ require "magic_pipe/senders/metrics_mixin"
3
+
4
+ module MagicPipe
5
+ module Senders
6
+ class Sync < Base
7
+ include MetricsMixin
8
+
9
+ def call
10
+ metadata = build_metadata
11
+ envelope = build_message(metadata)
12
+ payload = @codec.new(envelope).encode
13
+ @transport.submit(payload, metadata)
14
+ track_success(@metrics, @topic)
15
+ rescue => e
16
+ track_failure(@metrics, @topic)
17
+ raise e
18
+ end
19
+
20
+ def build_message(metadata)
21
+ Envelope.new(body: data, **metadata)
22
+ end
23
+
24
+ def build_metadata
25
+ {
26
+ topic: @topic,
27
+ producer: @config.producer_name,
28
+ time: @time.to_i,
29
+ mime: @codec::TYPE
30
+ }
31
+ end
32
+
33
+ def data
34
+ @wrapper ? @wrapper.new(@object) : @object
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ module MagicPipe
2
+ module Transports
3
+ def self.lookup(type)
4
+ case type
5
+ when :https then Https
6
+ when :sqs then Sqs
7
+ when :log then Log
8
+ when :debug then Debug
9
+ when Array then Multi
10
+ when Class then type
11
+ else
12
+ raise ConfigurationError, "Unknown MagicPipe::Transports type: '#{type}'."
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ files = File.expand_path("../transports/**/*.rb", __FILE__)
19
+ Dir[files].each do |f|
20
+ begin
21
+ require f
22
+ rescue LoadError
23
+ # Some components have extra dependencies
24
+ end
25
+ end
26
+
@@ -0,0 +1,17 @@
1
+ module MagicPipe
2
+ module Transports
3
+ class Base
4
+ def initialize(config, metrics)
5
+ @config = config
6
+ @metrics = metrics
7
+ @logger = @config.logger
8
+ end
9
+
10
+ attr_reader :metrics, :logger
11
+
12
+ def submit(payload, metadata)
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require "magic_pipe/transports/base"
2
+
3
+ module MagicPipe
4
+ module Transports
5
+ class Debug < Base
6
+ def initialize(*)
7
+ end
8
+
9
+ def submit(payload, metadata)
10
+ $magic_pipe_out = {
11
+ payload: payload,
12
+ metadata: metadata
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,80 @@
1
+ require "magic_pipe/transports/base"
2
+
3
+ require "faraday"
4
+ require "typhoeus"
5
+ require "typhoeus/adapters/faraday"
6
+
7
+ module MagicPipe
8
+ module Transports
9
+ class Https < Base
10
+ def initialize(config, metrics)
11
+ super(config, metrics)
12
+ @options = @config.https_transport_options
13
+ @conn = build_connection
14
+ end
15
+
16
+ attr_reader :conn
17
+
18
+
19
+ # TODO: should this raise an error on failure?
20
+ # So that it can be retried?
21
+ #
22
+ def submit(payload, metadata)
23
+ @conn.post do |r|
24
+ r.body = payload
25
+ r.headers["X-MagicPipe-Sent-At"] = metadata[:time]
26
+ r.headers["X-MagicPipe-Topic"] = metadata[:topic]
27
+ r.headers["X-MagicPipe-Producer"] = metadata[:producer]
28
+ end
29
+ end
30
+
31
+
32
+ private
33
+
34
+ def url
35
+ @options.fetch(:url)
36
+ end
37
+
38
+ def auth_token
39
+ @options.fetch(:auth_token)
40
+ end
41
+
42
+ def timeout
43
+ @options.fetch(:timeout)
44
+ end
45
+
46
+ def open_timeout
47
+ @options.fetch(:open_timeout)
48
+ end
49
+
50
+ def content_type
51
+ MagicPipe::Codecs.lookup(@config.codec)::TYPE
52
+ end
53
+
54
+ def user_agent
55
+ "MagicPipe v%s (Faraday v%s, Typhoeus v%s)" % [
56
+ MagicPipe::VERSION,
57
+ Faraday::VERSION,
58
+ Typhoeus::VERSION
59
+ ]
60
+ end
61
+
62
+ # For a single backend, can't this be cached as a read only global?
63
+ #
64
+ def build_connection
65
+ Faraday.new(url) do |f|
66
+ f.request :retry, max: 2, interval: 0.1, backoff_factor: 2
67
+ f.request :basic_auth, auth_token, 'x'
68
+
69
+ f.headers['Content-Type'] = content_type
70
+ f.headers['User-Agent'] = user_agent
71
+
72
+ f.options.timeout = timeout
73
+ f.options.open_timeout = open_timeout
74
+
75
+ f.adapter :typhoeus
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end