zizq 0.5.0 → 0.7.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -12
  3. data/lib/active_job/queue_adapters/zizq_adapter.rb +27 -24
  4. data/lib/zizq/ack_processor.rb +17 -11
  5. data/lib/zizq/active_job_config.rb +30 -10
  6. data/lib/zizq/backoff.rb +5 -1
  7. data/lib/zizq/bulk_enqueue.rb +67 -14
  8. data/lib/zizq/client.rb +845 -239
  9. data/lib/zizq/configuration.rb +15 -14
  10. data/lib/zizq/crontab.rb +70 -35
  11. data/lib/zizq/crontab_builder.rb +11 -8
  12. data/lib/zizq/crontab_entry.rb +36 -35
  13. data/lib/zizq/crontab_entry_builder.rb +80 -31
  14. data/lib/zizq/enqueue_request.rb +67 -53
  15. data/lib/zizq/enqueue_with.rb +66 -11
  16. data/lib/zizq/error.rb +22 -6
  17. data/lib/zizq/job.rb +31 -13
  18. data/lib/zizq/job_config.rb +303 -16
  19. data/lib/zizq/lifecycle.rb +10 -2
  20. data/lib/zizq/middleware.rb +7 -6
  21. data/lib/zizq/query.rb +175 -85
  22. data/lib/zizq/resources/budget.rb +109 -0
  23. data/lib/zizq/resources/cron_entry.rb +7 -7
  24. data/lib/zizq/resources/cron_group.rb +9 -5
  25. data/lib/zizq/resources/error_enumerator.rb +11 -9
  26. data/lib/zizq/resources/error_page.rb +2 -1
  27. data/lib/zizq/resources/error_record.rb +5 -5
  28. data/lib/zizq/resources/job.rb +149 -26
  29. data/lib/zizq/resources/job_page.rb +19 -8
  30. data/lib/zizq/resources/job_template.rb +31 -8
  31. data/lib/zizq/resources/page.rb +2 -1
  32. data/lib/zizq/resources/resource.rb +5 -4
  33. data/lib/zizq/resources.rb +10 -9
  34. data/lib/zizq/router.rb +3 -2
  35. data/lib/zizq/test/client.rb +82 -58
  36. data/lib/zizq/test.rb +8 -8
  37. data/lib/zizq/tls_configuration.rb +7 -6
  38. data/lib/zizq/version.rb +1 -1
  39. data/lib/zizq/worker.rb +68 -50
  40. data/lib/zizq/worker_configuration.rb +11 -10
  41. data/lib/zizq.rb +239 -55
  42. data/sig/generated/zizq/active_job_config.rbs +16 -5
  43. data/sig/generated/zizq/bulk_enqueue.rbs +4 -15
  44. data/sig/generated/zizq/client.rbs +378 -8
  45. data/sig/generated/zizq/crontab.rbs +17 -1
  46. data/sig/generated/zizq/crontab_builder.rbs +9 -0
  47. data/sig/generated/zizq/crontab_entry_builder.rbs +4 -16
  48. data/sig/generated/zizq/enqueue_request.rbs +16 -4
  49. data/sig/generated/zizq/enqueue_with.rbs +4 -9
  50. data/sig/generated/zizq/error.rbs +10 -0
  51. data/sig/generated/zizq/job.rbs +6 -0
  52. data/sig/generated/zizq/job_config.rbs +163 -0
  53. data/sig/generated/zizq/query.rbs +79 -2
  54. data/sig/generated/zizq/resources/budget.rbs +97 -0
  55. data/sig/generated/zizq/resources/cron_group.rbs +3 -0
  56. data/sig/generated/zizq/resources/job.rbs +96 -0
  57. data/sig/generated/zizq/resources/job_template.rbs +16 -2
  58. data/sig/generated/zizq.rbs +111 -32
  59. data/sig/zizq.rbs +111 -7
  60. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 424b67e8dbba3c82506c24ed18cbaa9fc87af3febe9e079ef458f705e9299fdd
