eventq 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +336 -0
  3. data/bin/console +14 -0
  4. data/bin/setup +8 -0
  5. data/lib/eventq/aws.rb +38 -0
  6. data/lib/eventq/eventq_aws/README.md +53 -0
  7. data/lib/eventq/eventq_aws/aws_eventq_client.rb +120 -0
  8. data/lib/eventq/eventq_aws/aws_queue_client.rb +64 -0
  9. data/lib/eventq/eventq_aws/aws_queue_manager.rb +68 -0
  10. data/lib/eventq/eventq_aws/aws_queue_worker.rb +168 -0
  11. data/lib/eventq/eventq_aws/aws_status_checker.rb +25 -0
  12. data/lib/eventq/eventq_aws/aws_subscription_manager.rb +65 -0
  13. data/lib/eventq/eventq_aws/jruby/aws_queue_worker.rb +370 -0
  14. data/lib/eventq/eventq_aws/sns.rb +64 -0
  15. data/lib/eventq/eventq_aws/sqs.rb +112 -0
  16. data/lib/eventq/eventq_base/configuration.rb +33 -0
  17. data/lib/eventq/eventq_base/event_raised_exchange.rb +7 -0
  18. data/lib/eventq/eventq_base/event_raised_queue.rb +7 -0
  19. data/lib/eventq/eventq_base/eventq_client_contract.rb +9 -0
  20. data/lib/eventq/eventq_base/eventq_logger.rb +28 -0
  21. data/lib/eventq/eventq_base/exceptions/invalid_signature_exception.rb +9 -0
  22. data/lib/eventq/eventq_base/exceptions/worker_thread_error.rb +10 -0
  23. data/lib/eventq/eventq_base/exceptions.rb +2 -0
  24. data/lib/eventq/eventq_base/exchange.rb +5 -0
  25. data/lib/eventq/eventq_base/message_args.rb +23 -0
  26. data/lib/eventq/eventq_base/nonce_manager.rb +57 -0
  27. data/lib/eventq/eventq_base/queue.rb +27 -0
  28. data/lib/eventq/eventq_base/queue_message.rb +31 -0
  29. data/lib/eventq/eventq_base/queue_worker_contract.rb +23 -0
  30. data/lib/eventq/eventq_base/serialization_providers/binary_serialization_provider.rb +15 -0
  31. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/array_writer.rb +20 -0
  32. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/attribute_writer.rb +24 -0
  33. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/class_writer.rb +20 -0
  34. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/date_time_writer.rb +33 -0
  35. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/date_writer.rb +22 -0
  36. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/hash_writer.rb +18 -0
  37. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/rational_writer.rb +20 -0
  38. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/serializer.rb +17 -0
  39. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/time_writer.rb +18 -0
  40. data/lib/eventq/eventq_base/serialization_providers/jruby/oj/value_writer.rb +16 -0
  41. data/lib/eventq/eventq_base/serialization_providers/jruby/oj.rb +10 -0
  42. data/lib/eventq/eventq_base/serialization_providers/jruby/oj_serialization_provider.rb +25 -0
  43. data/lib/eventq/eventq_base/serialization_providers/jruby.rb +2 -0
  44. data/lib/eventq/eventq_base/serialization_providers/json_serialization_provider.rb +28 -0
  45. data/lib/eventq/eventq_base/serialization_providers/oj_serialization_provider.rb +24 -0
  46. data/lib/eventq/eventq_base/serialization_providers.rb +36 -0
  47. data/lib/eventq/eventq_base/signature_providers/sha256_signature_provider.rb +31 -0
  48. data/lib/eventq/eventq_base/signature_providers.rb +44 -0
  49. data/lib/eventq/eventq_base/subscription_manager_contract.rb +13 -0
  50. data/lib/eventq/eventq_base/version.rb +3 -0
  51. data/lib/eventq/eventq_base/worker_id.rb +20 -0
  52. data/lib/eventq/eventq_rabbitmq/README.md +36 -0
  53. data/lib/eventq/eventq_rabbitmq/default_queue.rb +12 -0
  54. data/lib/eventq/eventq_rabbitmq/jruby/rabbitmq_queue_worker.rb +367 -0
  55. data/lib/eventq/eventq_rabbitmq/rabbitmq_eventq_client.rb +140 -0
  56. data/lib/eventq/eventq_rabbitmq/rabbitmq_queue_client.rb +54 -0
  57. data/lib/eventq/eventq_rabbitmq/rabbitmq_queue_manager.rb +104 -0
  58. data/lib/eventq/eventq_rabbitmq/rabbitmq_queue_worker.rb +168 -0
  59. data/lib/eventq/eventq_rabbitmq/rabbitmq_status_checker.rb +62 -0
  60. data/lib/eventq/eventq_rabbitmq/rabbitmq_subscription_manager.rb +54 -0
  61. data/lib/eventq/queue_worker.rb +241 -0
  62. data/lib/eventq/rabbitmq.rb +49 -0
  63. data/lib/eventq/worker_status.rb +64 -0
  64. data/lib/eventq.rb +25 -0
  65. metadata +289 -0
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module EventQ
6
+ module Amazon
7
+ # Helper SNS class to handle the API calls
8
+ class SNS
9
+ @@topic_arns = Concurrent::Hash.new
10
+
11
+ attr_reader :sns
12
+
13
+ def initialize(client)
14
+ @sns = client
15
+ end
16
+
17
+ # Create a TopicArn. if one already exists, it will return a pre-existing ARN from the cache.
18
+ # Even in the event of multiple threads trying to create one with AWS, AWS is idempotent and won't create
19
+ # duplicates
20
+ def create_topic_arn(event_type)
21
+ _event_type = EventQ.create_event_type(event_type)
22
+
23
+ arn = get_topic_arn(event_type)
24
+ unless arn
25
+ response = sns.create_topic(name: aws_safe_name(_event_type))
26
+ arn = response.topic_arn
27
+ @@topic_arns[_event_type] = arn
28
+ end
29
+
30
+ arn
31
+ end
32
+
33
+ # Check if a TopicArn exists. This will check with AWS if necessary and cache the results if one is found
34
+ # @return TopicArn [String]
35
+ def get_topic_arn(event_type)
36
+ _event_type = EventQ.create_event_type(event_type)
37
+
38
+ arn = @@topic_arns[_event_type]
39
+ unless arn
40
+ response = sns.list_topics
41
+ arn = response.topics.detect { |topic| topic.topic_arn.end_with?(_event_type) }&.topic_arn
42
+
43
+ @@topic_arns[_event_type] = arn if arn
44
+ end
45
+
46
+ arn
47
+ end
48
+
49
+ def drop_topic(event_type)
50
+ topic_arn = get_topic_arn(event_type)
51
+ sns.delete_topic(topic_arn: topic_arn)
52
+
53
+ _event_type = EventQ.create_event_type(event_type)
54
+ @@topic_arns.delete(_event_type)
55
+
56
+ true
57
+ end
58
+
59
+ def aws_safe_name(name)
60
+ return name[0..79].gsub(/[^a-zA-Z\d_\-]/,'')
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventQ
4
+ module Amazon
5
+ # Helper SQS class to handle the API calls
6
+ class SQS
7
+ @@queue_arns = Concurrent::Hash.new
8
+ @@queue_urls = Concurrent::Hash.new
9
+
10
+ attr_reader :sqs
11
+
12
+ def initialize(client)
13
+ @sqs = client
14
+ end
15
+
16
+ # Create a new queue.
17
+ def create_queue(queue, attributes = {})
18
+ _queue_name = EventQ.create_queue_name(queue.name)
19
+
20
+ url = get_queue_url(queue)
21
+ unless url
22
+ response = sqs.create_queue(
23
+ {
24
+ queue_name: aws_safe_name(_queue_name),
25
+ attributes: attributes
26
+ }
27
+ )
28
+ url = response.queue_url
29
+ @@queue_urls[_queue_name] = url
30
+ end
31
+
32
+ url
33
+ end
34
+
35
+ # Update a queue
36
+ def update_queue(queue, attributes = {})
37
+ url = get_queue_url(queue)
38
+ sqs.set_queue_attributes(
39
+ {
40
+ queue_url: url, # required
41
+ attributes: attributes
42
+ }
43
+ )
44
+
45
+ url
46
+ end
47
+
48
+ # Returns the ARN of a queue. If none exists, nil will be returned.
49
+ #
50
+ # @param queue [EventQ::Queue]
51
+ # @return ARN [String]
52
+ def get_queue_arn(queue)
53
+ _queue_name = EventQ.create_queue_name(queue.name)
54
+
55
+ arn = @@queue_arns[_queue_name]
56
+ unless arn
57
+ url = get_queue_url(queue)
58
+ if url
59
+ response = sqs.get_queue_attributes(
60
+ {
61
+ queue_url: url,
62
+ attribute_names: ['QueueArn']
63
+ }
64
+ )
65
+ arn = response.attributes['QueueArn']
66
+ end
67
+ end
68
+
69
+ arn
70
+ end
71
+
72
+ # Returns the URL of the queue. If none exists, nil will be returned.
73
+ #
74
+ # @param queue [EventQ::Queue]
75
+ # @return URL [String]
76
+ def get_queue_url(queue)
77
+ _queue_name = EventQ.create_queue_name(queue.name)
78
+
79
+ url = @@queue_urls[_queue_name]
80
+ unless url
81
+ begin
82
+ response= sqs.get_queue_url(
83
+ queue_name: aws_safe_name(_queue_name)
84
+ )
85
+ url = response.queue_url
86
+ rescue Aws::SQS::Errors::NonExistentQueue
87
+ # Only want to return nil for this method when not found.
88
+ end
89
+
90
+ @@queue_urls[_queue_name] = url if url
91
+ end
92
+
93
+ url
94
+ end
95
+
96
+ def drop_queue(queue)
97
+ q = get_queue_url(queue)
98
+ sqs.delete_queue(queue_url: q)
99
+
100
+ _queue_name = EventQ.create_queue_name(queue.name)
101
+ @@queue_urls.delete(_queue_name)
102
+ @@queue_arns.delete(_queue_name)
103
+
104
+ true
105
+ end
106
+
107
+ def aws_safe_name(name)
108
+ return name[0..79].gsub(/[^a-zA-Z\d_\-]/,'')
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,33 @@
1
+ module EventQ
2
+ class Configuration
3
+
4
+ def self.serialization_provider=(value)
5
+ @serialization_provider = value
6
+ end
7
+
8
+ def self.serialization_provider
9
+ if RUBY_PLATFORM =~ /java/
10
+ @serialization_provider ||= EventQ::SerializationProviders::JSON_PROVIDER
11
+ else
12
+ @serialization_provider ||= EventQ::SerializationProviders::OJ_PROVIDER
13
+ end
14
+ end
15
+
16
+ def self.signature_provider=(value)
17
+ @signature_provider = value
18
+ end
19
+
20
+ def self.signature_provider
21
+ @signature_provider ||= EventQ::SignatureProviders::SHA256
22
+ end
23
+
24
+ def self.signature_secret=(value)
25
+ @signature_secret = value
26
+ end
27
+
28
+ def self.signature_secret
29
+ @signature_secret
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ module EventQ
2
+ class EventRaisedExchange < Exchange
3
+ def initialize
4
+ @name = 'eventq.eventraised.ex'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module EventQ
2
+ class EventRaisedQueue < Queue
3
+ def initialize
4
+ @name = 'eventq.eventraised'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module EventQ
2
+ class EventQClientContract
3
+
4
+ def raise_event(event_type, event)
5
+
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ require 'logger'
2
+
3
+ module EventQ
4
+
5
+ def self.logger
6
+ return @@logger
7
+ end
8
+
9
+ def self.set_logger(logger)
10
+ @@logger = logger
11
+ end
12
+
13
+ def self.log(type, message)
14
+ case type
15
+ when :info
16
+ logger.info(message)
17
+ when :debug
18
+ logger.debug(message)
19
+ when :error
20
+ logger.error(message)
21
+ end
22
+ rescue
23
+ #do nothing
24
+ end
25
+
26
+ EventQ.set_logger(Logger.new(STDOUT))
27
+
28
+ end
@@ -0,0 +1,9 @@
1
+ module EventQ
2
+ module Exceptions
3
+ class InvalidSignatureException < StandardError
4
+ def initialize(message = "Invalid message signature.")
5
+ super(message)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module EventQ
2
+ module Exceptions
3
+ # General thread error that signifies a thread about to shutdown.
4
+ class WorkerThreadError < StandardError
5
+ def initialize(message = 'Worker thread error')
6
+ super(message)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'exceptions/invalid_signature_exception'
2
+ require_relative 'exceptions/worker_thread_error'
@@ -0,0 +1,5 @@
1
+ module EventQ
2
+ class Exchange
3
+ attr_accessor :name
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ module EventQ
2
+ class MessageArgs
3
+ attr_reader :type
4
+ attr_reader :content_type
5
+ attr_reader :retry_attempts
6
+ attr_accessor :abort
7
+ attr_accessor :drop
8
+ attr_reader :context
9
+ attr_reader :id
10
+ attr_reader :sent
11
+
12
+ def initialize(type:, retry_attempts:, context: {}, content_type:, id: nil, sent: nil)
13
+ @type = type
14
+ @retry_attempts = retry_attempts
15
+ @abort = false
16
+ @drop = false
17
+ @context = context
18
+ @content_type = content_type
19
+ @id = id
20
+ @sent = sent
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ module EventQ
2
+ class NonceManager
3
+
4
+ def self.configure(server:,timeout:10000,lifespan:3600000)
5
+ @server_url = server
6
+ @timeout = timeout
7
+ @lifespan = lifespan
8
+ end
9
+
10
+ def self.server_url
11
+ @server_url
12
+ end
13
+
14
+ def self.timeout
15
+ @timeout
16
+ end
17
+
18
+ def self.lifespan
19
+ @lifespan
20
+ end
21
+
22
+ def self.is_allowed?(nonce)
23
+ if @server_url == nil
24
+ return true
25
+ end
26
+
27
+ require 'redlock'
28
+ lock = Redlock::Client.new([ @server_url ]).lock(nonce, @timeout)
29
+ if lock == false
30
+ EventQ.log(:info, "[#{self.class}] - Message has already been processed: #{nonce}")
31
+ return false
32
+ end
33
+
34
+ return true
35
+ end
36
+
37
+ def self.complete(nonce)
38
+ if @server_url != nil
39
+ Redis.new(url: @server_url).expire(nonce, @lifespan)
40
+ end
41
+ return true
42
+ end
43
+
44
+ def self.failed(nonce)
45
+ if @server_url != nil
46
+ Redis.new(url: @server_url).del(nonce)
47
+ end
48
+ return true
49
+ end
50
+
51
+ def self.reset
52
+ @server_url = nil
53
+ @timeout = nil
54
+ @lifespan = nil
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ module EventQ
2
+ class Queue
3
+ attr_accessor :allow_retry
4
+ attr_accessor :allow_retry_back_off
5
+ attr_accessor :dlq
6
+ attr_accessor :max_retry_attempts
7
+ attr_accessor :max_retry_delay
8
+ attr_accessor :name
9
+ attr_accessor :max_receive_count
10
+ attr_accessor :require_signature
11
+ attr_accessor :retry_delay
12
+
13
+ def initialize
14
+ @allow_retry = false
15
+ # Default retry back off settings
16
+ @allow_retry_back_off = false
17
+ # Default max receive count is 30
18
+ @max_receive_count = 30
19
+ # Default max retry attempts is 5
20
+ @max_retry_attempts = 5
21
+ # Default require signature to false
22
+ @require_signature = false
23
+ # Default retry delay is 30 seconds
24
+ @retry_delay = 30000
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ module EventQ
2
+ class QueueMessage
3
+ extend ClassKit
4
+
5
+ attr_accessor_type :id, type: String
6
+ attr_accessor_type :retry_attempts, type: Integer
7
+ attr_accessor_type :type, type: String
8
+ attr_accessor_type :content
9
+ attr_accessor_type :content_type, type: String
10
+ attr_accessor_type :created, type: Float
11
+ attr_accessor_type :signature, type: String
12
+ attr_accessor_type :context, type: Hash
13
+
14
+ def initialize
15
+ @retry_attempts = 0
16
+ @created = Time.now.to_f
17
+ @id = SecureRandom.uuid
18
+ @context = {}
19
+ end
20
+
21
+ # Creates a signature for the message
22
+ #
23
+ # @param provider [EventQ::SignatureProviders::Sha256SignatureProvider] Signature provider that implements
24
+ # a write method
25
+ def sign(provider)
26
+ return unless EventQ::Configuration.signature_secret
27
+
28
+ self.signature = provider.write(message: self, secret: EventQ::Configuration.signature_secret)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ module EventQ
2
+ # Contract class for queue workers
3
+ class QueueWorkerContract
4
+
5
+ def start(queue, options = {}, &block)
6
+
7
+ end
8
+
9
+ def stop
10
+
11
+ end
12
+
13
+ def on_retry_exceeded(&block)
14
+
15
+ end
16
+
17
+ def running?
18
+
19
+ end
20
+
21
+ end
22
+ end
23
+
@@ -0,0 +1,15 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ class BinarySerializationProvider
4
+
5
+ def serialize(object)
6
+ Marshal::dump(object)
7
+ end
8
+
9
+ def deserialize(msg)
10
+ Marshal::load(msg)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class ArrayWriter < AttributeWriter
6
+ def valid?(obj)
7
+ obj.is_a?(Array)
8
+ end
9
+ def exec(obj)
10
+ array = []
11
+ obj.each do |a|
12
+ array << AttributeWriter.exec(a)
13
+ end
14
+ array
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class AttributeWriter
6
+
7
+ def self.exec(obj)
8
+ aw = descendants.detect { |a| a.new.valid?(obj) } || ClassWriter
9
+ aw.new.exec(obj)
10
+ end
11
+
12
+ def self.descendants
13
+ descendants = []
14
+ ObjectSpace.each_object(singleton_class) do |k|
15
+ next if k.singleton_class?
16
+ descendants.unshift k unless k == self
17
+ end
18
+ descendants
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class ClassWriter < AttributeWriter
6
+ def valid?(obj)
7
+ false
8
+ end
9
+ def exec(obj)
10
+ hash = { '^o': obj.class }
11
+ obj.instance_variables.each do |key|
12
+ hash[key[1..-1]] = AttributeWriter.exec(obj.instance_variable_get(key))
13
+ end
14
+ hash
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class DateTimeWriter < AttributeWriter
6
+ def valid?(obj)
7
+ obj.is_a?(DateTime)
8
+ end
9
+ def exec(obj)
10
+ seconds = obj.strftime('%S%N')
11
+ d = 1_000_000_000
12
+ if seconds.start_with?('0')
13
+ seconds[0] = ''
14
+ d = 100_000_000
15
+ end
16
+
17
+ {
18
+ '^O': 'DateTime',
19
+ year: obj.year,
20
+ month: obj.month,
21
+ day: obj.day,
22
+ hour: obj.hour,
23
+ min: obj.min,
24
+ sec: RationalWriter.new.exec(Rational(Integer(seconds), d)),
25
+ offset: RationalWriter.new.exec(obj.offset),
26
+ start: obj.start
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class DateWriter < AttributeWriter
6
+ def valid?(obj)
7
+ obj.is_a?(Date) && !obj.is_a?(DateTime)
8
+ end
9
+ def exec(obj)
10
+ {
11
+ '^O': 'Date',
12
+ year: obj.year,
13
+ month: obj.month,
14
+ day: obj.day,
15
+ start: obj.start
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class HashWriter < AttributeWriter
6
+ def valid?(obj)
7
+ obj.is_a?(Hash)
8
+ end
9
+ def exec(obj)
10
+ obj.each do |key, value|
11
+ obj[key] = AttributeWriter.exec(value)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class RationalWriter < AttributeWriter
6
+ def valid?(obj)
7
+ obj.is_a?(Rational)
8
+ end
9
+ def exec(obj)
10
+ {
11
+ '^O': 'Rational',
12
+ numerator: obj.numerator,
13
+ denominator: obj.denominator
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class Serializer
6
+ def dump(obj)
7
+ JSON.dump(AttributeWriter.exec(obj))
8
+ end
9
+
10
+ def load(json)
11
+ raise NotImplementedError.new("[#{self.class}] - #load method has not yet been implemented.")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module EventQ
2
+ module SerializationProviders
3
+ module JRuby
4
+ module Oj
5
+ class TimeWriter < AttributeWriter
6
+ def valid?(obj)
7
+ obj.is_a?(Time)
8
+ end
9
+ def exec(obj)
10
+ {
11
+ '^t': obj.to_f
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end