kvm2: apply the milestone review
Shorter start wait with immediate verdicts for dead or paused domains, ListByTag without state or address RPCs, Timeout for an expired check round, no adoption of unmarked domains, reference-counted keyed locks, dead options removed, HEAD fallback on any non-2xx. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,6 @@ type Provider struct {
|
||||
now func() time.Time
|
||||
hosts *hostTable
|
||||
http *http.Client
|
||||
budgets budgets
|
||||
tracing []tracing.Option
|
||||
|
||||
// runningWait caps the wait for a started domain to report running,
|
||||
@@ -42,28 +41,23 @@ type Provider struct {
|
||||
runningPoll time.Duration
|
||||
|
||||
// imageLocks serialises image fetches per (host, volume), spec entry 19.
|
||||
imageMu sync.Mutex
|
||||
imageLocks map[string]chan struct{}
|
||||
imageLocks *keyedLocks
|
||||
|
||||
// nameLocks serialises Create and Delete per instance name, spec entry
|
||||
// 19, so two Deletes never race on destroy and undefine and a Delete
|
||||
// cannot interleave with a Create's steps.
|
||||
nameMu sync.Mutex
|
||||
nameLocks map[string]chan struct{}
|
||||
nameLocks *keyedLocks
|
||||
}
|
||||
|
||||
// providerOption adjusts what newProvider builds; tests inject an HTTP
|
||||
// client bound to a test server, a span recorder and shorter budgets.
|
||||
// client bound to a test server, a span recorder and a shorter running
|
||||
// wait.
|
||||
type providerOption func(*Provider)
|
||||
|
||||
func withHTTPClient(c *http.Client) providerOption {
|
||||
return func(p *Provider) { p.http = c }
|
||||
}
|
||||
|
||||
func withBudgets(b budgets) providerOption {
|
||||
return func(p *Provider) { p.budgets = b }
|
||||
}
|
||||
|
||||
func withTracing(opts ...tracing.Option) providerOption {
|
||||
return func(p *Provider) { p.tracing = opts }
|
||||
}
|
||||
@@ -104,7 +98,7 @@ func New(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, er
|
||||
// newProvider is the constructor tests use: d hands out the backends, the
|
||||
// production one being newLibvirtDialer. Without options the image
|
||||
// downloads use http.DefaultClient (system CA roots, no client timeout: the
|
||||
// per-request ctx carries the transfer budget) and the default budgets.
|
||||
// per-request ctx carries the transfer budget).
|
||||
func newProvider(name string, cfg provider.KVMConfig, d dialer, opts ...providerOption) *Provider {
|
||||
p := &Provider{
|
||||
name: name,
|
||||
@@ -112,11 +106,10 @@ func newProvider(name string, cfg provider.KVMConfig, d dialer, opts ...provider
|
||||
now: time.Now,
|
||||
hosts: newHostTable(d),
|
||||
http: http.DefaultClient,
|
||||
budgets: defaultBudgets,
|
||||
runningWait: defaultRunningWait,
|
||||
runningPoll: defaultRunningPoll,
|
||||
imageLocks: make(map[string]chan struct{}),
|
||||
nameLocks: make(map[string]chan struct{}),
|
||||
imageLocks: newKeyedLocks(),
|
||||
nameLocks: newKeyedLocks(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(p)
|
||||
@@ -187,21 +180,64 @@ func (p *Provider) wrapErr(op, id string, err error) error {
|
||||
return provider.Wrap(classify(op, err), op, p.name, id, err)
|
||||
}
|
||||
|
||||
// lockName takes the per-name lock of spec entry 19, or gives up with ctx.
|
||||
// A one-slot channel rather than a mutex, so a caller whose ctx ends while
|
||||
// another caller's Create is downloading an image returns promptly.
|
||||
// lockName takes the per-name lock of spec entry 19, or gives up with ctx,
|
||||
// so a caller whose ctx ends while another caller's Create is downloading
|
||||
// an image returns promptly.
|
||||
func (p *Provider) lockName(ctx context.Context, name string) (func(), error) {
|
||||
p.nameMu.Lock()
|
||||
sem, ok := p.nameLocks[name]
|
||||
if !ok {
|
||||
sem = make(chan struct{}, 1)
|
||||
p.nameLocks[name] = sem
|
||||
release, err := p.nameLocks.acquire(ctx, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: waiting for the lock on %s: %w", provider.ErrTransient, name, err)
|
||||
}
|
||||
p.nameMu.Unlock()
|
||||
return release, nil
|
||||
}
|
||||
|
||||
// keyedLocks is a set of one-slot semaphores by key (spec entry 19): a
|
||||
// semaphore rather than a mutex so a waiter can give up with its ctx. Each
|
||||
// entry counts the callers holding or waiting for it and is dropped from
|
||||
// the map at zero, so the map does not grow with every name ever seen while
|
||||
// two waiters on one key still always share one semaphore.
|
||||
type keyedLocks struct {
|
||||
mu sync.Mutex
|
||||
locks map[string]*keyedLock
|
||||
}
|
||||
|
||||
type keyedLock struct {
|
||||
sem chan struct{}
|
||||
refs int
|
||||
}
|
||||
|
||||
func newKeyedLocks() *keyedLocks {
|
||||
return &keyedLocks{locks: make(map[string]*keyedLock)}
|
||||
}
|
||||
|
||||
// acquire takes the lock for key, or gives up with ctx's error. The
|
||||
// returned func releases it and must be called exactly once.
|
||||
func (k *keyedLocks) acquire(ctx context.Context, key string) (func(), error) {
|
||||
k.mu.Lock()
|
||||
l, ok := k.locks[key]
|
||||
if !ok {
|
||||
l = &keyedLock{sem: make(chan struct{}, 1)}
|
||||
k.locks[key] = l
|
||||
}
|
||||
l.refs++
|
||||
k.mu.Unlock()
|
||||
select {
|
||||
case sem <- struct{}{}:
|
||||
return func() { <-sem }, nil
|
||||
case l.sem <- struct{}{}:
|
||||
return func() {
|
||||
<-l.sem
|
||||
k.unref(key, l)
|
||||
}, nil
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%w: waiting for the lock on %s: %w", provider.ErrTransient, name, ctx.Err())
|
||||
k.unref(key, l)
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (k *keyedLocks) unref(key string, l *keyedLock) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
l.refs--
|
||||
if l.refs == 0 {
|
||||
delete(k.locks, key)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user