magic_pipe 0.1.0

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