falqon 0.0.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/Gemfile +40 -8
- data/README.md +107 -8
- data/bin/falqon +8 -0
- data/config/inflections.rb +3 -1
- data/lib/falqon/cli/base.rb +35 -0
- data/lib/falqon/cli/clear.rb +86 -0
- data/lib/falqon/cli/delete.rb +152 -0
- data/lib/falqon/cli/kill.rb +143 -0
- data/lib/falqon/cli/list.rb +26 -0
- data/lib/falqon/cli/refill.rb +36 -0
- data/lib/falqon/cli/revive.rb +36 -0
- data/lib/falqon/cli/schedule.rb +40 -0
- data/lib/falqon/cli/show.rb +189 -0
- data/lib/falqon/cli/stats.rb +44 -0
- data/lib/falqon/cli/status.rb +47 -0
- data/lib/falqon/cli/version.rb +14 -0
- data/lib/falqon/cli.rb +168 -0
- data/lib/falqon/concerns/hooks.rb +101 -0
- data/lib/falqon/configuration.rb +141 -0
- data/lib/falqon/connection_pool_snooper.rb +15 -0
- data/lib/falqon/data.rb +11 -0
- data/lib/falqon/error.rb +13 -0
- data/lib/falqon/identifier.rb +11 -0
- data/lib/falqon/message.rb +220 -0
- data/lib/falqon/middlewares/logger.rb +32 -0
- data/lib/falqon/queue.rb +626 -0
- data/lib/falqon/strategies/linear.rb +82 -0
- data/lib/falqon/strategies/none.rb +44 -0
- data/lib/falqon/strategy.rb +26 -0
- data/lib/falqon/sub_queue.rb +95 -0
- data/lib/falqon/sub_set.rb +91 -0
- data/lib/falqon/version.rb +7 -2
- data/lib/falqon.rb +63 -0
- data/lib/generators/falqon/install.rb +37 -0
- metadata +96 -9
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
module Strategies
|
7
|
+
##
|
8
|
+
# Retry strategy that does not retry
|
9
|
+
#
|
10
|
+
# When a message fails to process, it is immediately marked as dead and moved to the dead subqueue.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# queue = Falqon::Queue.new("my_queue", retry_strategy: :none)
|
14
|
+
# queue.push("Hello, World!")
|
15
|
+
# queue.pop { raise Falqon::Error }
|
16
|
+
# queue.inspect # => #<Falqon::Queue name="my_queue" pending=0 processing=0 scheduled=0 dead=1>
|
17
|
+
#
|
18
|
+
class None < Strategy
|
19
|
+
# @!visibility private
|
20
|
+
sig { params(message: Message, error: Error).void }
|
21
|
+
def retry(message, error)
|
22
|
+
queue.redis.with do |r|
|
23
|
+
# Set error metadata
|
24
|
+
r.hset(
|
25
|
+
"#{queue.id}:metadata:#{message.id}",
|
26
|
+
:retried_at, Time.now.to_i,
|
27
|
+
:retry_error, error.message,
|
28
|
+
)
|
29
|
+
|
30
|
+
r.multi do |t|
|
31
|
+
# Kill message immediately
|
32
|
+
message.kill
|
33
|
+
|
34
|
+
# Remove identifier from processing queue
|
35
|
+
queue.processing.remove(message.id)
|
36
|
+
|
37
|
+
# Set message status
|
38
|
+
t.hset("#{queue.id}:metadata:#{message.id}", :status, "dead")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
##
|
7
|
+
# Base class for retry strategies
|
8
|
+
# @!visibility private
|
9
|
+
#
|
10
|
+
class Strategy
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { returns(Queue) }
|
14
|
+
attr_reader :queue
|
15
|
+
|
16
|
+
sig { params(queue: Queue).void }
|
17
|
+
def initialize(queue)
|
18
|
+
@queue = queue
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { params(message: Message, error: Error).void }
|
22
|
+
def retry(message, error)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
##
|
7
|
+
# Simple queue abstraction on top of Redis
|
8
|
+
# @!visibility private
|
9
|
+
#
|
10
|
+
class SubQueue
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { returns(String) }
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
sig { returns(String) }
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
sig { returns(Queue) }
|
20
|
+
attr_reader :queue
|
21
|
+
|
22
|
+
sig { params(queue: Queue, type: T.nilable(String)).void }
|
23
|
+
def initialize(queue, type = nil)
|
24
|
+
@type = type || "pending"
|
25
|
+
@id = [queue.id, type].compact.join(":")
|
26
|
+
@queue = queue
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(message_id: Identifier, head: T.nilable(T::Boolean)).void }
|
30
|
+
def add(message_id, head: false)
|
31
|
+
queue.redis.with do |r|
|
32
|
+
if head
|
33
|
+
r.lpush(id, message_id)
|
34
|
+
else
|
35
|
+
r.rpush(id, message_id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { params(message_id: Identifier).void }
|
41
|
+
def remove(message_id)
|
42
|
+
queue.redis.with do |r|
|
43
|
+
r.lrem(id, 0, message_id)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(index: Integer).returns(T.nilable(Identifier)) }
|
48
|
+
def peek(index: 0)
|
49
|
+
queue.redis.with do |r|
|
50
|
+
r.lindex(id, index)&.to_i
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(start: Integer, stop: Integer).returns(T::Array[Identifier]) }
|
55
|
+
def range(start: 0, stop: -1)
|
56
|
+
queue.redis.with do |r|
|
57
|
+
r.lrange(id, start, stop).map(&:to_i)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { returns(T::Array[Identifier]) }
|
62
|
+
def clear
|
63
|
+
queue.redis.with do |r|
|
64
|
+
# Get all identifiers from queue
|
65
|
+
message_ids = r.lrange(id, 0, -1)
|
66
|
+
|
67
|
+
# Delete all data and clear queue
|
68
|
+
r.del(*message_ids.flat_map { |message_id| ["#{queue.id}:data:#{message_id}", "#{queue.id}:metadata:#{message_id}"] }, id, "#{queue.id}:id")
|
69
|
+
|
70
|
+
# Return identifiers
|
71
|
+
message_ids.map(&:to_i)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { returns(Integer) }
|
76
|
+
def size
|
77
|
+
queue.redis.with { |r| r.llen(id) }
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { returns(T::Boolean) }
|
81
|
+
def empty?
|
82
|
+
size.zero?
|
83
|
+
end
|
84
|
+
|
85
|
+
sig { returns(T::Array[Identifier]) }
|
86
|
+
def to_a
|
87
|
+
queue.redis.with { |r| r.lrange(id, 0, -1).map(&:to_i) }
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { returns(String) }
|
91
|
+
def inspect
|
92
|
+
"#<#{self.class} name=#{type} size=#{size}>"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
##
|
7
|
+
# Simple sorted set abstraction on top of Redis
|
8
|
+
# @!visibility private
|
9
|
+
#
|
10
|
+
class SubSet
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { returns(String) }
|
14
|
+
attr_reader :type
|
15
|
+
|
16
|
+
sig { returns(String) }
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
sig { returns(Queue) }
|
20
|
+
attr_reader :queue
|
21
|
+
|
22
|
+
sig { params(queue: Queue, type: T.nilable(String)).void }
|
23
|
+
def initialize(queue, type = nil)
|
24
|
+
@type = type || "pending"
|
25
|
+
@id = [queue.id, type].compact.join(":")
|
26
|
+
@queue = queue
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { params(message_id: Identifier, score: Integer).void }
|
30
|
+
def add(message_id, score)
|
31
|
+
queue.redis.with do |r|
|
32
|
+
r.zadd(id, score, message_id)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { params(message_id: Identifier).void }
|
37
|
+
def remove(message_id)
|
38
|
+
queue.redis.with do |r|
|
39
|
+
r.zrem(id, message_id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { params(index: Integer).returns(T.nilable(Identifier)) }
|
44
|
+
def peek(index: 0)
|
45
|
+
queue.redis.with do |r|
|
46
|
+
r.zrange(id, index, index).first&.to_i
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(start: Integer, stop: Integer).returns(T::Array[Identifier]) }
|
51
|
+
def range(start: 0, stop: -1)
|
52
|
+
queue.redis.with do |r|
|
53
|
+
r.zrange(id, start, stop).map(&:to_i)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
sig { returns(T::Array[Identifier]) }
|
58
|
+
def clear
|
59
|
+
queue.redis.with do |r|
|
60
|
+
# Get all identifiers from queue
|
61
|
+
message_ids = r.zrange(id, 0, -1)
|
62
|
+
|
63
|
+
# Delete all data and clear queue
|
64
|
+
r.del(*message_ids.flat_map { |message_id| ["#{queue.id}:data:#{message_id}", "#{queue.id}:metadata:#{message_id}"] }, id, "#{queue.id}:id")
|
65
|
+
|
66
|
+
# Return identifiers
|
67
|
+
message_ids.map(&:to_i)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { returns(Integer) }
|
72
|
+
def size
|
73
|
+
queue.redis.with { |r| r.zcount(id, "-inf", "+inf") }
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { returns(T::Boolean) }
|
77
|
+
def empty?
|
78
|
+
size.zero?
|
79
|
+
end
|
80
|
+
|
81
|
+
sig { returns(T::Array[Identifier]) }
|
82
|
+
def to_a
|
83
|
+
queue.redis.with { |r| r.zrange(id, 0, -1).map(&:to_i) }
|
84
|
+
end
|
85
|
+
|
86
|
+
sig { returns(String) }
|
87
|
+
def inspect
|
88
|
+
"#<#{self.class} name=#{type} size=#{size}>"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/falqon/version.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Falqon
|
4
|
+
# @!visibility private
|
4
5
|
module Version
|
5
6
|
MAJOR = 0
|
6
|
-
MINOR =
|
7
|
-
PATCH =
|
7
|
+
MINOR = 1
|
8
|
+
PATCH = 0
|
8
9
|
PRE = nil
|
9
10
|
|
10
11
|
VERSION = [MAJOR, MINOR, PATCH].compact.join(".")
|
@@ -12,5 +13,9 @@ module Falqon
|
|
12
13
|
STRING = [VERSION, PRE].compact.join("-")
|
13
14
|
end
|
14
15
|
|
16
|
+
# @!visibility private
|
15
17
|
VERSION = Version::STRING
|
18
|
+
|
19
|
+
# @!visibility private
|
20
|
+
PROTOCOL = 1
|
16
21
|
end
|
data/lib/falqon.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
require "forwardable"
|
6
|
+
|
7
|
+
require "sorbet-runtime"
|
8
|
+
require "zeitwerk"
|
9
|
+
|
10
|
+
module Falqon
|
11
|
+
class << self
|
12
|
+
extend Forwardable
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
# Code loader instance
|
16
|
+
# @!visibility private
|
17
|
+
attr_reader :loader
|
18
|
+
|
19
|
+
# Global configuration
|
20
|
+
#
|
21
|
+
# @see Falqon::Configuration
|
22
|
+
sig { returns(Configuration) }
|
23
|
+
def configuration
|
24
|
+
@configuration ||= Configuration.new
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def root
|
29
|
+
@root ||= Pathname.new(File.expand_path(File.join("..", ".."), __FILE__))
|
30
|
+
end
|
31
|
+
|
32
|
+
# @!visibility private
|
33
|
+
def setup
|
34
|
+
@loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
35
|
+
|
36
|
+
# Register inflections
|
37
|
+
require root.join("config/inflections.rb")
|
38
|
+
|
39
|
+
# Collapse concerns directory
|
40
|
+
loader.collapse(root.join("lib/falqon/concerns"))
|
41
|
+
|
42
|
+
# Configure Rails generators (if applicable)
|
43
|
+
if const_defined?(:Rails)
|
44
|
+
loader.collapse(root.join("lib/generators"))
|
45
|
+
else
|
46
|
+
loader.ignore(root.join("lib/generators"))
|
47
|
+
end
|
48
|
+
|
49
|
+
loader.setup
|
50
|
+
loader.eager_load
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!visibility private
|
54
|
+
def configure
|
55
|
+
yield configuration
|
56
|
+
end
|
57
|
+
|
58
|
+
def_delegator :configuration, :redis
|
59
|
+
def_delegator :configuration, :logger
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
Falqon.setup
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module Falqon
|
6
|
+
# @!visibility private
|
7
|
+
class Install < Rails::Generators::Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def create_initializer_file
|
11
|
+
create_file "config/initializers/falqon.rb", <<~RUBY
|
12
|
+
Falqon.configure do |config|
|
13
|
+
# Configure global queue name prefix
|
14
|
+
# config.prefix = ENV.fetch("FALQON_PREFIX", "falqon")
|
15
|
+
|
16
|
+
# Retry strategy (none or linear)
|
17
|
+
# config.retry_strategy = :linear
|
18
|
+
|
19
|
+
# Maximum number of retries before a message is discarded (-1 for infinite retries)
|
20
|
+
# config.max_retries = 3
|
21
|
+
|
22
|
+
# Retry delay (in seconds) for linear retry strategy (defaults to 0)
|
23
|
+
# config.retry_delay = 60
|
24
|
+
|
25
|
+
# Configure the Redis client options
|
26
|
+
# config.redis_options = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
|
27
|
+
|
28
|
+
# Or, configure the Redis client directly
|
29
|
+
# config.redis = ConnectionPool.new(size: 5, timeout: 5) { Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0")) }
|
30
|
+
|
31
|
+
# Configure logger
|
32
|
+
# config.logger = Logger.new(STDOUT)
|
33
|
+
end
|
34
|
+
RUBY
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,33 +1,90 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: falqon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Florian Dejonckheere
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-11-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: connection_pool
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sorbet-runtime
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.5'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: thor
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
13
69
|
- !ruby/object:Gem::Dependency
|
14
70
|
name: zeitwerk
|
15
71
|
requirement: !ruby/object:Gem::Requirement
|
16
72
|
requirements:
|
17
|
-
- - "
|
73
|
+
- - "~>"
|
18
74
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
75
|
+
version: '2.6'
|
20
76
|
type: :runtime
|
21
77
|
prerelease: false
|
22
78
|
version_requirements: !ruby/object:Gem::Requirement
|
23
79
|
requirements:
|
24
|
-
- - "
|
80
|
+
- - "~>"
|
25
81
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
27
|
-
description: Simple, efficient messaging queue for Ruby
|
82
|
+
version: '2.6'
|
83
|
+
description: Simple, efficient, and reliable messaging queue for Ruby
|
28
84
|
email:
|
29
85
|
- florian@floriandejonckheere.be
|
30
|
-
executables:
|
86
|
+
executables:
|
87
|
+
- falqon
|
31
88
|
extensions: []
|
32
89
|
extra_rdoc_files: []
|
33
90
|
files:
|
@@ -35,8 +92,38 @@ files:
|
|
35
92
|
- Gemfile
|
36
93
|
- LICENSE.md
|
37
94
|
- README.md
|
95
|
+
- bin/falqon
|
38
96
|
- config/inflections.rb
|
97
|
+
- lib/falqon.rb
|
98
|
+
- lib/falqon/cli.rb
|
99
|
+
- lib/falqon/cli/base.rb
|
100
|
+
- lib/falqon/cli/clear.rb
|
101
|
+
- lib/falqon/cli/delete.rb
|
102
|
+
- lib/falqon/cli/kill.rb
|
103
|
+
- lib/falqon/cli/list.rb
|
104
|
+
- lib/falqon/cli/refill.rb
|
105
|
+
- lib/falqon/cli/revive.rb
|
106
|
+
- lib/falqon/cli/schedule.rb
|
107
|
+
- lib/falqon/cli/show.rb
|
108
|
+
- lib/falqon/cli/stats.rb
|
109
|
+
- lib/falqon/cli/status.rb
|
110
|
+
- lib/falqon/cli/version.rb
|
111
|
+
- lib/falqon/concerns/hooks.rb
|
112
|
+
- lib/falqon/configuration.rb
|
113
|
+
- lib/falqon/connection_pool_snooper.rb
|
114
|
+
- lib/falqon/data.rb
|
115
|
+
- lib/falqon/error.rb
|
116
|
+
- lib/falqon/identifier.rb
|
117
|
+
- lib/falqon/message.rb
|
118
|
+
- lib/falqon/middlewares/logger.rb
|
119
|
+
- lib/falqon/queue.rb
|
120
|
+
- lib/falqon/strategies/linear.rb
|
121
|
+
- lib/falqon/strategies/none.rb
|
122
|
+
- lib/falqon/strategy.rb
|
123
|
+
- lib/falqon/sub_queue.rb
|
124
|
+
- lib/falqon/sub_set.rb
|
39
125
|
- lib/falqon/version.rb
|
126
|
+
- lib/generators/falqon/install.rb
|
40
127
|
homepage: https://github.com/floriandejonckheere/falqon
|
41
128
|
licenses:
|
42
129
|
- LGPL-3.0
|
@@ -51,7 +138,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
138
|
requirements:
|
52
139
|
- - ">="
|
53
140
|
- !ruby/object:Gem::Version
|
54
|
-
version: '3.
|
141
|
+
version: '3.1'
|
55
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
143
|
requirements:
|
57
144
|
- - ">="
|