mirror of
https://github.com/livekit/livekit.git
synced 2026-09-22 22:25:35 +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.
124 lines
3.4 KiB
Go
124 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 endpoint
|
|
|
|
import (
|
|
"slices"
|
|
"sync"
|
|
|
|
"github.com/livekit/protocol/logger"
|
|
)
|
|
|
|
// Scope is one deployment's serving state on this node: the registrations that
|
|
// hold it and their merged route table. Who a scope belongs to is the embedder's
|
|
// business - nothing here stores a tenancy identity, and the embedder owns the
|
|
// map that scopes it.
|
|
//
|
|
// Registrations and routes move together under lock, so a worker present in the
|
|
// candidate set always has its routes installed.
|
|
type Scope struct {
|
|
logger logger.Logger
|
|
|
|
lock sync.RWMutex
|
|
regs []*Registration
|
|
table *routeTable // nil while nothing declares a route
|
|
}
|
|
|
|
// NewScope takes a logger the embedder has already curried with whatever
|
|
// identifies the scope; the package adds no identity of its own.
|
|
func NewScope(l logger.Logger) *Scope {
|
|
return &Scope{logger: l}
|
|
}
|
|
|
|
// Candidates returns the registrations holding this scope. A nil scope holds
|
|
// none, so a caller that could not resolve one needs no separate test.
|
|
func (s *Scope) Candidates() []*Registration {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
return slices.Clone(s.regs)
|
|
}
|
|
|
|
// Empty reports that no registration holds this scope, so the embedder may drop
|
|
// it.
|
|
func (s *Scope) Empty() bool {
|
|
if s == nil {
|
|
return true
|
|
}
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
return len(s.regs) == 0
|
|
}
|
|
|
|
// routeTable returns the merged table, or nil when nothing declares a route. A
|
|
// scope whose last worker left must report nil rather than an empty tree: the
|
|
// front maps a missing table to "no worker for this deployment" (503) and an
|
|
// empty one to "no such path" (404).
|
|
func (s *Scope) routeTable() *routeTable {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
s.lock.RLock()
|
|
defer s.lock.RUnlock()
|
|
return s.table
|
|
}
|
|
|
|
// replace swaps one registration for another as a single transaction. Routes the
|
|
// removal empties are dropped only once the addition has run, so a registration
|
|
// replaced by an equivalent one keeps its Route pointers and the published tree
|
|
// stays live. Either side may be nil.
|
|
func (s *Scope) replace(remove, add *Registration) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
if remove != nil {
|
|
s.unlinkLocked(remove)
|
|
}
|
|
if add != nil {
|
|
s.regs = append(s.regs, add)
|
|
}
|
|
s.mutateLocked(remove, add)
|
|
}
|
|
|
|
// remove drops a registration and its routes.
|
|
func (s *Scope) remove(r *Registration) {
|
|
s.lock.Lock()
|
|
defer s.lock.Unlock()
|
|
s.unlinkLocked(r)
|
|
s.mutateLocked(r, nil)
|
|
}
|
|
|
|
func (s *Scope) unlinkLocked(r *Registration) {
|
|
if i := slices.Index(s.regs, r); i != -1 {
|
|
s.regs = slices.Delete(s.regs, i, i+1)
|
|
}
|
|
}
|
|
|
|
// mutateLocked applies the route change, creating the table on first use and
|
|
// dropping it once it empties.
|
|
func (s *Scope) mutateLocked(remove, add *Registration) {
|
|
if s.table == nil {
|
|
if add == nil {
|
|
return
|
|
}
|
|
s.table = newRouteTable(s.logger)
|
|
}
|
|
s.table.mutate(remove, add)
|
|
if s.table.empty() {
|
|
s.table = nil
|
|
}
|
|
}
|