falqon 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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 = 0
7
- PATCH = 1
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.1
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: 2023-06-22 00:00:00.000000000 Z
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: '0'
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: '0'
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.0'
141
+ version: '3.1'
55
142
  required_rubygems_version: !ruby/object:Gem::Requirement
56
143
  requirements:
57
144
  - - ">="