Files
livekit/pkg/service/agentendpointscope.go
T
Paul Wells eafd1056ca agent endpoints: address an unnamed deployment as "_" (#4997)
An unnamed deployment was addressed as "default" and that spelling leaked
into scope keys, so embedders keyed endpoints differently from job dispatch.
Map "_" back to "" at the URL boundary, as the agent name already does,
and drop NormalizeDeployment.
2026-10-12 14:58:36 -07:00

118 lines
3.4 KiB
Go

// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package service
import (
"sync"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/agent/endpoint"
)
// endpointScopeKey is this server's tenancy for agent HTTP endpoints: the api
// key is the project identity. The endpoint package holds no tenancy identity of
// its own, so this is the one place that rule is written.
type endpointScopeKey struct {
apiKey string
agentName string
deployment string
}
func newEndpointScopeKey(apiKey, agentName, deployment string) endpointScopeKey {
return endpointScopeKey{apiKey, agentName, deployment}
}
// EndpointScopes maps that tenancy onto the endpoint package's scopes, one per
// key, held for as long as a worker holds it.
type EndpointScopes struct {
logger logger.Logger
mu sync.Mutex
scopes map[endpointScopeKey]*endpointScopeEntry
}
// endpointScopeEntry refcounts a scope by the registrations that hold it: a
// scope must outlive a worker that has acquired it but not yet registered, or a
// concurrent release would strand the new registration in an orphaned scope.
type endpointScopeEntry struct {
scope *endpoint.Scope
n int
}
func NewEndpointScopes() *EndpointScopes {
return &EndpointScopes{
logger: logger.GetLogger().WithComponent("agents.endpoint"),
scopes: make(map[endpointScopeKey]*endpointScopeEntry),
}
}
// acquire returns the key's scope, creating it on first use. Every acquire must
// be matched by a release.
func (s *EndpointScopes) acquire(k endpointScopeKey) *endpoint.Scope {
s.mu.Lock()
defer s.mu.Unlock()
e := s.scopes[k]
if e == nil {
e = &endpointScopeEntry{scope: endpoint.NewScope(s.logger.WithValues(
"apiKey", k.apiKey, "agentName", k.agentName, "deployment", k.deployment))}
s.scopes[k] = e
}
e.n++
return e.scope
}
func (s *EndpointScopes) release(k endpointScopeKey) {
s.mu.Lock()
defer s.mu.Unlock()
e := s.scopes[k]
if e == nil {
return
}
if e.n--; e.n <= 0 {
delete(s.scopes, k)
}
}
// Scope returns the deployment's serving state, or nil when no worker holds it -
// which the front reads as "no worker for this deployment".
func (s *EndpointScopes) Scope(apiKey, agentName, deployment string) *endpoint.Scope {
s.mu.Lock()
defer s.mu.Unlock()
if e := s.scopes[newEndpointScopeKey(apiKey, agentName, deployment)]; e != nil {
return e.scope
}
return nil
}
// SingleKey returns the api key when every scope on this node belongs to one -
// the resolution for unauthenticated requests to public endpoints. It is sound
// only because a guessed api key confers no access: the front still serves such
// a request only against routes marked public. ok is false when zero or multiple
// keys are present.
func (s *EndpointScopes) SingleKey() (string, bool) {
s.mu.Lock()
defer s.mu.Unlock()
var key string
for k := range s.scopes {
if key == "" {
key = k.apiKey
} else if key != k.apiKey {
return "", false
}
}
return key, key != ""
}