fila-client 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a874620e781ea2213b76a152f9297b5e1378963b5a754e3f0275f5b211f9e98
4
- data.tar.gz: 58fa181e4c11ab88e3a6f58fe6c98424cc38e5ca2d91e42b9466d65a34f1aee8
3
+ metadata.gz: 2d12705da00a1094120e5a6c8ecd88b6193b73a88f18d5c6786211bc8458fb96
4
+ data.tar.gz: 3f1adf19ca765745a2a14bc99bd6e1239dbcc1ef5940f7cb4203c80c3a3cacc9
5
5
  SHA512:
6
- metadata.gz: 68a26f1a326b5e1fa02753009aa51afb334b96b1b8bb0f6266cd9d6652d2906f3b9f0e87d7c0ea0d267c38af706d79aaee6a67aca9282f6fce1429ac4e532a2b
7
- data.tar.gz: 34e4a41170586bba7e483cf119bb6a366f86ccf05a0847baa254ab8f0a69b61658d4a86d0a1f2862b120413db95fa6c6f34814167fc74f3c4514591b44c36f80
6
+ metadata.gz: 7e6db66c6b4a7dace398aff5659ccfa750e0d51e82ef2b34dff3175bdcd0ebb91768dc87e3515e1caa52c2aa24b6f94e81ac7078ef87886bd0701e0b8944c1d4
7
+ data.tar.gz: 7524edf3d910461afcf3844bb7ae7d7a780055b9700cecc2dec92ddab84e2aa5349d0b41116359fdc5063f2a23ee729973d14ba227bd62d544f9d2576f0fde24
data/README.md CHANGED
@@ -44,12 +44,88 @@ end
44
44
  client.close
