@@ -61,10 +61,11 @@ mutable struct RetryBucket
6161 capacity:: Int
6262 partitions:: Dict{String,_RetryPartition}
6363 lock:: ReentrantLock
64- # Copy-on-write snapshot of partition keys below full capacity. Published
65- # snapshots are treated as immutable. Writers publish under `lock`; readers
66- # use the snapshot to avoid locking for healthy traffic to unrelated keys.
67- @atomic depleted_partitions:: Set{String}
64+ # Number of partition keys below full capacity. Writers update it under
65+ # `lock`; readers use it to avoid locking while every partition is full.
66+ # Keep this field pointer-free: atomic references to GC-managed containers
67+ # can corrupt the referenced object on Julia 1.10 (#1355).
68+ @atomic depleted_partitions:: Int
6869end
6970
7071""" Handle returned by `acquire` and consumed by `release` to refund retry budget."""
@@ -105,7 +106,7 @@ function RetryBucket(;
105106 Int (capacity),
106107 Dict {String,_RetryPartition} (),
107108 ReentrantLock (),
108- Set {String} () ,
109+ 0 ,
109110 )
110111end
111112
@@ -118,9 +119,7 @@ function RetryBucket(
118119 partitions:: Dict{String,_RetryPartition} ,
119120 lock:: ReentrantLock ,
120121)
121- depleted_partitions = Set (
122- key for (key, state) in partitions if state. capacity < capacity
123- )
122+ depleted_partitions = count (state -> state. capacity < capacity, values (partitions))
124123 return RetryBucket (
125124 backoff_scale_factor_ms,
126125 max_backoff_secs,
@@ -131,6 +130,27 @@ function RetryBucket(
131130 )
132131end
133132
133+ # Preserve the six-field constructor exposed in HTTP 2.6.6. The set argument
134+ # is accepted only for compatibility; the pointer-free depleted-partition
135+ # count is derived from `partitions` so the count invariant holds even when
136+ # the caller's set disagrees with the partition states.
137+ function RetryBucket (
138+ backoff_scale_factor_ms:: Int ,
139+ max_backoff_secs:: Int ,
140+ capacity:: Int ,
141+ partitions:: Dict{String,_RetryPartition} ,
142+ lock:: ReentrantLock ,
143+ :: Set{String} ,
144+ )
145+ return RetryBucket (
146+ backoff_scale_factor_ms,
147+ max_backoff_secs,
148+ capacity,
149+ partitions,
150+ lock,
151+ )
152+ end
153+
134154function RetryBucket (
135155 backoff_scale_factor_ms,
136156 max_backoff_secs,
@@ -147,25 +167,23 @@ function RetryBucket(
147167 )
148168end
149169
150- # Set a partition's capacity while keeping the published depleted-key snapshot
151- # in sync. Must be called with `bucket.lock` held.
170+ # Set a partition's capacity while keeping the depleted-partition count in sync.
171+ # Must be called with `bucket.lock` held.
152172@inline function _retry_partition_set_capacity! (
153173 bucket:: RetryBucket ,
154- partition_key:: String ,
155174 state:: _RetryPartition ,
156175 new_capacity:: Int ,
157176):: Nothing
158177 was_full = state. capacity >= bucket. capacity
159178 now_full = new_capacity >= bucket. capacity
160179 state. capacity = new_capacity
161180 if was_full && ! now_full
162- depleted = copy (@atomic :acquire bucket. depleted_partitions)
163- push! (depleted, partition_key)
164- @atomic :release bucket. depleted_partitions = depleted
181+ depleted = @atomic :monotonic bucket. depleted_partitions
182+ @atomic :release bucket. depleted_partitions = depleted + 1
165183 elseif ! was_full && now_full
166- depleted = copy ( @atomic :acquire bucket. depleted_partitions)
167- delete! ( depleted, partition_key )
168- @atomic :release bucket. depleted_partitions = depleted
184+ depleted = @atomic :monotonic bucket. depleted_partitions
185+ depleted > 0 || error ( " retry bucket depleted-partition count underflow " )
186+ @atomic :release bucket. depleted_partitions = depleted - 1
169187 end
170188 return nothing
171189end
@@ -190,7 +208,7 @@ function acquire(bucket::RetryBucket, partition)
190208 if state. capacity < _RETRY_BUCKET_ACQUIRE_COST
191209 throw (RetryDeniedError (partition_key))
192210 end
193- _retry_partition_set_capacity! (bucket, partition_key, state, state. capacity - _RETRY_BUCKET_ACQUIRE_COST)
211+ _retry_partition_set_capacity! (bucket, state, state. capacity - _RETRY_BUCKET_ACQUIRE_COST)
194212 return RetryToken (bucket, partition_key, _RETRY_BUCKET_ACQUIRE_COST, false )
195213 end
196214end
222240 reserved = _retry_bucket_reserved_cost (token)
223241 consumed = min (reserved, max (0 , failure_cost))
224242 refund = reserved - consumed
225- _retry_partition_set_capacity! (bucket, token . partition, state, min (bucket. capacity, state. capacity + refund))
243+ _retry_partition_set_capacity! (bucket, state, min (bucket. capacity, state. capacity + refund))
226244 token. released = true
227245 return nothing
228246 finally
@@ -238,21 +256,20 @@ non-retried request, capped at the bucket's full capacity. This is the slow
238256recovery path that lets a partition legitimately drained by a burst of real
239257failures regain retry budget from healthy traffic instead of staying empty for
240258the transport's lifetime. Partitions that have never spent capacity are left
241- untouched. The published depleted-key snapshot also keeps this lock-free for
242- healthy traffic to other partitions .
259+ untouched. The depleted-partition count keeps this lock-free while all
260+ partitions are healthy .
243261"""
244262function _retry_bucket_replenish! (bucket:: RetryBucket , partition):: Nothing
245263 depleted = @atomic :acquire bucket. depleted_partitions
246- isempty ( depleted) && return nothing
264+ depleted == 0 && return nothing
247265 partition_key = _retry_bucket_partition_key (partition)
248- partition_key in depleted || return nothing
249266 lock (bucket. lock)
250267 try
251268 state = get (() -> nothing , bucket. partitions, partition_key)
252269 state === nothing && return nothing
253270 partition_state = state:: _RetryPartition
254271 partition_state. capacity >= bucket. capacity && return nothing
255- _retry_partition_set_capacity! (bucket, partition_key, partition_state, partition_state. capacity + 1 )
272+ _retry_partition_set_capacity! (bucket, partition_state, partition_state. capacity + 1 )
256273 return nothing
257274 finally
258275 unlock (bucket. lock)
0 commit comments