4
- data.tar.gz: 53bc5f8b3aefbdddc69e2471e6efbd2ba7c84016cb8eb63ff16bc463107991fe
3
+ metadata.gz: ae77b1f8501b8eae45ac668e2af45a5c69be600173c33d50ebc06d5520a04a05
4
+ data.tar.gz: 72afdb1b19ab3fb3fb34f4dba1e9bb19db41d8125bfbd2f278151482aaa0098f
5
5
  SHA512:
6
- metadata.gz: 10f7ab3fe215f04eff3c1753826734c609454c93f9c30c586dd0f38f49ac563d23dcb47317c12e7c00cde3dff558cb124ce0ae25a3c801b9b71a85ce806d96e9
7
- data.tar.gz: 422601a65e9862a73360e036a0e5f44ac36bdcaa04a3fe585567e4fefaf9b73e4fa4736ac3fe9d0cc552173bcd9c47fef98cbc1a6935ef3a64870a839a5a13b9
6
+ metadata.gz: 5b5177e0bf70576bc7632b35bbb9b32f4e85fb3324146b70420b1416ed1d312856eba49be52324c37154af3004f230e6667b3f403f123255af42e6059510367d
7
+ data.tar.gz: d8cbc6f97e3b4bcec66735f9089b3b4e6b201dfedd7e6210fe832a6b8afd718094a1ae1e9b50cb23bc4996f009f4f654c3587d334348431a7648d0628d153b65
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Zizq — Official Ruby Client
2
2
 