45
45
  ```
46
46
 
47
+ ### TLS (system trust store)
48
+
49
+ ```ruby
50
+ require "fila"
51
+
52
+ # TLS using the OS system trust store (e.g., server uses a public CA).
53
+ client = Fila::Client.new("localhost:5555", tls: true)
54
+ ```
55
+
56
+ ### TLS (custom CA)
57
+
58
+ ```ruby
59
+ require "fila"
60
+
61
+ # TLS with an explicit CA certificate (e.g., private/self-signed CA).
62
+ client = Fila::Client.new("localhost:5555",
63
+ ca_cert: File.read("ca.pem")
64
+ )
65
+ ```
66
+
67
+ ### mTLS (mutual TLS)
68
+
69
+ ```ruby
70
+ require "fila"
71
+
72
+ # Mutual TLS with system trust store.
73
+ client = Fila::Client.new("localhost:5555",
74
+ tls: true,
75
+ client_cert: File.read("client.pem"),
76
+ client_key: File.read("client-key.pem")
77
+ )
78
+
79
+ # Mutual TLS with explicit CA certificate.
80
+ client = Fila::Client.new("localhost:5555",
81
+ ca_cert: File.read("ca.pem"),
82
+ client_cert: File.read("client.pem"),
83
+ client_key: File.read("client-key.pem")
84
+ )
85
+ ```
86
+
87
+ ### API Key Authentication
88
+
89
+ ```ruby
90
+ require "fila"
91
+
92
+ # API key sent as Bearer token on every request.
93
+ client = Fila::Client.new("localhost:5555",
94
+ api_key: "fila_your_api_key_here"
95
+ )
96
+ ```
97
+
98
+ ### mTLS + API Key
99
+
100
+ ```ruby
101
+ require "fila"
102
+
103
+ # Full security: mTLS transport + API key authentication.
104
+ client = Fila::Client.new("localhost:5555",
105
+ ca_cert: File.read("ca.pem"),
106
+ client_cert: File.read("client.pem"),
107
+ client_key: File.read("client-key.pem"),
108
+ api_key: "fila_your_api_key_here"
109
+ )
110
+ ```
111
+
47
112
  ## API
48
113
 
49
- ### `Fila::Client.new(addr)`
114
+ ### `Fila::Client.new(addr, tls: false, ca_cert: nil, client_cert: nil, client_key: nil, api_key: nil)`
50
115
 
51
116
  Connect to a Fila broker at the given address (e.g., `"localhost:5555"`).
52
117
 
118
+ | Parameter | Type | Description |
119
+ |---|---|---|
120
+ | `addr` | `String` | Broker address in `"host:port"` format |
121
+ | `tls:` | `Boolean` | Enable TLS using the OS system trust store (default: `false`) |
122
+ | `ca_cert:` | `String` or `nil` | PEM-encoded CA certificate for TLS (implies `tls: true`) |
123
+ | `client_cert:` | `String` or `nil` | PEM-encoded client certificate for mTLS |
124
+ | `client_key:` | `String` or `nil` | PEM-encoded client private key for mTLS |
125
+ | `api_key:` | `String` or `nil` | API key for Bearer token authentication |
126
+
127
+ When no TLS/auth options are provided, the client connects over plaintext (backward compatible). When `tls: true` is set without `ca_cert:`, the OS system trust store is used for server certificate verification.
128
+
53
129
  ### `client.enqueue(queue:, headers:, payload:)`
54
130
 
55
131
  Enqueue a message. Returns the broker-assigned message ID (UUIDv7).
data/lib/fila/client.rb CHANGED
@@ -14,43 +14,37 @@ module Fila
14
14
  #
15
15
  # Wraps the hot-path gRPC operations: enqueue, consume, ack, nack.
16
16
  #
17
- # @example
17
+ # @example Plain-text (no auth)
18
18
  # client = Fila::Client.new("localhost:5555")
19
- # msg_id = client.enqueue(queue: "my-queue", headers: { "tenant" => "acme" }, payload: "hello")
20
- # client.consume(queue: "my-queue") do |msg|
21
- # client.ack(queue: "my-queue", msg_id: msg.id)
22
- # break
23
- # end
24
- # client.close
19
+ #
20
+ # @example TLS with system trust store
21
+ # client = Fila::Client.new("localhost:5555", tls: true)
22
+ #
23
+ # @example TLS with custom CA
24
+ # client = Fila::Client.new("localhost:5555", ca_cert: File.read("ca.pem"))
25
+ #
26
+ # @example mTLS + API key
27
+ # client = Fila::Client.new("localhost:5555",
28
+ # ca_cert: File.read("ca.pem"),
29
+ # client_cert: File.read("client.pem"),
30
+ # client_key: File.read("client-key.pem"),
31
+ # api_key: "fila_abc123")
25
32
  class Client
26
- # Connect to a Fila broker at the given address.
27
- #
28
- # @param addr [String] broker address in "host:port" format (e.g., "localhost:5555")
29
- def initialize(addr)
30
- @stub = ::Fila::V1::FilaService::Stub.new(addr, :this_channel_is_insecure)
33
+ def initialize(addr, tls: false, ca_cert: nil, client_cert: nil, client_key: nil, api_key: nil)
34
+ @api_key = api_key
35
+ @credentials = build_credentials(tls: tls, ca_cert: ca_cert, client_cert: client_cert, client_key: client_key)
36
+ @stub = ::Fila::V1::FilaService::Stub.new(addr, @credentials)
31
37
  end
32
38
 
33
- # Close the underlying gRPC channel.
34
- def close
35
- # grpc-ruby doesn't expose a direct channel close on stubs;
36
- # the channel is garbage-collected. This is a no-op for API symmetry.
37
- end
39
+ def close; end
38
40
 
39
- # Enqueue a message to the specified queue.
40
- #
41
- # @param queue [String] target queue name
42
- # @param headers [Hash<String, String>, nil] optional message headers
43
- # @param payload [String] message payload bytes
44
- # @return [String] broker-assigned message ID (UUIDv7)
45
- # @raise [QueueNotFoundError] if the queue does not exist
46
- # @raise [RPCError] for unexpected gRPC failures
47
41
  def enqueue(queue:, payload:, headers: nil)
48
42
  req = ::Fila::V1::EnqueueRequest.new(
49
43
  queue: queue,
50
44
  headers: headers || {},
51
45
  payload: payload
52
46
  )
53
- resp = @stub.enqueue(req)
47
+ resp = @stub.enqueue(req, metadata: call_metadata)
54
48
  resp.message_id
55
49
  rescue GRPC::NotFound => e
56
50
  raise QueueNotFoundError, "enqueue: #{e.details}"
@@ -58,36 +52,12 @@ module Fila
58
52
  raise RPCError.new(e.code, e.details)
59
53
  end
60
54
 
61
- # Open a streaming consumer on the specified queue.
62
- #
63
- # Yields messages as they become available. Nil message frames (keepalive
64
- # signals) are skipped automatically. Nacked messages are redelivered on
65
- # the same stream.
66
- #
67
- # If no block is given, returns an Enumerator.
68
- #
69
- # @param queue [String] queue to consume from
70
- # @yield [ConsumeMessage] each message received from the broker
71
- # @return [Enumerator<ConsumeMessage>] if no block given
72
- # @raise [QueueNotFoundError] if the queue does not exist
73
- # @raise [RPCError] for unexpected gRPC failures
55
+ # Open a streaming consumer. Yields messages as they arrive.
56
+ # Returns an Enumerator if no block given.
74
57
  def consume(queue:, &block)
75
58
  return enum_for(:consume, queue: queue) unless block
76
59
 
77
- req = ::Fila::V1::ConsumeRequest.new(queue: queue)
78
- stream = @stub.consume(req)
79
- stream.each do |resp|
80
- msg = resp.message
81
- next if msg.nil? || msg.id.empty?
82
-
83
- block.call(build_consume_message(msg))
84
- end
85
- rescue GRPC::Cancelled
86
- # Stream cancelled — normal when consumer breaks out of the loop.
87
- rescue GRPC::NotFound => e
88
- raise QueueNotFoundError, "consume: #{e.details}"
89
- rescue GRPC::BadStatus => e
90
- raise RPCError.new(e.code, e.details)
60
+ consume_with_redirect(queue: queue, redirected: false, &block)
91
61
  end
92
62
 
93
63
  # Acknowledge a successfully processed message.
@@ -98,7 +68,7 @@ module Fila
98
68
  # @raise [RPCError] for unexpected gRPC failures
99
69
  def ack(queue:, msg_id:)
100
70
  req = ::Fila::V1::AckRequest.new(queue: queue, message_id: msg_id)
101
- @stub.ack(req)
71
+ @stub.ack(req, metadata: call_metadata)
102
72
  nil
103
73
  rescue GRPC::NotFound => e
104
74
  raise MessageNotFoundError, "ack: #{e.details}"
@@ -115,7 +85,7 @@ module Fila
115
85
  # @raise [RPCError] for unexpected gRPC failures
116
86
  def nack(queue:, msg_id:, error:)
117
87
  req = ::Fila::V1::NackRequest.new(queue: queue, message_id: msg_id, error: error)
118
- @stub.nack(req)
88
+ @stub.nack(req, metadata: call_metadata)
119
89
  nil
120
90
  rescue GRPC::NotFound => e
121
91
  raise MessageNotFoundError, "nack: #{e.details}"
@@ -123,8 +93,65 @@ module Fila
123
93
  raise RPCError.new(e.code, e.details)
124
94
  end
125
95
 
96
+ LEADER_ADDR_KEY = 'x-fila-leader-addr'
97
+
98
+ private_constant :LEADER_ADDR_KEY
99
+
126
100
  private
127
101
 
102
+ def consume_with_redirect(queue:, redirected:, &block) # rubocop:disable Metrics/AbcSize
103
+ stream = @stub.consume(::Fila::V1::ConsumeRequest.new(queue: queue), metadata: call_metadata)
104
+ stream.each do |resp|
105
+ msg = resp.message
106
+ next if msg.nil? || msg.id.empty?
107
+
108
+ block.call(build_consume_message(msg))
109
+ end
110
+ rescue GRPC::Cancelled then nil
111
+ rescue GRPC::NotFound => e
112
+ raise QueueNotFoundError, "consume: #{e.details}"
113
+ rescue GRPC::Unavailable => e
114
+ raise RPCError.new(e.code, e.details) if (leader_addr = extract_leader_addr(e)).nil? || redirected
115
+
116
+ @stub = ::Fila::V1::FilaService::Stub.new(leader_addr, @credentials)
117
+ consume_with_redirect(queue: queue, redirected: true, &block)
118
+ rescue GRPC::BadStatus => e
119
+ raise RPCError.new(e.code, e.details)
120
+ end
121
+
122
+ def extract_leader_addr(err)
123
+ err.metadata[LEADER_ADDR_KEY]
124
+ rescue StandardError
125
+ nil
126
+ end
127
+
128
+ def build_credentials(tls:, ca_cert:, client_cert:, client_key:)
129
+ tls_enabled = tls || ca_cert
130
+ validate_tls_options(tls_enabled, client_cert, client_key)
131
+ return :this_channel_is_insecure unless tls_enabled
132
+
133
+ build_channel_credentials(ca_cert, client_cert, client_key)
134
+ end
135
+
136
+ def validate_tls_options(tls_enabled, client_cert, client_key)
137
+ return if tls_enabled || (!client_cert && !client_key)
138
+
139
+ raise ArgumentError, 'tls: true or ca_cert is required when client_cert or client_key is provided'
140
+ end
141
+
142
+ def build_channel_credentials(ca_cert, client_cert, client_key)
143
+ if ca_cert then GRPC::Core::ChannelCredentials.new(ca_cert, client_key, client_cert)
144
+ elsif client_cert && client_key then GRPC::Core::ChannelCredentials.new(nil, client_key, client_cert)
145
+ else GRPC::Core::ChannelCredentials.new
146
+ end
147
+ end
148
+
149
+ def call_metadata
150
+ return {} unless @api_key
151
+
152
+ { 'authorization' => "Bearer #{@api_key}" }
153
+ end
154
+
128
155
  def build_consume_message(msg)
129
156
  metadata = msg.metadata
130
157
  ConsumeMessage.new(
@@ -5,7 +5,7 @@
5
5
  require 'google/protobuf'
6
6
 
7
7
 
8
- descriptor_data = "\n\x13\x66ila/v1/admin.proto\x12\x07\x66ila.v1\"H\n\x12\x43reateQueueRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x06\x63onfig\x18\x02 \x01(\x0b\x32\x14.fila.v1.QueueConfig\"b\n\x0bQueueConfig\x12\x19\n\x11on_enqueue_script\x18\x01 \x01(\t\x12\x19\n\x11on_failure_script\x18\x02 \x01(\t\x12\x1d\n\x15visibility_timeout_ms\x18\x03 \x01(\x04\"\'\n\x13\x43reateQueueResponse\x12\x10\n\x08queue_id\x18\x01 \x01(\t\"#\n\x12\x44\x65leteQueueRequest\x12\r\n\x05queue\x18\x01 \x01(\t\"\x15\n\x13\x44\x65leteQueueResponse\".\n\x10SetConfigRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x13\n\x11SetConfigResponse\"\x1f\n\x10GetConfigRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\"\"\n\x11GetConfigResponse\x12\r\n\x05value\x18\x01 \x01(\t\")\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"#\n\x11ListConfigRequest\x12\x0e\n\x06prefix\x18\x01 \x01(\t\"P\n\x12ListConfigResponse\x12%\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x14.fila.v1.ConfigEntry\x12\x13\n\x0btotal_count\x18\x02 \x01(\r\" \n\x0fGetStatsRequest\x12\r\n\x05queue\x18\x01 \x01(\t\"b\n\x13PerFairnessKeyStats\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x15\n\rpending_count\x18\x02 \x01(\x04\x12\x17\n\x0f\x63urrent_deficit\x18\x03 \x01(\x03\x12\x0e\n\x06weight\x18\x04 \x01(\r\"Z\n\x13PerThrottleKeyStats\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06tokens\x18\x02 \x01(\x01\x12\x17\n\x0frate_per_second\x18\x03 \x01(\x01\x12\r\n\x05\x62urst\x18\x04 \x01(\x01\"\xec\x01\n\x10GetStatsResponse\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\x04\x12\x11\n\tin_flight\x18\x02 \x01(\x04\x12\x1c\n\x14\x61\x63tive_fairness_keys\x18\x03 \x01(\x04\x12\x18\n\x10\x61\x63tive_consumers\x18\x04 \x01(\r\x12\x0f\n\x07quantum\x18\x05 \x01(\r\x12\x33\n\rper_key_stats\x18\x06 \x03(\x0b\x32\x1c.fila.v1.PerFairnessKeyStats\x12\x38\n\x12per_throttle_stats\x18\x07 \x03(\x0b\x32\x1c.fila.v1.PerThrottleKeyStats\"2\n\x0eRedriveRequest\x12\x11\n\tdlq_queue\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"#\n\x0fRedriveResponse\x12\x10\n\x08redriven\x18\x01 \x01(\x04\"\x13\n\x11ListQueuesRequest\"U\n\tQueueInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\x04\x12\x11\n\tin_flight\x18\x03 \x01(\x04\x12\x18\n\x10\x61\x63tive_consumers\x18\x04 \x01(\r\"8\n\x12ListQueuesResponse\x12\"\n\x06queues\x18\x01 \x03(\x0b\x32\x12.fila.v1.QueueInfo2\xb4\x04\n\tFilaAdmin\x12H\n\x0b\x43reateQueue\x12\x1b.fila.v1.CreateQueueRequest\x1a\x1c.fila.v1.CreateQueueResponse\x12H\n\x0b\x44\x65leteQueue\x12\x1b.fila.v1.DeleteQueueRequest\x1a\x1c.fila.v1.DeleteQueueResponse\x12\x42\n\tSetConfig\x12\x19.fila.v1.SetConfigRequest\x1a\x1a.fila.v1.SetConfigResponse\x12\x42\n\tGetConfig\x12\x19.fila.v1.GetConfigRequest\x1a\x1a.fila.v1.GetConfigResponse\x12\x45\n\nListConfig\x12\x1a.fila.v1.ListConfigRequest\x1a\x1b.fila.v1.ListConfigResponse\x12?\n\x08GetStats\x12\x18.fila.v1.GetStatsRequest\x1a\x19.fila.v1.GetStatsResponse\x12<\n\x07Redrive\x12\x17.fila.v1.RedriveRequest\x1a\x18.fila.v1.RedriveResponse\x12\x45\n\nListQueues\x12\x1a.fila.v1.ListQueuesRequest\x1a\x1b.fila.v1.ListQueuesResponseb\x06proto3"
8
+ descriptor_data = "\n\x13\x66ila/v1/admin.proto\x12\x07\x66ila.v1\"H\n\x12\x43reateQueueRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x06\x63onfig\x18\x02 \x01(\x0b\x32\x14.fila.v1.QueueConfig\"b\n\x0bQueueConfig\x12\x19\n\x11on_enqueue_script\x18\x01 \x01(\t\x12\x19\n\x11on_failure_script\x18\x02 \x01(\t\x12\x1d\n\x15visibility_timeout_ms\x18\x03 \x01(\x04\"\'\n\x13\x43reateQueueResponse\x12\x10\n\x08queue_id\x18\x01 \x01(\t\"#\n\x12\x44\x65leteQueueRequest\x12\r\n\x05queue\x18\x01 \x01(\t\"\x15\n\x13\x44\x65leteQueueResponse\".\n\x10SetConfigRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x13\n\x11SetConfigResponse\"\x1f\n\x10GetConfigRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\"\"\n\x11GetConfigResponse\x12\r\n\x05value\x18\x01 \x01(\t\")\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"#\n\x11ListConfigRequest\x12\x0e\n\x06prefix\x18\x01 \x01(\t\"P\n\x12ListConfigResponse\x12%\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x14.fila.v1.ConfigEntry\x12\x13\n\x0btotal_count\x18\x02 \x01(\r\" \n\x0fGetStatsRequest\x12\r\n\x05queue\x18\x01 \x01(\t\"b\n\x13PerFairnessKeyStats\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x15\n\rpending_count\x18\x02 \x01(\x04\x12\x17\n\x0f\x63urrent_deficit\x18\x03 \x01(\x03\x12\x0e\n\x06weight\x18\x04 \x01(\r\"Z\n\x13PerThrottleKeyStats\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0e\n\x06tokens\x18\x02 \x01(\x01\x12\x17\n\x0frate_per_second\x18\x03 \x01(\x01\x12\r\n\x05\x62urst\x18\x04 \x01(\x01\"\x9f\x02\n\x10GetStatsResponse\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\x04\x12\x11\n\tin_flight\x18\x02 \x01(\x04\x12\x1c\n\x14\x61\x63tive_fairness_keys\x18\x03 \x01(\x04\x12\x18\n\x10\x61\x63tive_consumers\x18\x04 \x01(\r\x12\x0f\n\x07quantum\x18\x05 \x01(\r\x12\x33\n\rper_key_stats\x18\x06 \x03(\x0b\x32\x1c.fila.v1.PerFairnessKeyStats\x12\x38\n\x12per_throttle_stats\x18\x07 \x03(\x0b\x32\x1c.fila.v1.PerThrottleKeyStats\x12\x16\n\x0eleader_node_id\x18\x08 \x01(\x04\x12\x19\n\x11replication_count\x18\t \x01(\r\"2\n\x0eRedriveRequest\x12\x11\n\tdlq_queue\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"#\n\x0fRedriveResponse\x12\x10\n\x08redriven\x18\x01 \x01(\x04\"\x13\n\x11ListQueuesRequest\"m\n\tQueueInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\x04\x12\x11\n\tin_flight\x18\x03 \x01(\x04\x12\x18\n\x10\x61\x63tive_consumers\x18\x04 \x01(\r\x12\x16\n\x0eleader_node_id\x18\x05 \x01(\x04\"T\n\x12ListQueuesResponse\x12\"\n\x06queues\x18\x01 \x03(\x0b\x32\x12.fila.v1.QueueInfo\x12\x1a\n\x12\x63luster_node_count\x18\x02 \x01(\r\"Q\n\x13\x43reateApiKeyRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\rexpires_at_ms\x18\x02 \x01(\x04\x12\x15\n\ris_superadmin\x18\x03 \x01(\x08\"J\n\x14\x43reateApiKeyResponse\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x15\n\ris_superadmin\x18\x03 \x01(\x08\"%\n\x13RevokeApiKeyRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\"\x16\n\x14RevokeApiKeyResponse\"\x14\n\x12ListApiKeysRequest\"o\n\nApiKeyInfo\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x15\n\rcreated_at_ms\x18\x03 \x01(\x04\x12\x15\n\rexpires_at_ms\x18\x04 \x01(\x04\x12\x15\n\ris_superadmin\x18\x05 \x01(\x08\"8\n\x13ListApiKeysResponse\x12!\n\x04keys\x18\x01 \x03(\x0b\x32\x13.fila.v1.ApiKeyInfo\".\n\rAclPermission\x12\x0c\n\x04kind\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\"L\n\rSetAclRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12+\n\x0bpermissions\x18\x02 \x03(\x0b\x32\x16.fila.v1.AclPermission\"\x10\n\x0eSetAclResponse\"\x1f\n\rGetAclRequest\x12\x0e\n\x06key_id\x18\x01 \x01(\t\"d\n\x0eGetAclResponse\x12\x0e\n\x06key_id\x18\x01 \x01(\t\x12+\n\x0bpermissions\x18\x02 \x03(\x0b\x32\x16.fila.v1.AclPermission\x12\x15\n\ris_superadmin\x18\x03 \x01(\x08\x32\x8e\x07\n\tFilaAdmin\x12H\n\x0b\x43reateQueue\x12\x1b.fila.v1.CreateQueueRequest\x1a\x1c.fila.v1.CreateQueueResponse\x12H\n\x0b\x44\x65leteQueue\x12\x1b.fila.v1.DeleteQueueRequest\x1a\x1c.fila.v1.DeleteQueueResponse\x12\x42\n\tSetConfig\x12\x19.fila.v1.SetConfigRequest\x1a\x1a.fila.v1.SetConfigResponse\x12\x42\n\tGetConfig\x12\x19.fila.v1.GetConfigRequest\x1a\x1a.fila.v1.GetConfigResponse\x12\x45\n\nListConfig\x12\x1a.fila.v1.ListConfigRequest\x1a\x1b.fila.v1.ListConfigResponse\x12?\n\x08GetStats\x12\x18.fila.v1.GetStatsRequest\x1a\x19.fila.v1.GetStatsResponse\x12<\n\x07Redrive\x12\x17.fila.v1.RedriveRequest\x1a\x18.fila.v1.RedriveResponse\x12\x45\n\nListQueues\x12\x1a.fila.v1.ListQueuesRequest\x1a\x1b.fila.v1.ListQueuesResponse\x12K\n\x0c\x43reateApiKey\x12\x1c.fila.v1.CreateApiKeyRequest\x1a\x1d.fila.v1.CreateApiKeyResponse\x12K\n\x0cRevokeApiKey\x12\x1c.fila.v1.RevokeApiKeyRequest\x1a\x1d.fila.v1.RevokeApiKeyResponse\x12H\n\x0bListApiKeys\x12\x1b.fila.v1.ListApiKeysRequest\x1a\x1c.fila.v1.ListApiKeysResponse\x12\x39\n\x06SetAcl\x12\x16.fila.v1.SetAclRequest\x1a\x17.fila.v1.SetAclResponse\x12\x39\n\x06GetAcl\x12\x16.fila.v1.GetAclRequest\x1a\x17.fila.v1.GetAclResponseb\x06proto3"
9
9
 
10
10
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
11
  pool.add_serialized_file(descriptor_data)
@@ -33,5 +33,17 @@ module Fila
33
33
  ListQueuesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.ListQueuesRequest").msgclass
34
34
  QueueInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.QueueInfo").msgclass
35
35
  ListQueuesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.ListQueuesResponse").msgclass
36
+ CreateApiKeyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.CreateApiKeyRequest").msgclass
37
+ CreateApiKeyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.CreateApiKeyResponse").msgclass
38
+ RevokeApiKeyRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.RevokeApiKeyRequest").msgclass
39
+ RevokeApiKeyResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.RevokeApiKeyResponse").msgclass
40
+ ListApiKeysRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.ListApiKeysRequest").msgclass
41
+ ApiKeyInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.ApiKeyInfo").msgclass
42
+ ListApiKeysResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.ListApiKeysResponse").msgclass
43
+ AclPermission = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.AclPermission").msgclass
44
+ SetAclRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.SetAclRequest").msgclass
45
+ SetAclResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.SetAclResponse").msgclass
46
+ GetAclRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.GetAclRequest").msgclass
47
+ GetAclResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("fila.v1.GetAclResponse").msgclass
36
48
  end
37
49
  end
@@ -24,6 +24,13 @@ module Fila
24
24
  rpc :GetStats, ::Fila::V1::GetStatsRequest, ::Fila::V1::GetStatsResponse
25
25
  rpc :Redrive, ::Fila::V1::RedriveRequest, ::Fila::V1::RedriveResponse
26
26
  rpc :ListQueues, ::Fila::V1::ListQueuesRequest, ::Fila::V1::ListQueuesResponse
27
+ # API key management. CreateApiKey bypasses auth (bootstrap); others require a valid key.
28
+ rpc :CreateApiKey, ::Fila::V1::CreateApiKeyRequest, ::Fila::V1::CreateApiKeyResponse
29
+ rpc :RevokeApiKey, ::Fila::V1::RevokeApiKeyRequest, ::Fila::V1::RevokeApiKeyResponse
30
+ rpc :ListApiKeys, ::Fila::V1::ListApiKeysRequest, ::Fila::V1::ListApiKeysResponse
31
+ # Per-key ACL management.
32
+ rpc :SetAcl, ::Fila::V1::SetAclRequest, ::Fila::V1::SetAclResponse
33
+ rpc :GetAcl, ::Fila::V1::GetAclRequest, ::Fila::V1::GetAclResponse
27
34
  end
28
35
 
29
36
  Stub = Service.rpc_stub_class
data/lib/fila/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fila
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -11,6 +11,15 @@ service FilaAdmin {
11
11
  rpc GetStats(GetStatsRequest) returns (GetStatsResponse);
12
12
  rpc Redrive(RedriveRequest) returns (RedriveResponse);
13
13
  rpc ListQueues(ListQueuesRequest) returns (ListQueuesResponse);
14
+
15
+ // API key management. CreateApiKey bypasses auth (bootstrap); others require a valid key.
16
+ rpc CreateApiKey(CreateApiKeyRequest) returns (CreateApiKeyResponse);
17
+ rpc RevokeApiKey(RevokeApiKeyRequest) returns (RevokeApiKeyResponse);
18
+ rpc ListApiKeys(ListApiKeysRequest) returns (ListApiKeysResponse);
19
+
20
+ // Per-key ACL management.
21
+ rpc SetAcl(SetAclRequest) returns (SetAclResponse);
22
+ rpc GetAcl(GetAclRequest) returns (GetAclResponse);
14
23
  }
15
24
 
16
25
  message CreateQueueRequest {
@@ -89,6 +98,9 @@ message GetStatsResponse {
89
98
  uint32 quantum = 5;
90
99
  repeated PerFairnessKeyStats per_key_stats = 6;
91
100
  repeated PerThrottleKeyStats per_throttle_stats = 7;
101
+ // Cluster fields (0 when not in cluster mode).
102
+ uint64 leader_node_id = 8;
103
+ uint32 replication_count = 9;
92
104
  }
93
105
 
94
106
  message RedriveRequest {
@@ -107,8 +119,79 @@ message QueueInfo {
107
119
  uint64 depth = 2;
108
120
  uint64 in_flight = 3;
109
121
  uint32 active_consumers = 4;
122
+ uint64 leader_node_id = 5;
110
123
  }
111
124
 
112
125
  message ListQueuesResponse {
113
126
  repeated QueueInfo queues = 1;
127
+ uint32 cluster_node_count = 2;
128
+ }
129
+
130
+ // --- API Key Management ---
131
+
132
+ message CreateApiKeyRequest {
133
+ /// Human-readable label for the key.
134
+ string name = 1;
135
+ /// Optional Unix timestamp (milliseconds) after which the key expires.
136
+ /// 0 means no expiration.
137
+ uint64 expires_at_ms = 2;
138
+ /// When true, the key bypasses all ACL checks (superadmin).
139
+ bool is_superadmin = 3;
140
+ }
141
+
142
+ message CreateApiKeyResponse {
143
+ /// Opaque key ID for management operations (revoke, list, set-acl).
144
+ string key_id = 1;
145
+ /// Plaintext API key. Returned once — store it securely.
146
+ string key = 2;
147
+ /// Whether this key has superadmin privileges.
148
+ bool is_superadmin = 3;
149
+ }
150
+
151
+ message RevokeApiKeyRequest {
152
+ string key_id = 1;
153
+ }
154
+
155
+ message RevokeApiKeyResponse {}
156
+
157
+ message ListApiKeysRequest {}
158
+
159
+ message ApiKeyInfo {
160
+ string key_id = 1;
161
+ string name = 2;
162
+ uint64 created_at_ms = 3;
163
+ /// 0 means no expiration.
164
+ uint64 expires_at_ms = 4;
165
+ bool is_superadmin = 5;
166
+ }
167
+
168
+ message ListApiKeysResponse {
169
+ repeated ApiKeyInfo keys = 1;
170
+ }
171
+
172
+ // --- ACL Management ---
173
+
174
+ /// A single permission grant: kind (produce/consume/admin) + queue pattern.
175
+ message AclPermission {
176
+ /// One of: "produce", "consume", "admin".
177
+ string kind = 1;
178
+ /// Queue name or wildcard ("*" or "orders.*").
179
+ string pattern = 2;
180
+ }
181
+
182
+ message SetAclRequest {
183
+ string key_id = 1;
184
+ repeated AclPermission permissions = 2;
185
+ }
186
+
187
+ message SetAclResponse {}
188
+
189
+ message GetAclRequest {
190
+ string key_id = 1;
191
+ }
192
+
193
+ message GetAclResponse {
194
+ string key_id = 1;
195
+ repeated AclPermission permissions = 2;
196
+ bool is_superadmin = 3;
114
197
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fila-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Faisca
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-02 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-protobuf