PHP Classes

File: extension/limits/limits.go

Recommend this page to a friend!
  Packages of Roni   Easy-Excel   extension/limits/limits.go   Download  
File: extension/limits/limits.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: 6 days ago
Size: 3,315 bytes
 

Contents

Class file image Download
// Package limits implements load control for FrankenPHP worker mode // (PLAN.md §7): a weighted admission semaphore around heavy operations and a // process-wide memory budget acting as a circuit breaker. Overload is // surfaced to PHP as a typed error so applications can shed load (HTTP 429) // or dispatch to their existing queues instead of OOM-killing the worker. package limits import ( "context" "errors" "fmt" "os" "runtime" "strconv" "sync/atomic" "time" "golang.org/x/sync/semaphore" ) // ErrOverloaded maps to the EasyExcel\Exception\Overloaded PHP exception. var ErrOverloaded = errors.New("easy-excel: overloaded, try again later") // Gate combines the heavy-operation semaphore and the memory budget. type Gate struct { sem *semaphore.Weighted acquireTimeout time.Duration memBudget int64 memUsed atomic.Int64 } // Config controls a Gate. Zero values pick safe defaults. type Config struct { MaxConcurrent int64 // heavy ops in flight; default max(2, NumCPU) AcquireTimeout time.Duration // wait before ErrOverloaded; default 30s MemoryBudget int64 // bytes of estimated live workbook data; default 512MiB } // FromEnv reads EASY_EXCEL_MAX_CONCURRENT, EASY_EXCEL_ACQUIRE_TIMEOUT_MS and // EASY_EXCEL_MEMORY_BUDGET_MB, falling back to defaults for unset/invalid values. func FromEnv() Config { cfg := Config{} if v, err := strconv.ParseInt(os.Getenv("EASY_EXCEL_MAX_CONCURRENT"), 10, 64); err == nil && v > 0 { cfg.MaxConcurrent = v } if v, err := strconv.ParseInt(os.Getenv("EASY_EXCEL_ACQUIRE_TIMEOUT_MS"), 10, 64); err == nil && v > 0 { cfg.AcquireTimeout = time.Duration(v) * time.Millisecond } if v, err := strconv.ParseInt(os.Getenv("EASY_EXCEL_MEMORY_BUDGET_MB"), 10, 64); err == nil && v > 0 { cfg.MemoryBudget = v << 20 } return cfg } // NewGate builds a Gate from cfg, applying defaults. func NewGate(cfg Config) *Gate { if cfg.MaxConcurrent <= 0 { cfg.MaxConcurrent = int64(max(2, runtime.NumCPU())) } if cfg.AcquireTimeout <= 0 { cfg.AcquireTimeout = 30 * time.Second } if cfg.MemoryBudget <= 0 { cfg.MemoryBudget = 512 << 20 } return &Gate{ sem: semaphore.NewWeighted(cfg.MaxConcurrent), acquireTimeout: cfg.AcquireTimeout, memBudget: cfg.MemoryBudget, } } // AcquireHeavy blocks until a heavy-operation slot is free or the timeout // elapses. The returned release function must be called exactly once. func (g *Gate) AcquireHeavy() (release func(), err error) { ctx, cancel := context.WithTimeout(context.Background(), g.acquireTimeout) defer cancel() if err := g.sem.Acquire(ctx, 1); err != nil { return nil, fmt.Errorf("%w (waited %s for a free slot)", ErrOverloaded, g.acquireTimeout) } return func() { g.sem.Release(1) }, nil } // ReserveMemory accounts estimated bytes against the budget, failing fast // when the budget is exhausted. Negative deltas always succeed (release). func (g *Gate) ReserveMemory(delta int64) error { used := g.memUsed.Add(delta) if delta > 0 && used > g.memBudget { g.memUsed.Add(-delta) return fmt.Errorf("%w (memory budget %dMiB exhausted)", ErrOverloaded, g.memBudget>>20) } return nil } // MemoryUsed reports current accounted bytes (observability hook). func (g *Gate) MemoryUsed() int64 { return g.memUsed.Load() }