3
- This is the official Zizq client library for Ruby.
3
+ Zizq (**/zɪsk/**) is a fast and durable job queue packed into a single native
4
+ binary, built on an embedded LSM database — not on Redis, and not on your
5
+ RDBMS. It works in any stack, crossing programming language boundaries.
4
6
 
5
- Zizq is a simple, zero dependency, single binary job queue system that is both
6
- fast and durable. It is designed to work in any stack through a simple HTTP
7
- API.
7
+ This is the official Zizq client library for Ruby.
8
8
 
9
9
  [![CI](https://github.com/zizq-labs/zizq-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/zizq-labs/zizq-ruby/actions/workflows/ci.yml)
10
10
  [![Gem Version](https://img.shields.io/gem/v/zizq.svg)](https://rubygems.org/gems/zizq)
@@ -21,7 +21,10 @@ API.
21
21
  * Configurable job retention policies
22
22
  * Recurring jobs (cron)
23
23
  * Job introspection and management APIs, with support for `jq` query filters
24
- * Unique jobs
24
+ * Concurrency control that doesn't impact other jobs
25
+ * Rate limiting with configurable burst
26
+ * Unique jobs (deduplicated)
27
+ * Batched jobs (folded/merged)
25
28
  * Testing helpers
26
29
 
27
30
  ## Installation
@@ -33,13 +36,13 @@ API.
33
36
  Add it to your application's `Gemfile`:
34
37
 
35
38
  ```ruby
36
- gem 'zizq', '~> 0.5.0'
39
+ gem 'zizq', '~> 0.7.0'
37
40
  ```
38
41
 
39
42
  Or install it manually:
40
43
 
41
44
  ```shell
42
- $ gem install zizq -v 0.5.0
45
+ $ gem install zizq -v 0.7.0
43
46
  ```
44
47
 
45
48
  Ruby **3.2.8 or newer** is required. Client and server share version
@@ -140,8 +143,8 @@ Zizq.enqueue_with(ready_at: Time.new(2027, 3, 15, 14, 30)).enqueue(SendEmailJob,
140
143
  ```
141
144
 
142
145
  To enqueue many jobs efficiently, `Zizq.enqueue_bulk` sends them in a single
143
- atomic request — across queues and job types, and `enqueue_raw` enqueues can
144
- be mixed in too:
146
+ atomic request — across queues and job types, and job classes vs raw enqueues
147
+ can be mixed in too:
145
148
 
146
149
  ```ruby
147
150
  Zizq.enqueue_bulk do |b|
@@ -149,22 +152,91 @@ Zizq.enqueue_bulk do |b|
149
152
  end
150
153
  ```
151
154
 
152
- Jobs can also be enqueued without `Zizq::Job` via `Zizq.enqueue_raw`
155
+ Jobs can also be enqueued without `Zizq::Job` by providing the named fields
153
156
  designed for lower-level code style, and for cross-language workflows where,
154
157
  for example, a Ruby app enqueues jobs consumed by a Go service.
155
158
 
156
159
  ```ruby
157
- Zizq.enqueue_raw(
160
+ Zizq.enqueue(
158
161
  type: "send_email",
159
162
  queue: "comms",
160
163
  payload: { user_id: 42, template: "welcome" }
161
164
  )
162
165
  ```
163
166
 
167
+ ### Concurrency control
168
+
169
+ The Zizq server supports constraining the number of `in_flight` set of jobs for
170
+ specific job types by binding a named _budget_ to those jobs.
171
+
172
+ ```ruby
173
+ # Define a named budget called "notifications" that jobs can reference
174
+ # (generally somewhere in your application startup code).
175
+ Zizq.define_budget(
176
+ "notifications",
177
+ allocation: 10,
178
+ strategy: { type: :while_in_flight }
179
+ )
180
+
181
+ # Specify the budget on the Job class (or at enqueue time).
182
+ class SendNotificationJob
183
+ include Zizq::Job
184
+
185
+ zizq_budget "notifications"
186
+
187
+ def perform(id)
188
+ # ...
189
+ end
190
+ end
191
+
192
+ # Those jobs execute no more than 10 at any given time. Other jobs continue to
193
+ # flow normally.
194
+ 100.times do |n|
195
+ Zizq.enqueue(SendNotificationJob, n)
196
+ end
197
+ ```
198
+
199
+ ### Rate limiting
200
+
201
+ The Zizq server supports dispatching jobs to workers at a throttled rate by
202
+ binding a named _budget_ to those jobs. Jobs that exceed the limit do not
203
+ block other work. The server is smart enough to "park" them until the moment
204
+ they are ready to dispatch.
205
+
206
+ ```ruby
207
+ # Define a named budget called "image-service" that jobs can reference
208
+ # (generally somewhere in your application startup code).
209
+ Zizq.define_budget(
210
+ "image-service",
211
+ allocation: 1000,
212
+ strategy: {
213
+ type: :time_based,
214
+ duration: 1.minute # or just 60 without ActiveSupport
215
+ }
216
+ )
217
+
218
+ # Specify the budget on the Job class (or at enqueue time).
219
+ class NormalizeImageJob
220
+ include Zizq::Job
221
+
222
+ zizq_budget "image-service"
223
+
224
+ def perform(id)
225
+ # ...
226
+ end
227
+ end
228
+
229
+ # Those jobs are delivered by the server at a rate of 1000/minute without
230
+ # blocking other jobs. Your workers remain free of such knowledge.
231
+ 5000.times do |n|
232
+ Zizq.enqueue(NormalizeImageJob, n)
233
+ end
234
+ ```
235
+
164
236
  ### Cross-language and low-level dispatch
165
237
 
166
238
  When a Ruby app needs to *process* jobs enqueued by another language
167
- (or by `Zizq.enqueue_raw`), `Zizq::Router` maps `type` strings to
239
+ (or itself using raw enqueue form), `Zizq::Router` maps `type` strings to
168
240
  handler blocks operating on plain JSON payloads:
169
241
 
170
242
  ```ruby
@@ -33,7 +33,8 @@ module ActiveJob
33
33
  class ZizqAdapter
34
34
  # Enqueue a job for immediate execution.
35
35
  def enqueue(job)
36
- result = Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
36
+ result =
37
+ Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
37
38
  job.provider_job_id = result.id
38
39
  job.successfully_enqueued = true
39
40
  result
@@ -42,7 +43,8 @@ module ActiveJob
42
43
  # Enqueue a job for execution at a specific time.
43
44
  def enqueue_at(job, timestamp)
44
45
  job.scheduled_at = timestamp
45
- result = Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
46
+ result =
47
+ Zizq.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
46
48
  job.provider_job_id = result.id
47
49
  job.successfully_enqueued = true
48
50
  result
@@ -53,16 +55,19 @@ module ActiveJob
53
55
  # Called by `ActiveJob.perform_all_later` (Rails 7.1+).
54
56
  # Returns the number of successfully enqueued jobs.
55
57
  def enqueue_all(jobs)
56
- results = Zizq.enqueue_bulk do |b|
57
- jobs.each do |job|
58
- b.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
58
+ results =
59
+ Zizq.enqueue_bulk do |b|
60
+ jobs.each do |job|
61
+ b.enqueue_raw(**build_enqueue_request(job).to_enqueue_params)
62
+ end
59
63
  end
60
- end
61
64
 
62
- jobs.zip(results).each do |job, result|
63
- job.provider_job_id = result.id
64
- job.successfully_enqueued = true
65
- end
65
+ jobs
66
+ .zip(results)
67
+ .each do |job, result|
68
+ job.provider_job_id = result.id
69
+ job.successfully_enqueued = true
70
+ end
66
71
 
67
72
  jobs.size
68
73
  rescue => e
@@ -85,23 +90,21 @@ module ActiveJob
85
90
  def build_enqueue_request(job)
86
91
  klass = job.class
87
92
 
88
- req = Zizq::EnqueueRequest.new(
89
- queue: job.queue_name,
90
- type: klass.name,
91
- payload: job.serialize,
92
- priority: job.priority,
93
- ready_at: job.scheduled_at
94
- )
93
+ # Prepare with instance-specific config first.
94
+ req =
95
+ Zizq::EnqueueRequest.new(
96
+ queue: job.queue_name,
97
+ type: klass.name,
98
+ payload: job.serialize,
99
+ priority: job.priority,
100
+ ready_at: job.scheduled_at
101
+ )
95
102
 
96
- if klass.respond_to?(:zizq_unique) && klass.zizq_unique
97
- req.unique_key = klass.zizq_unique_key(*job.arguments)
98
- req.unique_while = klass.zizq_unique_scope
103
+ # Augment with class-level config if the class extends ActiveJobConfig
104
+ if klass.respond_to?(:zizq_apply_class_config)
105
+ klass.zizq_apply_class_config(req, *job.arguments)
99
106
  end
100
107
 
101
- req.retry_limit = klass.zizq_retry_limit if klass.respond_to?(:zizq_retry_limit) && klass.zizq_retry_limit
102
- req.backoff = klass.zizq_backoff if klass.respond_to?(:zizq_backoff) && klass.zizq_backoff
103
- req.retention = klass.zizq_retention if klass.respond_to?(:zizq_retention) && klass.zizq_retention
104
-
105
108
  req
106
109
  end
107
110
  end
@@ -79,13 +79,9 @@ module Zizq
79
79
  # Partition: acks go bulk, nacks go individually.
80
80
  acks, nacks = batch.partition { |i| i.is_a?(Ack) }
81
81
 
82
- unless acks.empty?
83
- barrier.async { process_ack_batch(acks) }
84
- end
82
+ barrier.async { process_ack_batch(acks) } unless acks.empty?
85
83
 
86
- nacks.each do |nack|
87
- barrier.async { process_nack(nack) }
88
- end
84
+ nacks.each { |nack| barrier.async { process_nack(nack) } }
89
85
  end
90
86
 
91
87
  barrier.wait
@@ -101,9 +97,13 @@ module Zizq
101
97
  begin
102
98
  @client.report_success_bulk(ids)
103
99
  rescue ClientError => e
104
- @logger.warn { "Bulk ack (#{ids.size} jobs) returned #{e.status} (dropping: #{e.message})" }
100
+ @logger.warn do
101
+ "Bulk ack (#{ids.size} jobs) returned #{e.status} (dropping: #{e.message})"
102
+ end
105
103
  rescue => e
106
- @logger.warn { "Retrying bulk ack (#{ids.size} jobs) in #{backoff.duration}s: #{e.message}" }
104
+ @logger.warn do
105
+ "Retrying bulk ack (#{ids.size} jobs) in #{backoff.duration}s: #{e.message}"
106
+ end
107
107
  backoff.wait
108
108
  retry
109
109
  end
@@ -119,11 +119,17 @@ module Zizq
119
119
  backtrace: nack.backtrace
120
120
  )
121
121
  rescue NotFoundError
122
- @logger.debug { "Nack for #{nack.job_id} returned 404 (already handled)" }
122
+ @logger.debug do
123
+ "Nack for #{nack.job_id} returned 404 (already handled)"
124
+ end
123
125
  rescue ClientError => e
124
- @logger.error { "Nack for #{nack.job_id} returned #{e.status} (dropping)" }
126
+ @logger.error do
127
+ "Nack for #{nack.job_id} returned #{e.status} (dropping)"
128
+ end
125
129
  rescue => e
126
- @logger.warn { "Retrying nack for #{nack.job_id} in #{backoff.duration}s: #{e.message}" }
130
+ @logger.warn do
131
+ "Retrying nack for #{nack.job_id} in #{backoff.duration}s: #{e.message}"
132
+ end
127
133
  backoff.wait
128
134
  retry
129
135
  end
@@ -65,16 +65,7 @@ module Zizq
65
65
  # side. This method is not used in the ActiveJob dispatch path.
66
66
  def zizq_deserialize(_payload) #: (untyped) -> [Array[untyped], Hash[Symbol, untyped]]
67
67
  raise NotImplementedError,
68
- "ActiveJob handles deserialization via ActiveJob::Base.execute"
69
- end
70
-
71
- # Override unique key generation to hash only the arguments portion
72
- # of the serialized payload. The full payload contains volatile fields
73
- # (job_id, enqueued_at, etc.) that change per instance.
74
- def zizq_unique_key(*args, **kwargs) #: (*untyped, **untyped) -> String
75
- arguments = new(*args, **kwargs).serialize["arguments"]
76
- payload = normalize_payload(arguments)
77
- "#{name}:#{Digest::SHA256.hexdigest(JSON.generate(payload))}"
68
+ "ActiveJob handles deserialization via ActiveJob::Base.execute"
78
69
  end
79
70
 
80
71
  # Generate a jq expression that exactly matches payloads with the given
@@ -139,5 +130,34 @@ module Zizq
139
130
 
140
131
  parts.join(" and ")
141
132
  end
133
+
134
+ # Static jq expressions for the batched-job configuration on this
135
+ # class. Targets `.arguments[N]` for a positional batch arg or
136
+ # `.arguments[-1].NAME` for a keyword batch arg — the latter
137
+ # assumes the class actually receives kwargs so that the last
138
+ # argument is the ActiveJob kwargs hash. Behaviour is unspecified
139
+ # if the declared `kwarg:` isn't present at enqueue time.
140
+ def zizq_batch_expressions #: () -> Zizq::batch_expressions?
141
+ return nil unless zizq_batched
142
+
143
+ target =
144
+ if (idx = zizq_batch_arg)
145
+ ".arguments[#{idx}]"
146
+ else
147
+ ".arguments[-1].#{zizq_batch_kwarg}"
148
+ end
149
+
150
+ build_batch_expressions(target)
151
+ end
152
+
153
+ private
154
+
155
+ # Hash only the arguments portion of the serialized ActiveJob
156
+ # payload. The full envelope contains volatile fields (job_id,
157
+ # enqueued_at, etc.) that change per instance and would defeat
158
+ # both `zizq_unique_key` and `zizq_batch_key`.
159
+ def hashable_args(*args, **kwargs) #: (*untyped, **untyped) -> untyped
160
+ new(*args, **kwargs).serialize["arguments"]
161
+ end
142
162
  end
143
163
  end
data/lib/zizq/backoff.rb CHANGED
@@ -44,7 +44,11 @@ module Zizq
44
44
 
45
45
  # Returns a new Backoff with the same configuration but reset state.
46
46
  def fresh #: () -> Backoff
47
- self.class.new(min_wait: @min_wait, max_wait: @max_wait, multiplier: @multiplier)
47
+ self.class.new(
48
+ min_wait: @min_wait,
49
+ max_wait: @max_wait,
50
+ multiplier: @multiplier
51
+ )
48
52
  end
49
53
  end
50
54
  end
@@ -27,26 +27,79 @@ module Zizq
27
27
  # @rbs kwargs: Hash[Symbol, untyped]
28
28
  # @rbs &block: ?(EnqueueRequest) -> void
29
29
  # @rbs return: void
30
- def enqueue(job_class, *args, **kwargs, &block)
31
- @requests << Zizq.build_enqueue_request(job_class, *args, **kwargs, &block)
30
+ def enqueue_job_class(job_class, *args, **kwargs, &block)
31
+ @requests << Zizq.build_enqueue_request(
32
+ job_class,
33
+ *args,
34
+ **kwargs,
35
+ &block
36
+ )
37
+ end
38
+
39
+ # Collect an enqueue, by class or by raw inputs.
40
+ #
41
+ # Both forms of `Zizq.enqueue` work here too:
42
+ #
43
+ # b.enqueue(SendEmailJob, 42, template: "welcome")
44
+ # b.enqueue(queue: "emails", type: "send_email", payload: {...})
45
+ #
46
+ # @rbs skip
47
+ # @rbs!
48
+ # def enqueue: (
49
+ # Class & Zizq::JobConfig job_class,
50
+ # *untyped args,
51
+ # **untyped kwargs
52
+ # ) ?{ (EnqueueRequest) -> void } -> void
53
+ # | (
54
+ # queue: String,
55
+ # type: String,
56
+ # payload: untyped,
57
+ # ?priority: Integer?,
58
+ # ?delay: Zizq::to_f?,
59
+ # ?ready_at: Zizq::to_f?,
60
+ # ?retry_limit: Integer?,
61
+ # ?backoff: Zizq::backoff?,
62
+ # ?retention: Zizq::retention?,
63
+ # ?unique_key: String?,
64
+ # ?unique_while: Zizq::unique_scope?,
65
+ # ?batch: Zizq::batch?,
66
+ # ?budgets: Array[Zizq::budget_binding_params]?
67
+ # ) ?{ (EnqueueRequest) -> void } -> void
68
+ def enqueue(*args, **kwargs, &block)
69
+ return enqueue_job_class(*args, **kwargs, &block) unless args.empty?
70
+
71
+ begin
72
+ # See `Zizq.enqueue` for why only this branch is wrapped.
73
+ enqueue_raw(**kwargs, &block) # steep:ignore InsufficientKeywordArguments
74
+ rescue ArgumentError => e
75
+ raise ArgumentError, e.message, caller(1)
76
+ end
32
77
  end
33
78
 
34
79
  # Collect a raw enqueue. Accepts the same arguments as
35
80
  # `Zizq.enqueue_raw`.
36
81
  #
37
- # @rbs queue: String
38
- # @rbs type: String
39
- # @rbs payload: untyped
40
- # @rbs priority: Integer?
41
- # @rbs ready_at: Zizq::to_f?
42
- # @rbs retry_limit: Integer?
43
- # @rbs backoff: Zizq::backoff?
44
- # @rbs retention: Zizq::retention?
45
- # @rbs unique_key: String?
46
- # @rbs unique_while: Zizq::unique_scope?
47
- # @rbs return: void
82
+ # @rbs skip
83
+ # @rbs!
84
+ # def enqueue_raw: (
85
+ # queue: String,
86
+ # type: String,
87
+ # payload: untyped,
88
+ # ?priority: Integer?,
89
+ # ?delay: Zizq::to_f?,
90
+ # ?ready_at: Zizq::to_f?,
91
+ # ?retry_limit: Integer?,
92
+ # ?backoff: Zizq::backoff?,
93
+ # ?retention: Zizq::retention?,
94
+ # ?unique_key: String?,
95
+ # ?unique_while: Zizq::unique_scope?,
96
+ # ?batch: Zizq::batch?,
97
+ # ?budgets: Array[Zizq::budget_binding_params]?
98
+ # ) ?{ (EnqueueRequest) -> void } -> void
48
99
  def enqueue_raw(queue:, type:, payload:, **opts)
49
- @requests << EnqueueRequest.new(queue:, type:, payload:, **opts)
100
+ req = EnqueueRequest.new(queue:, type:, payload:, **opts)
101
+ yield req if block_given?
102
+ @requests << req
50
103
  end
51
104
 
52
105
  # Build a scoped enqueue helper that applies the given overrides to a