PHP Classes

File: extension/registry/registry.go

Recommend this page to a friend!
  Packages of Roni   Easy-Excel   extension/registry/registry.go   Download  
File: extension/registry/registry.go
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Easy-Excel
Export and read XLSX files with a PHP Go extension
Author: By
Last change:
Date: 8 days ago
Size: 3,751 bytes
 

Contents

Class file image Download
// Package registry maps opaque int64 handles to live workbook objects. // // Handles are random (capability-style, PLAN.md §8): a stale or forged handle // from another request errors instead of aliasing someone else's workbook. // The table is sharded to avoid global lock contention in FrankenPHP worker // mode, and a janitor closes entries idle longer than the TTL as a backstop // for PHP code that never calls close (PLAN.md §7.6). package registry import ( "crypto/rand" "encoding/binary" "errors" "sync" "time" ) const shardCount = 16 // ErrNotFound is returned for unknown, stale, or foreign handles. var ErrNotFound = errors.New("easy-excel: unknown or expired workbook handle") // Closer is implemented by registered values needing cleanup on eviction. type Closer interface { Close() error } type entry struct { value any lastUsed time.Time } type shard struct { mu sync.RWMutex entries map[int64]*entry } // Registry is a sharded handle table with idle-TTL eviction. type Registry struct { shards [shardCount]shard ttl time.Duration stop chan struct{} once sync.Once } // New creates a registry. idleTTL <= 0 disables the janitor. func New(idleTTL time.Duration) *Registry { r := &Registry{ttl: idleTTL, stop: make(chan struct{})} for i := range r.shards { r.shards[i].entries = make(map[int64]*entry) } if idleTTL > 0 { go r.janitor() } return r } func (r *Registry) shardFor(h int64) *shard { return &r.shards[uint64(h)%shardCount] } // Put registers v and returns its new random handle. func (r *Registry) Put(v any) int64 { for { h := randomHandle() s := r.shardFor(h) s.mu.Lock() if _, exists := s.entries[h]; !exists { s.entries[h] = &entry{value: v, lastUsed: time.Now()} s.mu.Unlock() return h } s.mu.Unlock() } } // Get returns the value for h and refreshes its idle timer. func (r *Registry) Get(h int64) (any, error) { s := r.shardFor(h) s.mu.RLock() e, ok := s.entries[h] if ok { e.lastUsed = time.Now() } s.mu.RUnlock() if !ok { return nil, ErrNotFound } return e.value, nil } // Remove unregisters h and returns its value, closing is the caller's job. func (r *Registry) Remove(h int64) (any, error) { s := r.shardFor(h) s.mu.Lock() e, ok := s.entries[h] if ok { delete(s.entries, h) } s.mu.Unlock() if !ok { return nil, ErrNotFound } return e.value, nil } // Len reports the number of live handles. func (r *Registry) Len() int { n := 0 for i := range r.shards { s := &r.shards[i] s.mu.RLock() n += len(s.entries) s.mu.RUnlock() } return n } // Shutdown stops the janitor and closes every remaining entry. func (r *Registry) Shutdown() { r.once.Do(func() { close(r.stop) }) for i := range r.shards { s := &r.shards[i] s.mu.Lock() for h, e := range s.entries { if c, ok := e.value.(Closer); ok { _ = c.Close() } delete(s.entries, h) } s.mu.Unlock() } } func (r *Registry) janitor() { tick := time.NewTicker(r.ttl / 4) defer tick.Stop() for { select { case <-r.stop: return case <-tick.C: r.evictIdle(time.Now().Add(-r.ttl)) } } } func (r *Registry) evictIdle(deadline time.Time) { for i := range r.shards { s := &r.shards[i] var victims []Closer s.mu.Lock() for h, e := range s.entries { if e.lastUsed.Before(deadline) { if c, ok := e.value.(Closer); ok { victims = append(victims, c) } delete(s.entries, h) } } s.mu.Unlock() for _, c := range victims { _ = c.Close() } } } func randomHandle() int64 { var b [8]byte if _, err := rand.Read(b[:]); err != nil { panic(err) // crypto/rand failure is unrecoverable } h := int64(binary.LittleEndian.Uint64(b[:]) &^ (1 << 63)) if h == 0 { return 1 } return h }