mirror of
https://github.com/livekit/livekit.git
synced 2026-09-25 09:04:37 +00:00
The registry was doing three jobs at once: fencing worker epochs by id, listing a deployment's candidates, and holding its merged route table. The last two are per-deployment state, so it keyed them on (api key, agent name, deployment) and grew a tenancy concept that only an embedder can actually define. Cloud has to lie to it, passing a project id in a field named APIKey. Split them. Scope is one deployment's serving state and stores no identity at all; whoever embeds the package keys a map of scopes however its own tenancy works, and hands the front a resolved one. Registry keeps only the worker-id fence, which is genuinely node-wide: worker ids are server-issued, so an epoch is superseded wherever it was scoped. The front loses its registry, its SingleKeyFallback and FallbackRequest: the resolver now returns the scope and a fallback already curried on the deployment, plus an ok that carries the 401-vs-503 split the empty api key used to encode. routeTable drops its key and takes the scope's logger, so identity is curried in rather than stored. pkg/service takes ownership of the "api key is the tenant" rule, which is true there and nowhere else, and of releasing a scope once nothing holds it. Behavior is unchanged, including serving public routes to an unauthenticated caller when one configured key or one attached tenant makes the key unambiguous.
118 lines
3.5 KiB
Go
118 lines
3.5 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, endpoint.NormalizeDeployment(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 != ""
|
|
}
|