Files
yatm/internal/executor/resource_queue.go
T
Samuel CuiandClaude Opus 5.5 47b525b747 Write, read and publish encrypted tar Media
Compose tar FORMAT/APPEND, Restore, Scan and Verify through complete Media sessions and the configured encrypt adapter. Publish only complete members at normal or capacity/EW completion, maintain position maps, apply Logical or Auto read order, record passive read learning observations in each Job and refit learned read parameters after drive release. LTFS keeps its adapters and gains the shared read order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:01:44 +08:00

149 lines
4.1 KiB
Go

package executor
import (
"context"
"fmt"
"sync"
)
func tapeResource(device string) string {
return fmt.Sprintf("tape:%s", device)
}
func volumeResource(uuid string) string {
return fmt.Sprintf("volume:%s", uuid)
}
// resourceQueue serializes Tape devices and Volume leases. Contention waits instead of failing the
// Job, and waiters are admitted in arrival order so a long-running holder cannot starve an earlier request.
type resourceQueue struct {
lock sync.Mutex
held map[string]struct{}
waiters map[string][]chan struct{}
}
func newResourceQueue() *resourceQueue {
return &resourceQueue{held: make(map[string]struct{}), waiters: make(map[string][]chan struct{})}
}
// AcquireJobResource reserves one resource key until the returned release runs exactly once.
// onWait observes an actual wait, so callers can publish their waiting phase.
func (e *Executor) AcquireJobResource(ctx context.Context, key string, onWait func()) (func(), error) {
if err := e.resources.acquire(ctx, key, onWait); err != nil {
return nil, err
}
return func() { e.resources.release(key) }, nil
}
// tryAcquire reserves a free key without waiting, for operator-bound requests that report a busy
// resource instead of blocking their response.
func (e *Executor) tryAcquire(key string) (func(), bool) {
e.resources.lock.Lock()
defer e.resources.lock.Unlock()
if _, busy := e.resources.held[key]; busy {
return nil, false
}
e.resources.held[key] = struct{}{}
return func() { e.resources.release(key) }, true
}
// resourceHeld reports whether any owner currently holds the key.
func (e *Executor) resourceHeld(key string) bool {
e.resources.lock.Lock()
defer e.resources.lock.Unlock()
_, busy := e.resources.held[key]
return busy
}
func (q *resourceQueue) acquire(ctx context.Context, key string, onWait func()) error {
ready, err := q.enqueue(ctx, key)
if err != nil {
return err
}
return q.wait(ctx, key, ready, onWait)
}
// enqueue captures ownership or a FIFO slot before the caller drops its identity lock.
// A nil ready channel means the resource was acquired without waiting.
func (q *resourceQueue) enqueue(ctx context.Context, key string) (chan struct{}, error) {
// A caller that is already canceled never takes or waits for the resource.
if err := ctx.Err(); err != nil {
return nil, err
}
q.lock.Lock()
defer q.lock.Unlock()
if _, busy := q.held[key]; !busy {
q.held[key] = struct{}{}
return nil, nil
}
ready := make(chan struct{})
q.waiters[key] = append(q.waiters[key], ready)
return ready, nil
}
func (q *resourceQueue) wait(ctx context.Context, key string, ready chan struct{}, onWait func()) error {
// An immediate admission already owns its slot.
if ready == nil {
return nil
}
// Report the wait outside the queue lock; the caller may publish a waiting phase.
if onWait != nil {
onWait()
}
select {
case <-ready:
return nil
case <-ctx.Done():
q.lock.Lock()
defer q.lock.Unlock()
if dropWaiter(q.waiters, key, ready) {
return ctx.Err()
}
// The slot was handed over while this caller gave up; pass it on instead of leaking it.
q.handover(key)
return ctx.Err()
}
}
// release hands the key to the first waiter, or marks it free when nobody waits.
func (q *resourceQueue) release(key string) {
q.lock.Lock()
defer q.lock.Unlock()
q.handover(key)
}
// handover transfers the key while the queue lock is held, so the resource is never briefly free.
func (q *resourceQueue) handover(key string) {
waiting := q.waiters[key]
if len(waiting) == 0 {
delete(q.held, key)
return
}
next := waiting[0]
if len(waiting) == 1 {
delete(q.waiters, key)
} else {
q.waiters[key] = waiting[1:]
}
close(next)
}
// dropWaiter removes one still-queued waiter and reports whether it was found.
func dropWaiter(waiters map[string][]chan struct{}, key string, ready chan struct{}) bool {
waiting := waiters[key]
for index, candidate := range waiting {
if candidate != ready {
continue
}
if len(waiting) == 1 {
delete(waiters, key)
} else {
remaining := append(waiting[:index:index], waiting[index+1:]...)
waiters[key] = remaining
}
return true
}
return false
}