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.
@@ -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
  - - ">="