surfliner-metadata_consumer 0.1.0.pre.alpha.4 → 0.1.0.pre.alpha.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 832b89f48813e1a03fa41326e48e32030ecdd20f1f1719705b9fb87b6883b17e
4
- data.tar.gz: 406fc690ac6d2a771737953d53babf83f52406b9870b5837653e02357083bb1b
3
+ metadata.gz: 2fff0fe7a29854ecb3b75f91ed5aa1e5abdbf6fe7fc625b4b117a3e2c4e4805c
4
+ data.tar.gz: 534536637b909d309f90095aaba4e9cb51a75d4ca92a78531971d3550ea42bfc
5
5
  SHA512:
6
- metadata.gz: 5f6fda0ab5aa1cc54053bd688ef353d3d303adb27a3820aef8506c90357860592fae98b201622b82513ba501b111dc35c52379b434ed6082a1b73edb4acef88b
7
- data.tar.gz: 8106c7d167c2bcd1046c403d9a171f9b8c1c2d72651d002f08a828326f993cce7f45863e3e7db32928eeb295bdf58ee947303a502eb69366522a30ed14262dad
6
+ metadata.gz: 6e20f325aa14042b5ed7e950803d8a045834df543da1f5c4a8f30297645015d97add22a5de38e01d15a6ab74adb856777bba4fccfb609d50bd622c9fae1f51d5
7
+ data.tar.gz: a69109802f8c408f077b2fc2d1c748c02a900eda042d6e40a4423ee3257b39d2dde892eecf9c77c690df06aa53d367136a8246f3d8bc69114d0e1278326b4531
data/CHANGES.md CHANGED
@@ -1,3 +1,36 @@
1
+ # 0.1.0.pre.alpha.5 (2025-03-25)
2
+
3
+ In order to support using one connection (and one underlying `Bunny::Session`)
4
+ with multiple topics, queues, and/or routing keys, as well as publishing to
5
+ a topic without creating or subscribing to a queue, this release introduces
6
+ the following **breaking changes**:
7
+
8
+ - RabbitMQ-related configuration and connection code (i.e. not specific to
9
+ the consumer/handler setup) has been moved from `Surfliner::MetadataConsumer`
10
+ to `Surfliner::Mq`. This may move to a separate gem in a future release.
11
+ - `MetadataConsumer::MqConnection` is now `Mq::Connection`
12
+ - `MetadataConsumer::MqConfig` is now `Mq::ConnectionConfig`
13
+ - `ConnectionConfig` no longer includes topic or queue configuration;
14
+ topics are configured with `TopicConfig` and queues with
15
+ `QueueConfig`.
16
+ - The `MqConnection#open` method, which yielded a `Bunny::Queue`, has been
17
+ replaced by the `Connection#with_topic` method, which yields an
18
+ `Mq::Topic` object wrapping a single topic; call `Mq::Topic#bind_queue`
19
+ to bind to a queue. Like the removed`#open`, `#with_topic` opens and
20
+ closes the connection implicitly and allows connecting only to a single
21
+ topic.
22
+ - The `Mq::Connection#topic_from(config)` method allows you to connect to
23
+ any number of topics, but you must open and close the connection
24
+ explicitly.
25
+ - The `MqConnection#publish` method has been moved to `Mq::Topic` and can
26
+ now take an explicit routing key, although if one is not provided it will
27
+ still default to `ENV["RABBITMQ_PLATFORM_ROUTING_KEY"]`.
28
+ - `MetadataConsumer::Consumer` now requires an explicit `Mq:Connection`
29
+ instead of creating one by default.
30
+
31
+ In addition, the `daylight-index-listen` script has been renamed to the more
32
+ generic `index-on-publish`.
33
+
1
34
  # 0.1.0.pre.alpha.4 (2025-03-17)
2
35
 
3
36
  - lower required Ruby version to 3.2
@@ -19,4 +52,4 @@
19
52
 
20
53
  # 0.1.0.pre.alpha (2025-02-12)
21
54
 
22
- - Initial release
55
+ - Initial release
data/Dockerfile CHANGED
@@ -56,4 +56,4 @@ COPY --from=build $APP_PATH/Gemfile.lock $APP_PATH
56
56
  RUN bundle config set frozen 'true'
57
57
 
58
58
  # TODO: don't hard-code a specific handler / script
59
- CMD ["./bin/daylight-index-listen"]
59
+ CMD ["./bin/index-on-publish"]
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- surfliner-metadata_consumer (0.1.0.pre.alpha.4)
4
+ surfliner-metadata_consumer (0.1.0.pre.alpha.5)
5
5
  bunny (~> 2.23)
6
6
  opentelemetry-exporter-otlp (~> 0.26.3)
7
7
  opentelemetry-instrumentation-all (~> 0.60.0)
@@ -14,11 +14,11 @@ GEM
14
14
  addressable (2.8.7)
15
15
  public_suffix (>= 2.0.2, < 7.0)
16
16
  amq-protocol (2.3.3)
17
- ast (2.4.2)
17
+ ast (2.4.3)
18
18
  bigdecimal (3.1.9)
19
19
  builder (3.3.0)
20
- bunny (2.23.0)
21
- amq-protocol (~> 2.3, >= 2.3.1)
20
+ bunny (2.24.0)
21
+ amq-protocol (~> 2.3)
22
22
  sorted_set (~> 1, >= 1.0.2)
23
23
  ci_reporter (2.1.0)
24
24
  builder (>= 2.1.2)
@@ -34,7 +34,7 @@ GEM
34
34
  debug (1.9.2)
35
35
  irb (~> 1.10)
36
36
  reline (>= 0.3.8)
37
- diff-lcs (1.6.0)
37
+ diff-lcs (1.6.1)
38
38
  docile (1.4.1)
39
39
  dotenv (2.8.1)
40
40
  faraday (2.12.2)
@@ -43,8 +43,9 @@ GEM
43
43
  logger
44
44
  faraday-net_http (3.4.0)
45
45
  net-http (>= 0.5.0)
46
+ github-markup (5.0.1)
46
47
  google-protobuf (3.25.6)
47
- googleapis-common-protos-types (1.18.0)
48
+ googleapis-common-protos-types (1.19.0)
48
49
  google-protobuf (>= 3.18, < 5.a)
49
50
  hashdiff (1.1.2)
50
51
  io-console (0.8.0)
@@ -255,12 +256,13 @@ GEM
255
256
  opentelemetry-semantic_conventions (1.11.0)
256
257
  opentelemetry-api (~> 1.0)
257
258
  parallel (1.26.3)
258
- parser (3.3.7.1)
259
+ parser (3.3.7.2)
259
260
  ast (~> 2.4.1)
