File size: 3,107 Bytes
6a7089a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | package instance
import (
"fmt"
"log/slog"
"sync"
"github.com/pinchtab/pinchtab/internal/bridge"
)
// Locator discovers which instance owns a given tab.
// Uses an in-memory cache for O(1) lookups, falling back to
// querying bridge instances on cache miss.
type Locator struct {
mu sync.RWMutex
cache map[string]string // tabID → instanceID
repo *Repository
fetcher TabFetcher
}
// NewLocator creates a Locator backed by the given repository and tab fetcher.
func NewLocator(repo *Repository, fetcher TabFetcher) *Locator {
return &Locator{
cache: make(map[string]string),
repo: repo,
fetcher: fetcher,
}
}
// FindInstanceByTabID returns the instance that owns the given tab.
// Fast path: cache hit (O(1)). Slow path: queries all bridges.
func (l *Locator) FindInstanceByTabID(tabID string) (*bridge.Instance, error) {
// Fast path: check cache.
l.mu.RLock()
instID, ok := l.cache[tabID]
l.mu.RUnlock()
if ok {
inst, found := l.repo.Get(instID)
if found && inst.Status == "running" {
return inst, nil
}
// Stale cache entry — instance gone or not running.
l.Invalidate(tabID)
}
// Slow path: query all running bridges.
running := l.repo.Running()
for _, inst := range running {
url := inst.URL
if url == "" {
url = fmt.Sprintf("http://localhost:%s", inst.Port)
}
tabs, err := l.fetcher.FetchTabs(url)
if err != nil {
slog.Debug("locator: failed to fetch tabs", "instance", inst.ID, "err", err)
continue
}
for _, tab := range tabs {
// Cache every tab we discover for future lookups.
l.mu.Lock()
l.cache[tab.ID] = inst.ID
l.mu.Unlock()
if tab.ID == tabID {
result := inst // copy
return &result, nil
}
}
}
return nil, fmt.Errorf("tab %q not found in any running instance", tabID)
}
// Register adds a tab→instance mapping to the cache.
func (l *Locator) Register(tabID, instanceID string) {
l.mu.Lock()
l.cache[tabID] = instanceID
l.mu.Unlock()
}
// Invalidate removes a tab from the cache.
func (l *Locator) Invalidate(tabID string) {
l.mu.Lock()
delete(l.cache, tabID)
l.mu.Unlock()
}
// InvalidateInstance removes all cached tabs for an instance.
func (l *Locator) InvalidateInstance(instanceID string) {
l.mu.Lock()
for tabID, instID := range l.cache {
if instID == instanceID {
delete(l.cache, tabID)
}
}
l.mu.Unlock()
}
// RefreshAll clears the cache and re-discovers all tabs from running instances.
func (l *Locator) RefreshAll() {
l.mu.Lock()
l.cache = make(map[string]string)
l.mu.Unlock()
running := l.repo.Running()
for _, inst := range running {
url := inst.URL
if url == "" {
url = fmt.Sprintf("http://localhost:%s", inst.Port)
}
tabs, err := l.fetcher.FetchTabs(url)
if err != nil {
slog.Debug("locator: refresh failed", "instance", inst.ID, "err", err)
continue
}
l.mu.Lock()
for _, tab := range tabs {
l.cache[tab.ID] = inst.ID
}
l.mu.Unlock()
}
}
// CacheSize returns the number of cached tab→instance mappings.
func (l *Locator) CacheSize() int {
l.mu.RLock()
defer l.mu.RUnlock()
return len(l.cache)
}
|