260
261
  racc
261
262
  pp (0.6.2)
262
263
  prettyprint
263
264
  prettyprint (0.2.0)
265
+ prism (1.4.0)
264
266
  psych (5.2.3)
265
267
  date
266
268
  stringio
@@ -269,8 +271,9 @@ GEM
269
271
  rainbow (3.1.1)
270
272
  rake (13.2.1)
271
273
  rbtree (0.4.6)
272
- rdoc (6.12.0)
274
+ rdoc (6.13.0)
273
275
  psych (>= 4.0.0)
276
+ redcarpet (3.6.1)
274
277
  regexp_parser (2.10.0)
275
278
  reline (0.6.0)
276
279
  io-console (~> 0.5)
@@ -302,8 +305,9 @@ GEM
302
305
  rubocop-ast (>= 1.38.0, < 2.0)
303
306
  ruby-progressbar (~> 1.7)
304
307
  unicode-display_width (>= 2.4.0, < 4.0)
305
- rubocop-ast (1.39.0)
306
- parser (>= 3.3.1.0)
308
+ rubocop-ast (1.43.0)
309
+ parser (>= 3.3.7.2)
310
+ prism (~> 1.4)
307
311
  rubocop-performance (1.24.0)
308
312
  lint_roller (~> 1.1)
309
313
  rubocop (>= 1.72.1, < 2.0)
@@ -334,7 +338,7 @@ GEM
334
338
  standard-performance (1.7.0)
335
339
  lint_roller (~> 1.1)
336
340
  rubocop-performance (~> 1.24.0)
337
- stringio (3.1.5)
341
+ stringio (3.1.6)
338
342
  unicode-display_width (3.1.4)
339
343
  unicode-emoji (~> 4.0, >= 4.0.4)
340
344
  unicode-emoji (4.0.4)
@@ -354,7 +358,9 @@ DEPENDENCIES
354
358
  colorize (~> 0.8)
355
359
  debug (~> 1.9.2)
356
360
  dotenv (~> 2.7)
361
+ github-markup (~> 5.0)
357
362
  rake (~> 13.0)
363
+ redcarpet (~> 3.6)
358
364
  rspec (~> 3.13)
359
365
  simplecov (~> 0.22)
360
366
  simplecov-cobertura (~> 2.1)
data/README.md CHANGED
@@ -36,23 +36,59 @@ if prior steps succeed.
36
36
 
37
37
  ## Configuration
38
38
 
39
- <!--
40
- TODO: Just provide a sample env file
41
- -->
39
+ > Note: Sample values taken from Surfliner's development
40
+ > [daylight-listener.sh](https://gitlab.com/surfliner/surfliner/-/blob/trunk/docker-compose/env/daylight-listener.sh).)
41
+
42
+ An `Mq::Connection` object, suitable for listening or publishing, can be configured
43
+ explicitly with a `ConnectionConfig` object, or implicitly, reading a default
44
+ configuration with `ConnectionConfig#from_env`. `ConnectionConfig#from_env` expects
45
+ the following environment variables:
46
+
47
+ | Variable | Sample value | Description |
48
+ |-----------------------------|--------------|------------------------------|
49
+ | `RABBITMQ_HOST` | `rabbitmq` | Hostname of RabbitMQ server |
50
+ | `RABBITMQ_NODE_PORT_NUMBER` | `5672` | Port name of RabbitMQ server |
51
+ | `RABBITMQ_USERNAME` | `user` | RabbitMQ username |
52
+ | `RABBITMQ_PASSWORD` | `bitnami` | RabbitMQ password |
53
+
54
+ The `Mq::Connection#with_topic` and `Mq::Connection#topic_from` can either take an
55
+ explicit `TopicConfig` object, or implicitly read a default configuration with
56
+ `TopicConfig#from_env`. `TopicConfig#from_env` expects the following environment variables:
57
+
58
+ | Variable | Sample value | Description |
59
+ |------------------|----------------------|---------------------|
60
+ | `RABBITMQ_TOPIC` | `surfliner.metadata` | RabbitMQ topic name |
61
+
62
+ Note that `TopicConfig#from_env` also accepts keyword options, which are passed through
63
+ to [`Bunny::Channel#topic`](https://api.rubybunny.info/Bunny/Channel.html#topic-instance_method).
64
+ E.g.
65
+
66
+ ```ruby
67
+ topic_config = TopicConfig.from_env(durable: true, auto_delete: true)
68
+ connection.with_topic(topic_config) do |topic|
69
+ # ...
70
+ end
71
+ ```
42
72
 
43
- The `MqConnection` class expects the following environment variables:
73
+ Similarly, `Mq::Topic#bind_queue` can either take an explicit `QueueConfig`, or implicitly
74
+ read a default with `QueConfig#from_env`. `QueueConfig#from_env` expects the following environment
75
+ variables:
44
76
 
45
- ```sh
46
- RABBITMQ_HOST=rabbitmq
47
- RABBITMQ_NODE_PORT_NUMBER=5672
48
- RABBITMQ_PASSWORD=bitnami
49
- RABBITMQ_QUEUE=surfliner.metadata
50
- RABBITMQ_PLATFORM_ROUTING_KEY=surfliner.metadata.daylight
51
- RABBITMQ_TOPIC=surfliner.metadata
52
- RABBITMQ_USERNAME=user
53
- ```
77
+ | Variable | Sample value | Description |
78
+ |---------------------------------|-------------------------------|----------------------|
79
+ | `RABBITMQ_QUEUE` | `surfliner.metadata` | RabbitMQ queue name |
80
+ | `RABBITMQ_PLATFORM_ROUTING_KEY` | `surfliner.metadata.daylight` | RabbitMQ routing key |
54
81
 
55
- (Sample values taken from Surfliner's development [daylight-listener.sh](https://gitlab.com/surfliner/surfliner/-/blob/trunk/docker-compose/env/daylight-listener.sh).)
82
+ And `QueueConfig#from_env` similarly accepts keyword options, which are forwarded to
83
+ [`Bunny::Channel#queue`](https://api.rubybunny.info/Bunny/Channel.html#queue-instance_method):
84
+
85
+ ```ruby
86
+ connection.with_topic(topic_config) do |topic|
87
+ queue_config = QueueConfig.from_env(exclusive: true, durable: true)
88
+ queue = topic.bind_queue(queue_config)
89
+ # ...
90
+ end
91
+ ```
56
92
 
57
93
  The Solr / Daylight handler implementation (see below) additionally expects
58
94
  a configured `SOLR_URL`, e.g.:
@@ -74,17 +110,19 @@ The `Surfliner::MetadataConsumer::Solr` package contains a handler that retrieve
74
110
  metadata for `:published`/`:updated` events, transforms results for indexing, and
75
111
  updates a target Solr index.
76
112
 
77
- <!-- TODO: better name for this script -->
113
+ The `bin/index-on-publish` script starts a consumer using this handler, and accepts
114
+ (but does not require) the following environment variables:
78
115
 
79
- The `bin/daylight-index-listen` script starts a consumer using this handler.
80
-
81
- In addition to the environment variables listed above, this handler expects
82
- a configured `SOLR_URL`, e.g.:
116
+ | Variable | Sample value | Default | Description |
117
+ |---------------------|-------------------------------|----------------|----------------------------------|
118
+ | `OTEL_SDK_DISABLED` | `true` | (not set) | Whether to disable OpenTelemetry |
119
+ | `OTEL_SERVICE_NAME` | `surfliner-daylight-consumer` | name of script | OpenTelemetry service name |
120
+ | `OTEL_TRACER_NAME` | `DaylightConsumerTracer` | name of script | OpenTelemetry tracer name |
83
121
 
84
122
  ### Running the service
85
123
 
86
124
  <!--
87
- TODO: Dockerfile shouldn't be daylight-index-listen specific
125
+ TODO: Dockerfile shouldn't be index-on-publish specific
88
126
  -->
89
127
 
90
128
  Build the listener Dockerfile and tag it with the name `solr-listener`:
@@ -101,5 +139,5 @@ docker run --env-file <env-file> solr-listener
101
139
 
102
140
  ## Utility scripts
103
141
 
104
- - `bin/daylight-index-listen` script starts a consumer using the Daylight Solr handler.
105
- - `bin/simulate-publish-event` posts a publish event to the queue configured with `MqConnection`
142
+ - `bin/index-on-publish` script starts a consumer using `MetadataConsumer::Solr::MessageHandler`.
143
+ - `bin/simulate-publish-event` posts a publish event to the queue configured with `Mq::Connection`
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ require "logger"
4
+
5
+ require "surfliner/metadata_consumer"
6
+ require "surfliner/mq"
7
+
8
+ $stdout.sync = true # don't buffer log output
9
+ logger = Logger.new($stdout).tap do |logger|
10
+ logger.level = ENV.fetch("LOG_LEVEL", Logger::INFO)
11
+ end
12
+
13
+ unless ENV["OTEL_SDK_DISABLED"] == "true"
14
+ require "opentelemetry/sdk"
15
+ require "opentelemetry-exporter-otlp"
16
+
17
+ otel_service_name = ENV.fetch('OTEL_SERVICE_NAME', File.basename(__FILE__))
18
+ otel_tracer_name = ENV.fetch('OTEL_TRACER_NAME', File.basename(__FILE__))
19
+
20
+ OpenTelemetry::SDK.configure do |c|
21
+ c.service_name = otel_service_name
22
+ c.use_all # enables auto instrumentation for Bunny, Net::HTTP, etc...
23
+ end
24
+
25
+ tracer = OpenTelemetry.tracer_provider.tracer(otel_tracer_name)
26
+ end
27
+
28
+ consumer = Surfliner::MetadataConsumer::Consumer.new(
29
+ connection: Surfliner::Mq::Connection.new(logger: logger),
30
+ tracer: (tracer if defined?(tracer)),
31
+ handler: Surfliner::MetadataConsumer::Solr::MessageHandler
32
+ )
33
+ consumer.run(topic_config: Surfliner::Mq::TopicConfig.from_env(auto_delete: true))
@@ -4,24 +4,24 @@ require "json"
4
4
  require "logger"
5
5
 
6
6
  require "surfliner/metadata_consumer"
7
+ require "surfliner/mq"
7
8
 
8
9
  module Surfliner
9
10
  module MetadataConsumer
10
11
  class Publisher
12
+ attr_reader :topic
13
+ attr_reader :api_base
14
+ attr_reader :routing_key
11
15
 
12
- attr_reader :logger
13
- attr_reader :topic_opts
14
- attr_reader :queue_opts
15
-
16
- def initialize(logger:, topic_opts: {}, queue_opts: {})
17
- @logger = logger
18
- @topic_opts = topic_opts
19
- @queue_opts = queue_opts
16
+ def initialize(topic, api_base:, routing_key:)
17
+ @topic = topic
18
+ @api_base = api_base
19
+ @routing_key = routing_key
20
20
  end
21
21
 
22
22
  def publish(resource_id)
23
23
  payload = payload_for(resource_id)
24
- connection.publish(payload)
24
+ topic.publish(payload, routing_key:)
25
25
  end
26
26
 
27
27
  def payload_for(id)
@@ -30,27 +30,37 @@ module Surfliner
30
30
  time_stamp: DateTime.now }.to_json
31
31
  end
32
32
 
33
- def connection
34
- @connection ||= MqConnection.new(logger:).connect(topic_opts:, queue_opts:)
35
- end
36
-
37
- def api_base
38
- @api_base ||= ENV.fetch("METADATA_API_URL_BASE", "http://metadata.test/resources")
33
+ class << self
34
+ # Connects with default (environment-configured) settings, yields a publisher
35
+ # for the specified topic, and closes the connection after the provided block
36
+ # completes.
37
+ #
38
+ # @param logger [Logger] the logger
39
+ # @param topic_opts [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
40
+ #
41
+ # @yieldparam publisher [Publisher] the publisher
42
+ def connect(logger:, **topic_opts)
43
+ topic_config = Mq::TopicConfig.from_env(**topic_opts)
44
+ Mq::Connection.new(logger:).with_topic(topic_config) do |topic|
45
+ yield Publisher.new(
46
+ topic,
47
+ api_base: ENV.fetch("METADATA_API_URL_BASE", "http://metadata.test/resources"),
48
+ routing_key: ENV.fetch("RABBITMQ_PLATFORM_ROUTING_KEY")
49
+ )
50
+ end
51
+ end
39
52
  end
40
53
  end
41
54
  end
42
55
  end
43
56
 
44
57
  $stdout.sync = true # don't buffer log output
45
- logger = Logger.new($stdout).tap do |logger|
58
+ logger = Logger.new($stdout).tap do |logger|
46
59
  logger.level = ENV.fetch("LOG_LEVEL", Logger::INFO)
47
60
  end
48
61
 
49
- publisher = Surfliner::MetadataConsumer::Publisher.new(logger:)
50
- begin
62
+ Surfliner::MetadataConsumer::Publisher.connect(logger:, auto_delete: true) do |publisher|
51
63
  ARGV.each do |resource_id|
52
64
  publisher.publish(resource_id)
53
65
  end
54
- ensure
55
- publisher.connection.close
56
66
  end
@@ -1,5 +1,8 @@
1
1
  require "rsolr"
2
- require "surfliner/metadata_consumer/mq_connection"
2
+ require "surfliner/util/assert"
3
+ require "surfliner/mq/connection"
4
+ require "surfliner/mq/topic_config"
5
+ require "surfliner/mq/queue_config"
3
6
  require "surfliner/metadata_consumer/solr/message_handler"
4
7
 
5
8
  module Surfliner
@@ -7,29 +10,40 @@ module Surfliner
7
10
  # A metadata consumer that subscribes to a RabbitMQ queue and passes
8
11
  # messages to the specified handler.
9
12
  class Consumer
10
- attr_reader :connection, :logger, :tracer, :handler
13
+ include Util::Assert
14
+
15
+ # @return [Mq::Connection] the connection
16
+ attr_reader :connection
17
+
18
+ # @return [OpenTelemetry::Trace::Tracer] OpenTelemetry tracer
19
+ attr_reader :tracer
20
+
21
+ # @return [#handle] an object accepting a JSON string
22
+ attr_reader :handler
11
23
 
12
24
  # Initializes a new `Consumer`
25
+ #
26
+ # @param connection [Mq::Connection] the connection
27
+ # @param handler [#handle] an object accepting a JSON string
13
28
  # @param tracer [OpenTelemetry::Trace::Tracer] OpenTelemetry tracer
14
- # @param logger [Logger] log message destination
15
- # @param handler #handle an object accepting a JSON string
16
- def initialize(tracer:, logger:, handler:)
17
- @connection = MqConnection.new(logger:)
18
- @logger = logger
29
+ def initialize(connection:, handler:, tracer: nil)
30
+ @connection = not_nil!(connection, "connection")
31
+ @handler = not_nil!(handler, "handler")
19
32
  @tracer = tracer
20
- @handler = handler
33
+ end
34
+
35
+ # @return [Logger] log message destination
36
+ def logger
37
+ connection.logger
21
38
  end
22
39
 
23
40
  # Starts listening to the message queue and passing messages to the handler.
24
- # @param queue_opts [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
25
- # @param topic_opts [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
26
- def run(queue_opts: {}, topic_opts: {})
27
- connection.open(queue_opts:, topic_opts:) do |queue|
28
- queue.subscribe(block: true) do |_delivery_info, _properties, payload_json|
29
- tracer.in_span("surfliner metadata consumer message") do |_span|
30
- handle(payload_json)
31
- end
32
- end
41
+ # @param topic_config [TopicConfig] topic configuration
42
+ # @param queue_config [QueueConfig] queue configuration
43
+ def run(topic_config: Mq::TopicConfig.from_env, queue_config: Mq::QueueConfig.from_env)
44
+ connection.with_topic(topic_config) do |topic|
45
+ queue = topic.bind_queue(queue_config)
46
+ queue.subscribe(block: true, &method(:on_delivery))
33
47
  end
34
48
  end
35
49
 
@@ -43,6 +57,16 @@ module Surfliner
43
57
  rescue => err
44
58
  logger.error(" [!] failed to handle message: #{err.full_message}")
45
59
  end
60
+
61
+ private
62
+
63
+ def on_delivery(_delivery_info, _properties, payload_json)
64
+ return handle(payload_json) unless tracer
65
+
66
+ tracer.in_span("surfliner metadata consumer message") do |_span, _context|
67
+ handle(payload_json)
68
+ end
69
+ end
46
70
  end
47
71
  end
48
72
  end
@@ -1,8 +1,7 @@
1
1
  # Umbrella module for general Surfliner code
2
2
  module Surfliner
3
- # Parent module for this gem
4
3
  module MetadataConsumer
5
4
  # The gem version
6
- VERSION = "0.1.0.pre.alpha.4"
5
+ VERSION = "0.1.0.pre.alpha.5"
7
6
  end
8
7
  end
@@ -1 +1,4 @@
1
- Dir.glob(File.expand_path("metadata_consumer/*.rb", __dir__)).sort.each(&method(:require))
1
+ Dir.glob(File.expand_path("metadata_consumer/*.rb", __dir__)).each(&method(:require))
2
+
3
+ # Functionality specific to consuming RabbitMQ feeds
4
+ module Surfliner::MetadataConsumer; end
@@ -0,0 +1,127 @@
1
+ require "bunny"
2
+ require "surfliner/util/assert"
3
+
4
+ module Surfliner
5
+ module Mq
6
+ # An object encapsulating a RabbitMQ connection.
7
+ class Connection
8
+ include Util::Assert
9
+
10
+ # @return [Logger] The logger
11
+ attr_reader :logger
12
+
13
+ # @return [Bunny::Session] The current RabbitMQ session
14
+ attr_reader :session
15
+
16
+ # @return [Bunny::Channel] The channel being listened to
17
+ attr_reader :channel
18
+
19
+ # @return [ConnectionConfig] The configuration
20
+ attr_reader :config
21
+
22
+ # Initializes a new `MqConnection`.
23
+ #
24
+ # @param logger [Logger] the logger
25
+ # @param config [ConnectionConfig] the configuration
26
+ def initialize(logger:, config: ConnectionConfig.from_env)
27
+ @logger = not_nil!(logger)
28
+ @config = not_nil!(config)
29
+ end
30
+
31
+ # Opens a connection.
32
+ # @return [self]
33
+ # @raise [IOError] if already connected
34
+ def connect
35
+ raise IOError, "RabbitMQ session #{session} already open." if open?
36
+
37
+ logger.info("Rabbitmq message broker session url: #{config.redacted_url}")
38
+ @session = Bunny.new(config.session_url, logger: logger)
39
+ connect_on(session)
40
+ @channel = session.create_channel
41
+
42
+ self
43
+ rescue Bunny::TCPConnectionFailed => err
44
+ # TODO: realistically, this only happens in session.start, where we're eating it
45
+ logger.error("Connection to #{config.redacted_url} failed")
46
+ raise err
47
+ rescue Bunny::PossibleAuthenticationFailureError => err
48
+ # TODO: realistically, this only happens in session.start, where we're eating it
49
+ logger.error("Failed to authenticate to #{config.redacted_url}")
50
+ raise err
51
+ end
52
+
53
+ # Opens a session, yields a client for the specified topic, and closes the
54
+ # session after the provided block completes.
55
+ # @param config [TopicConfig] topic configuration
56
+ # @yield [MqTopic] A client for the topic
57
+ def with_topic(config = TopicConfig.from_env)
58
+ connect
59
+
60
+ yield topic_from(config)
61
+ ensure
62
+ close
63
+ end
64
+
65
+ # Returns a client for the specified topic. Note that this does _not_ open
66
+ # or close the connection.
67
+ #
68
+ # @param config [TopicConfig] topic configuration
69
+ # @return [Topic] A client for the topic
70
+ # @raise [IOError] if the connection is not open
71
+ def topic_from(config)
72
+ raise IOError, "RabbitMQ session not open" unless open?
73
+
74
+ Topic.from_config(config, channel:, logger:)
75
+ end
76
+
77
+ # Closes the session.
78
+ def close
79
+ return unless channel
80
+ return if channel.closed?
81
+ logger.info("closing channel")
82
+ channel.close
83
+ ensure
84
+ logger.info("closing session")
85
+ session&.close
86
+ end
87
+
88
+ # @return [true, false] True if the session is open, false otherwise
89
+ def open?
90
+ session&.status == :open
91
+ end
92
+
93
+ # @return [Symbol, nil] The session status, or nil if there is no session
94
+ def status
95
+ session&.status
96
+ end
97
+
98
+ # @return [String] The RabbitMQ hostname
99
+ def host
100
+ config.host
101
+ end
102
+
103
+ # @return [String] The RabbitMQ port
104
+ def port
105
+ config.port
106
+ end
107
+
108
+ private
109
+
110
+ def connect_on(session, timeout = 120)
111
+ timer = 0
112
+ logger.info "Trying to open queue session with timeout=#{timeout}"
113
+ while timer < timeout
114
+ begin
115
+ session.start
116
+ rescue
117
+ # TODO: do we actually want to rescue from everything?
118
+ end
119
+ return session if session.status == :open
120
+ sleep 1
121
+ timer += 1
122
+ end
123
+ raise "Failed to connect to queue."
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,62 @@
1
+ module Surfliner
2
+ module Mq
3
+ # An object encapsulating RabbitMQ configuration.
4
+ class ConnectionConfig
5
+ # @return [String] The RabbitMQ hostname
6
+ attr_reader :host
7
+
8
+ # @return [String] The RabbitMQ AMQP port
9
+ attr_reader :port
10
+
11
+ # @return [String] The RabbitMQ username
12
+ attr_reader :username
13
+
14
+ # @return [String] The RabbitMQ passsword
15
+ attr_reader :password
16
+
17
+ # Initializes a new `MqConfig` object.
18
+ # @param host [String] RabbitMQ hostname
19
+ # @param port [String] RabbitMQ AMQP port
20
+ # @param username [String] RabbitMQ username
21
+ # @param password [String] RabbitMQ passsword
22
+ def initialize(host:, port:, username:, password:)
23
+ @host = host
24
+ @port = port
25
+ @username = username
26
+ @password = password
27
+ end
28
+
29
+ class << self
30
+ # Reads RabbitMQ configuration from environment variables and
31
+ # returns it as a new `ConnectionConfig` object.
32
+ #
33
+ # | Variable | Sample value | Description |
34
+ # |-----------------------------|--------------|------------------------------|
35
+ # | `RABBITMQ_HOST` | `rabbitmq` | Hostname of RabbitMQ server |
36
+ # | `RABBITMQ_NODE_PORT_NUMBER` | `5672` | Port name of RabbitMQ server |
37
+ # | `RABBITMQ_USERNAME` | `user` | RabbitMQ username |
38
+ # | `RABBITMQ_PASSWORD` | `bitnami` | RabbitMQ password |
39
+ #
40
+ # @return [ConnectionConfig] The configuration.
41
+ def from_env
42
+ ConnectionConfig.new(
43
+ host: ENV.fetch("RABBITMQ_HOST"),
44
+ port: ENV.fetch("RABBITMQ_NODE_PORT_NUMBER"),
45
+ username: ENV.fetch("RABBITMQ_USERNAME"),
46
+ password: ENV.fetch("RABBITMQ_PASSWORD")
47
+ )
48
+ end
49
+ end
50
+
51
+ # @return [String] the connection URL as a string
52
+ def session_url
53
+ @session_url ||= "amqp://#{username}:#{password}@#{host}:#{port}"
54
+ end
55
+
56
+ # @return [String] the connection URL as a string, without the password
57
+ def redacted_url
58
+ @redacted_url ||= session_url.sub(password, "REDACTED")
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ module Surfliner
2
+ module Mq
3
+ # Encapsulates queue configuration
4
+ class QueueConfig
5
+ # @return [String] The queue to listen to
6
+ attr_reader :name
7
+
8
+ # @return [String] platform routing key to listen to
9
+ attr_reader :routing_key
10
+
11
+ # @return [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
12
+ attr_reader :options
13
+
14
+ # @param name [String] queue exchange to listen to
15
+ # @param routing_key [String] platform routing key to listen to
16
+ # @param options [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
17
+ def initialize(name:, routing_key:, options: {})
18
+ @name = name
19
+ @routing_key = routing_key
20
+ @options = options
21
+ end
22
+
23
+ class << self
24
+ # Returns a default (environment-variable-based) configuration with the
25
+ # specified options.
26
+ #
27
+ # | Variable | Sample value | Description |
28
+ # |---------------------------------|-------------------------------|----------------------|
29
+ # | `RABBITMQ_QUEUE` | `surfliner.metadata` | RabbitMQ queue name |
30
+ # | `RABBITMQ_PLATFORM_ROUTING_KEY` | `surfliner.metadata.daylight` | RabbitMQ routing key |
31
+ #
32
+ # @param options [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
33
+ # @return [QueueConfig] The configuration.
34
+ def from_env(**options)
35
+ QueueConfig.new(
36
+ name: ENV.fetch("RABBITMQ_QUEUE"),
37
+ routing_key: default_routing_key,
38
+ options:
39
+ )
40
+ end
41
+
42
+ # @return [String] The default routing key from `ENV["RABBITMQ_PLATFORM_ROUTING_KEY"]`
43
+ def default_routing_key
44
+ ENV.fetch("RABBITMQ_PLATFORM_ROUTING_KEY")
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ require "surfliner/util/assert"
2
+
3
+ module Surfliner
4
+ module Mq
5
+ # Encapsulates a RabbitMQ topic
6
+ class Topic
7
+ include Surfliner::Util::Assert
8
+
9
+ # @return [String] The name of the topic
10
+ attr_reader :name
11
+
12
+ # @return [Bunny::Channel] The channel being used
13
+ attr_reader :channel
14
+
15
+ # @return [Logger] The logger
16
+ attr_reader :logger
17
+
18
+ # @return [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
19
+ attr_reader :options
20
+
21
+ # @param name [String] The name of the topic
22
+ # @param channel [Bunny::Channel] The channel to use
23
+ # @param logger [Logger] the logger
24
+ # @param options [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
25
+ def initialize(name, channel:, logger:, options: {})
26
+ @name = not_nil!(name, "topic name")
27
+ @channel = not_nil!(channel, "channel")
28
+ @logger = not_nil!(logger, "logger")
29
+ @options = not_nil!(options, "options")
30
+ end
31
+
32
+ # @return [Bunny::Exchange] The exchange for the topic
33
+ def exchange
34
+ @exchange ||= channel.topic(name, options)
35
+ end
36
+
37
+ # Publishes the specified payload
38
+ # @param payload [String] the payload to publish
39
+ # @param routing_key [String] platform routing key to publish to
40
+ # @return [Bunny::Exchange] see #exchange
41
+ def publish(payload, routing_key: QueueConfig.default_routing_key)
42
+ logger.info "Publishing to #{routing_key} with payload: #{payload}"
43
+ exchange.publish(payload, routing_key:)
44
+ end
45
+
46
+ # @param config [QueueConfig] queue configuration
47
+ # @return [Bunny::Queue] the queue
48
+ def bind_queue(config = QueueConfig.from_env)
49
+ channel.queue(config.name, config.options).tap do |q|
50
+ q.bind(exchange, routing_key: config.routing_key)
51
+ end
52
+ end
53
+
54
+ class << self
55
+ # Creates a new `MqTopic` from the specified configuration
56
+ # @param config [TopicConfig] the configuration
57
+ # @param channel [Bunny::Channel] The channel to use
58
+ # @param logger [Logger] the logger
59
+ def from_config(config, channel:, logger:)
60
+ new(config.name, channel:, logger:, options: config.options)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ module Surfliner
2
+ module Mq
3
+ # Encapsulates topic configuration
4
+ class TopicConfig
5
+ # @return [String] The topic exchange to listen to
6
+ attr_reader :name
7
+
8
+ # @return [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
9
+ attr_reader :options
10
+
11
+ # @param name [String] topic exchange to listen to
12
+ # @param options [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
13
+ def initialize(name:, options: {})
14
+ @name = name
15
+ @options = options
16
+ end
17
+
18
+ class << self
19
+ # Returns a default (environment-variable-based) configuration with the
20
+ # specified options.
21
+ #
22
+ # | Variable | Sample value | Description |
23
+ # |------------------|----------------------|---------------------|
24
+ # | `RABBITMQ_TOPIC` | `surfliner.metadata` | RabbitMQ topic name |
25
+ #
26
+ # @param options [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
27
+ # @return [TopicConfig] The configuration.
28
+ def from_env(**options)
29
+ TopicConfig.new(
30
+ name: ENV.fetch("RABBITMQ_TOPIC"),
31
+ options:
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ Dir.glob(File.expand_path("mq/*.rb", __dir__)).each(&method(:require))
2
+
3
+ # General RabbitMQ functionality
4
+ module Surfliner::Mq; end
@@ -0,0 +1,21 @@
1
+ module Surfliner
2
+ # Miscellaneous utility module
3
+ module Util
4
+ # Helper methods for argument validation
5
+ module Assert
6
+ class << self
7
+ include Assert
8
+ end
9
+
10
+ # @param arg_value [Object, nil] the argument value
11
+ # @param arg_name [String] the name of the argument, for error messages
12
+ # @return arg_value
13
+ # @raise [ArgumentError] if `arg_value` is `nil`
14
+ def not_nil!(arg_value, arg_name = "argument")
15
+ return arg_value unless arg_value.nil?
16
+
17
+ raise ArgumentError, "#{arg_name} must not be nil"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ Dir.glob(File.expand_path("util/*.rb", __dir__)).each(&method(:require))
2
+
3
+ # Miscellaneous utility module
4
+ module Surfliner::Util; end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surfliner-metadata_consumer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.alpha.4
4
+ version: 0.1.0.pre.alpha.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Project Surfliner
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-17 00:00:00.000000000 Z
10
+ date: 2025-03-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bunny
@@ -86,103 +86,117 @@ dependencies:
86
86
  - !ruby/object:Gem::Version
87
87
  version: '3'
88
88
  - !ruby/object:Gem::Dependency
89
- name: debug
89
+ name: ci_reporter_rspec
90
90
  requirement: !ruby/object:Gem::Requirement
91
91
  requirements:
92
92
  - - "~>"
93
93
  - !ruby/object:Gem::Version
94
- version: 1.9.2
94
+ version: '1.0'
95
95
  type: :development
96
96
  prerelease: false
97
97
  version_requirements: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - "~>"
100
100
  - !ruby/object:Gem::Version
101
- version: 1.9.2
101
+ version: '1.0'
102
102
  - !ruby/object:Gem::Dependency
103
- name: rspec
103
+ name: colorize
104
104
  requirement: !ruby/object:Gem::Requirement
105
105
  requirements:
106
106
  - - "~>"
107
107
  - !ruby/object:Gem::Version
108
- version: '3.13'
108
+ version: '0.8'
109
109
  type: :development
110
110
  prerelease: false
111
111
  version_requirements: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - "~>"
114
114
  - !ruby/object:Gem::Version
115
- version: '3.13'
115
+ version: '0.8'
116
116
  - !ruby/object:Gem::Dependency
117
- name: standard
117
+ name: debug
118
118
  requirement: !ruby/object:Gem::Requirement
119
119
  requirements:
120
120
  - - "~>"
121
121
  - !ruby/object:Gem::Version
122
- version: '1.31'
122
+ version: 1.9.2
123
123
  type: :development
124
124
  prerelease: false
125
125
  version_requirements: !ruby/object:Gem::Requirement
126
126
  requirements:
127
127
  - - "~>"
128
128
  - !ruby/object:Gem::Version
129
- version: '1.31'
129
+ version: 1.9.2
130
130
  - !ruby/object:Gem::Dependency
131
- name: ci_reporter_rspec
131
+ name: dotenv
132
132
  requirement: !ruby/object:Gem::Requirement
133
133
  requirements:
134
134
  - - "~>"
135
135
  - !ruby/object:Gem::Version
136
- version: '1.0'
136
+ version: '2.7'
137
137
  type: :development
138
138
  prerelease: false
139
139
  version_requirements: !ruby/object:Gem::Requirement
140
140
  requirements:
141
141
  - - "~>"
142
142
  - !ruby/object:Gem::Version
143
- version: '1.0'
143
+ version: '2.7'
144
144
  - !ruby/object:Gem::Dependency
145
- name: colorize
145
+ name: github-markup
146
146
  requirement: !ruby/object:Gem::Requirement
147
147
  requirements:
148
148
  - - "~>"
149
149
  - !ruby/object:Gem::Version
150
- version: '0.8'
150
+ version: '5.0'
151
151
  type: :development
152
152
  prerelease: false
153
153
  version_requirements: !ruby/object:Gem::Requirement
154
154
  requirements:
155
155
  - - "~>"
156
156
  - !ruby/object:Gem::Version
157
- version: '0.8'
157
+ version: '5.0'
158
158
  - !ruby/object:Gem::Dependency
159
- name: dotenv
159
+ name: rake
160
160
  requirement: !ruby/object:Gem::Requirement
161
161
  requirements:
162
162
  - - "~>"
163
163
  - !ruby/object:Gem::Version
164
- version: '2.7'
164
+ version: '13.0'
165
165
  type: :development
166
166
  prerelease: false
167
167
  version_requirements: !ruby/object:Gem::Requirement
168
168
  requirements:
169
169
  - - "~>"
170
170
  - !ruby/object:Gem::Version
171
- version: '2.7'
171
+ version: '13.0'
172
172
  - !ruby/object:Gem::Dependency
173
- name: rake
173
+ name: redcarpet
174
174
  requirement: !ruby/object:Gem::Requirement
175
175
  requirements:
176
176
  - - "~>"
177
177
  - !ruby/object:Gem::Version
178
- version: '13.0'
178
+ version: '3.6'
179
179
  type: :development
180
180
  prerelease: false
181
181
  version_requirements: !ruby/object:Gem::Requirement
182
182
  requirements:
183
183
  - - "~>"
184
184
  - !ruby/object:Gem::Version
185
- version: '13.0'
185
+ version: '3.6'
186
+ - !ruby/object:Gem::Dependency
187
+ name: rspec
188
+ requirement: !ruby/object:Gem::Requirement
189
+ requirements:
190
+ - - "~>"
191
+ - !ruby/object:Gem::Version
192
+ version: '3.13'
193
+ type: :development
194
+ prerelease: false
195
+ version_requirements: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - "~>"
198
+ - !ruby/object:Gem::Version
199
+ version: '3.13'
186
200
  - !ruby/object:Gem::Dependency
187
201
  name: simplecov
188
202
  requirement: !ruby/object:Gem::Requirement
@@ -211,6 +225,20 @@ dependencies:
211
225
  - - "~>"
212
226
  - !ruby/object:Gem::Version
213
227
  version: '2.1'
228
+ - !ruby/object:Gem::Dependency
229
+ name: standard
230
+ requirement: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - "~>"
233
+ - !ruby/object:Gem::Version
234
+ version: '1.31'
235
+ type: :development
236
+ prerelease: false
237
+ version_requirements: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - "~>"
240
+ - !ruby/object:Gem::Version
241
+ version: '1.31'
214
242
  - !ruby/object:Gem::Dependency
215
243
  name: webmock
216
244
  requirement: !ruby/object:Gem::Requirement
@@ -240,7 +268,7 @@ dependencies:
240
268
  - !ruby/object:Gem::Version
241
269
  version: 0.9.37
242
270
  executables:
243
- - daylight-index-listen
271
+ - index-on-publish
244
272
  - simulate-publish-event
245
273
  extensions: []
246
274
  extra_rdoc_files: []
@@ -252,12 +280,10 @@ files:
252
280
  - LICENSE
253
281
  - README.md
254
282
  - Rakefile
255
- - bin/daylight-index-listen
283
+ - bin/index-on-publish
256
284
  - bin/simulate-publish-event
257
285
  - lib/surfliner/metadata_consumer.rb
258
286
  - lib/surfliner/metadata_consumer/consumer.rb
259
- - lib/surfliner/metadata_consumer/mq_config.rb
260
- - lib/surfliner/metadata_consumer/mq_connection.rb
261
287
  - lib/surfliner/metadata_consumer/payload.rb
262
288
  - lib/surfliner/metadata_consumer/solr.rb
263
289
  - lib/surfliner/metadata_consumer/solr/delete_handler.rb
@@ -265,6 +291,14 @@ files:
265
291
  - lib/surfliner/metadata_consumer/solr/message_handler.rb
266
292
  - lib/surfliner/metadata_consumer/superskunk_client.rb
267
293
  - lib/surfliner/metadata_consumer/version.rb
294
+ - lib/surfliner/mq.rb
295
+ - lib/surfliner/mq/connection.rb
296
+ - lib/surfliner/mq/connection_config.rb
297
+ - lib/surfliner/mq/queue_config.rb
298
+ - lib/surfliner/mq/topic.rb
299
+ - lib/surfliner/mq/topic_config.rb
300
+ - lib/surfliner/util.rb
301
+ - lib/surfliner/util/assert.rb
268
302
  homepage: https://gitlab.com/surfliner/metadata_consumer
269
303
  licenses:
270
304
  - MIT
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require "bundler/setup"
3
- require "logger"
4
- require "opentelemetry/sdk"
5
-
6
- require "surfliner/metadata_consumer"
7
-
8
- unless ENV["OTEL_SDK_DISABLED"] == "true"
9
- OpenTelemetry::SDK.configure do |c|
10
- c.service_name = "surfliner-daylight-consumer"
11
- c.use_all # enables auto instrumentation for Bunny, Net::HTTP, etc...
12
- end
13
- end
14
- tracer = OpenTelemetry.tracer_provider.tracer("DaylightConsumerTracer")
15
-
16
- $stdout.sync = true # don't buffer log output
17
- logger = Logger.new($stdout).tap do |logger|
18
- logger.level = ENV.fetch("LOG_LEVEL", Logger::INFO)
19
- end
20
-
21
- handler = Surfliner::MetadataConsumer::Solr::MessageHandler
22
- consumer = Surfliner::MetadataConsumer::Consumer.new(tracer:, logger:, handler:)
23
- consumer.run
@@ -1,79 +0,0 @@
1
- module Surfliner
2
- module MetadataConsumer
3
- # An object encapsulating RabbitMQ configuration.
4
- class MqConfig
5
- # @return [String] The RabbitMQ hostname
6
- attr_reader :host
7
-
8
- # @return [String] The RabbitMQ AMQP port
9
- attr_reader :port
10
-
11
- # @return [String] The RabbitMQ username
12
- attr_reader :username
13
-
14
- # @return [String] The RabbitMQ passsword
15
- attr_reader :password
16
-
17
- # @return [String] The topic exchange to listen to
18
- attr_reader :topic
19
-
20
- # @return [String] The name of the queue to listen to
21
- attr_reader :queue_name
22
-
23
- # @return [String] The platform routing key to listen to
24
- attr_reader :routing_key
25
-
26
- # Initializes a new `MqConfig` object.
27
- # @param host [The] RabbitMQ hostname
28
- # @param port [The] RabbitMQ AMQP port
29
- # @param username [The] RabbitMQ username
30
- # @param password [The] RabbitMQ passsword
31
- # @param topic [The] topic exchange to listen to
32
- # @param queue_name [The] name of the queue to listen to
33
- # @param routing_key [The] platform routing key to listen to
34
- def initialize(host:, port:, username:, password:, topic:, queue_name:, routing_key:)
35
- @host = host
36
- @port = port
37
- @username = username
38
- @password = password
39
- @topic = topic
40
- @queue_name = queue_name
41
- @routing_key = routing_key
42
- end
43
-
44
- class << self
45
- # Reads RabbitMQ configuration from environment variables and
46
- # returns it as a new `MqConfig` object.
47
- #
48
- # - `RABBITMQ_HOST` → `host`
49
- # - `RABBITMQ_NODE_PORT_NUMBER` → `port`
50
- # - `RABBITMQ_USERNAME` → `username`
51
- # - `RABBITMQ_PASSWORD` → `password`
52
- # - `RABBITMQ_TOPIC` → `topic`
53
- # - `RABBITMQ_QUEUE` → `queue_name`
54
- # - `RABBITMQ_PLATFORM_ROUTING_KEY` → `routing_key`
55
- def from_env
56
- MqConfig.new(
57
- host: ENV.fetch("RABBITMQ_HOST"),
58
- port: ENV.fetch("RABBITMQ_NODE_PORT_NUMBER"),
59
- username: ENV.fetch("RABBITMQ_USERNAME"),
60
- password: ENV.fetch("RABBITMQ_PASSWORD"),
61
- topic: ENV.fetch("RABBITMQ_TOPIC"),
62
- queue_name: ENV.fetch("RABBITMQ_QUEUE"),
63
- routing_key: ENV.fetch("RABBITMQ_PLATFORM_ROUTING_KEY")
64
- )
65
- end
66
- end
67
-
68
- # @return [String] the connection URL as a string
69
- def connection_url
70
- @connection_url ||= "amqp://#{username}:#{password}@#{host}:#{port}"
71
- end
72
-
73
- # @return [String] the connection URL as a string, without the password
74
- def redacted_url
75
- @redacted_url ||= connection_url.sub(password, "REDACTED")
76
- end
77
- end
78
- end
79
- end
@@ -1,136 +0,0 @@
1
- require "bunny"
2
-
3
- module Surfliner
4
- module MetadataConsumer
5
- # An object encapsulating a RabbitMQ connection.
6
- class MqConnection
7
- # @return [Logger] The logger
8
- attr_reader :logger
9
-
10
- # @return [Bunny::Session] The current RabbitMQ session
11
- attr_reader :connection
12
-
13
- # @return [Bunny::Channel] The channel being listened to
14
- attr_reader :channel
15
-
16
- # @return [Bunny::Exchange] The exchange being listened to
17
- attr_reader :exchange
18
-
19
- # @return [Bunny::Queue] The queue being listened to
20
- attr_reader :queue
21
-
22
- # @return [MqConfig] The configuration
23
- attr_reader :config
24
-
25
- # Initializes a new `MqConnection`.
26
- #
27
- # @param logger [Logger] the logger
28
- # @param config [MqConfig] the configuration
29
- def initialize(logger:, config: MqConfig.from_env)
30
- @logger = logger
31
- @config = config
32
- end
33
-
34
- # Opens a connection.
35
- # @param topic_opts [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
36
- # @param queue_opts [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
37
- # @return [self]
38
- # @raise RuntimeError if already connected
39
- def connect(topic_opts: {}, queue_opts: {})
40
- raise "RabbitMQ connection #{connection} already open." if open?
41
-
42
- logger.info("Rabbitmq message broker connection url: #{config.redacted_url}")
43
- @connection = Bunny.new(config.connection_url, logger: logger)
44
- connect_on(connection)
45
- @channel = connection.create_channel
46
- @exchange = channel.topic(config.topic, topic_opts)
47
- @queue = channel.queue(config.queue_name, queue_opts)
48
- queue.bind(exchange, routing_key: config.routing_key)
49
-
50
- self
51
- rescue Bunny::TCPConnectionFailed => err
52
- # TODO: realistically, this only happens in connection.start, where we're eating it
53
- logger.error("Connection to #{config.redacted_url} failed")
54
- raise err
55
- rescue Bunny::PossibleAuthenticationFailureError => err
56
- # TODO: realistically, this only happens in connection.start, where we're eating it
57
- logger.error("Failed to authenticate to #{config.redacted_url}")
58
- raise err
59
- end
60
-
61
- # Opens a connection, yields the queue, and closes the connection after
62
- # the provided block completes.
63
- # @param topic_opts [Hash] RabbitMQ topic options. (See Bunny::Channel#topic)
64
- # @param queue_opts [Hash] RabbitMQ queue options. (See Bunny::Channel#queue)
65
- # @yield [Bunny::Queue] the queue
66
- def open(topic_opts: {}, queue_opts: {})
67
- connect(topic_opts:, queue_opts:)
68
- yield queue
69
- ensure
70
- close
71
- end
72
-
73
- # Closes the connection.
74
- def close
75
- return unless channel
76
- return if channel.closed?
77
- logger.info("closing channel")
78
- channel.close
79
- ensure
80
- logger.info("closing connection")
81
- connection&.close
82
- end
83
-
84
- # @return [true, false] True if the connection is open, false otherwise
85
- def open?
86
- connection&.status == :open
87
- end
88
-
89
- # @return [Symbol, nil] The connection status, or nil if there is no connection
90
- def status
91
- connection&.status
92
- end
93
-
94
- # @return [String] The RabbitMQ hostname
95
- def host
96
- config.host
97
- end
98
-
99
- # @return [String] The RabbitMQ port
100
- def port
101
- config.port
102
- end
103
-
104
- # @return [String] The routing key
105
- def routing_key
106
- config.routing_key
107
- end
108
-
109
- # Publishes the specified payload
110
- # @param payload [String] the payload to publish
111
- # @return [Bunny::Exchange] see #exchange
112
- def publish(payload)
113
- logger.info "Publishing to #{routing_key} with payload: #{payload}"
114
- exchange.publish(payload, routing_key:)
115
- end
116
-
117
- private
118
-
119
- def connect_on(connection, timeout = 120)
120
- timer = 0
121
- logger.info "Trying to open queue connection with timeout=#{timeout}"
122
- while timer < timeout
123
- begin
124
- connection.start
125
- rescue
126
- # TODO: do we actually want to rescue from everything?
127
- end
128
- return connection if connection.status == :open
129
- sleep 1
130
- timer += 1
131
- end
132
- raise "Failed to connect to queue."
133
- end
134
- end
135
- end
136
- end