This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin.git
The following commit(s) were added to refs/heads/master by this push:
new 5d4c147 feat: add EnforcerPool for filtered enforcers
5d4c147 is described below
commit 5d4c14788fc0234ae24756f52d75f60935dda12a
Author: Yang Luo <[email protected]>
AuthorDate: Sun Nov 1 02:25:14 2026 +0800
feat: add EnforcerPool for filtered enforcers
---
README.md | 24 ++++
enforcer_pool.go | 209 +++++++++++++++++++++++++++++++
enforcer_pool_test.go | 332 ++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 565 insertions(+)
diff --git a/README.md b/README.md
index c9747dd..f4a46f7 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,7 @@ Apache Casbin is a powerful and efficient open-source access
control library for
- [Policy management](#policy-management)
- [Policy persistence](#policy-persistence)
- [Policy consistence between multiple
nodes](#policy-consistence-between-multiple-nodes)
+- [Enforcer pooling](#enforcer-pooling)
- [Role manager](#role-manager)
- [Benchmarks](#benchmarks)
- [Examples](#examples)
@@ -229,6 +230,29 @@ https://casbin.apache.org/docs/watchers
https://casbin.apache.org/docs/production-deployment
+## Enforcer pooling
+
+If your application creates a filtered enforcer for every request, for example
one enforcer per tenant, use `EnforcerPool` to reuse one enforcer per key and
bound the cache with an LRU:
+
+```go
+// newAdapter creates a new filtered adapter for each enforcer.
+pool := casbin.NewFilteredEnforcerPool("model.conf", newAdapter, 1024)
+
+e, err := pool.Get(tenantID, []string{tenantID})
+if err != nil {
+ return err
+}
+allowed, err := e.Enforce("alice", "data1", "read")
+
+// After a policy change made outside this enforcer:
+pool.Invalidate(tenantID)
+
+// In a watcher callback:
+watcher.SetUpdateCallback(pool.OnUpdate)
+```
+
+`NewFilteredEnforcerPool` calls `newAdapter` once per enforcer, so filtered
adapters that keep state are not shared. The filter passed to `Get` must be the
type your adapter expects; use `NewEnforcerPool` with your own factory for
other filter types. A `maxSize` of zero or less means no limit.
+
## Role manager
https://casbin.apache.org/docs/role-managers
diff --git a/enforcer_pool.go b/enforcer_pool.go
new file mode 100644
index 0000000..18691b2
--- /dev/null
+++ b/enforcer_pool.go
@@ -0,0 +1,209 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 casbin
+
+import (
+ "container/list"
+ "errors"
+ "sync"
+
+ "github.com/casbin/casbin/v3/model"
+ "github.com/casbin/casbin/v3/persist"
+)
+
+// EnforcerFactory creates an enforcer for a cache entry. It is called only on
+// a cache miss. The enforcer is shared by every caller that uses the same key,
+// so it must be safe for concurrent use.
+type EnforcerFactory func(key string, filter interface{}) (*SyncedEnforcer,
error)
+
+// EnforcerPool caches enforcers by key and bounds the cache with an LRU.
+// It is intended for applications that would otherwise create a filtered
+// enforcer for every request, for example one enforcer per tenant.
+//
+// The key must uniquely identify the filter: a cache hit returns the cached
+// enforcer without calling the factory, even if a different filter is passed.
+// Call Invalidate after a policy change that was not made through the cached
+// enforcer, and call InvalidateAll from a watcher callback to drop enforcers
+// built from a stale policy.
+type EnforcerPool struct {
+ mu sync.Mutex
+ factory EnforcerFactory
+ maxSize int
+ generation uint64
+ enforcers map[string]*enforcerPoolEntry
+ lru *list.List
+}
+
+type enforcerPoolEntry struct {
+ enforcer *SyncedEnforcer
+ elem *list.Element
+}
+
+var (
+ errNilEnforcerFactory = errors.New("casbin: nil enforcer factory")
+ errNilEnforcer = errors.New("casbin: enforcer factory returned
nil")
+ errNilAdapterFactory = errors.New("casbin: nil adapter factory")
+)
+
+// NewEnforcerPool creates a pool that keeps at most maxSize enforcers.
+// A maxSize of zero or less means no limit.
+func NewEnforcerPool(factory EnforcerFactory, maxSize int) *EnforcerPool {
+ return &EnforcerPool{
+ factory: factory,
+ maxSize: maxSize,
+ enforcers: make(map[string]*enforcerPoolEntry),
+ lru: list.New(),
+ }
+}
+
+// NewFilteredEnforcerPool creates a pool that builds one filtered enforcer per
+// key. newAdapter is called once per enforcer, so adapters that keep filter
+// state are not shared. The filter passed to Get must be the type the adapter
+// expects.
+func NewFilteredEnforcerPool(modelPath string, newAdapter func()
persist.FilteredAdapter, maxSize int) *EnforcerPool {
+ return NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ if newAdapter == nil {
+ return nil, errNilAdapterFactory
+ }
+
+ m, err := model.NewModelFromFile(modelPath)
+ if err != nil {
+ return nil, err
+ }
+
+ e, err := NewSyncedEnforcer(m)
+ if err != nil {
+ return nil, err
+ }
+
+ e.SetAdapter(newAdapter())
+ if err := e.LoadFilteredPolicy(filter); err != nil {
+ return nil, err
+ }
+
+ return e, nil
+ }, maxSize)
+}
+
+// Get returns the cached enforcer for key, calling the factory on a cache
+// miss. filter is passed to the factory unchanged.
+func (p *EnforcerPool) Get(key string, filter interface{}) (*SyncedEnforcer,
error) {
+ if p.factory == nil {
+ return nil, errNilEnforcerFactory
+ }
+
+ for {
+ p.mu.Lock()
+ p.initLocked()
+ if entry, ok := p.enforcers[key]; ok {
+ p.lru.MoveToFront(entry.elem)
+ e := entry.enforcer
+ p.mu.Unlock()
+ return e, nil
+ }
+ generation := p.generation
+ p.mu.Unlock()
+
+ e, err := p.factory(key, filter)
+ if err != nil {
+ return nil, err
+ }
+ if e == nil {
+ return nil, errNilEnforcer
+ }
+
+ p.mu.Lock()
+ // The policy changed while the factory was loading, so the
enforcer
+ // may have been built from stale data. Build it again.
+ if generation != p.generation {
+ p.mu.Unlock()
+ continue
+ }
+ if entry, ok := p.enforcers[key]; ok {
+ p.lru.MoveToFront(entry.elem)
+ e = entry.enforcer
+ p.mu.Unlock()
+ return e, nil
+ }
+
+ entry := &enforcerPoolEntry{enforcer: e}
+ entry.elem = p.lru.PushFront(key)
+ p.enforcers[key] = entry
+ p.evictLocked()
+ p.mu.Unlock()
+ return e, nil
+ }
+}
+
+// Invalidate drops the enforcer cached for key.
+func (p *EnforcerPool) Invalidate(key string) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ p.initLocked()
+ p.generation++
+ if entry, ok := p.enforcers[key]; ok {
+ delete(p.enforcers, key)
+ p.lru.Remove(entry.elem)
+ }
+}
+
+// InvalidateAll drops every cached enforcer.
+func (p *EnforcerPool) InvalidateAll() {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ p.initLocked()
+ p.generation++
+ p.enforcers = make(map[string]*enforcerPoolEntry)
+ p.lru.Init()
+}
+
+// OnUpdate invalidates every cached enforcer. Its signature matches the
+// callback passed to persist.Watcher.SetUpdateCallback.
+func (p *EnforcerPool) OnUpdate(string) {
+ p.InvalidateAll()
+}
+
+// Len returns the number of cached enforcers.
+func (p *EnforcerPool) Len() int {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ return len(p.enforcers)
+}
+
+func (p *EnforcerPool) initLocked() {
+ if p.enforcers == nil {
+ p.enforcers = make(map[string]*enforcerPoolEntry)
+ }
+ if p.lru == nil {
+ p.lru = list.New()
+ }
+}
+
+func (p *EnforcerPool) evictLocked() {
+ if p.maxSize <= 0 {
+ return
+ }
+ for len(p.enforcers) > p.maxSize {
+ back := p.lru.Back()
+ if back == nil {
+ return
+ }
+ delete(p.enforcers, back.Value.(string))
+ p.lru.Remove(back)
+ }
+}
diff --git a/enforcer_pool_test.go b/enforcer_pool_test.go
new file mode 100644
index 0000000..65d259c
--- /dev/null
+++ b/enforcer_pool_test.go
@@ -0,0 +1,332 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 casbin
+
+import (
+ "errors"
+ "strconv"
+ "sync"
+ "sync/atomic"
+ "testing"
+
+ "github.com/casbin/casbin/v3/persist"
+ fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
+)
+
+func newTestPoolEnforcer() (*SyncedEnforcer, error) {
+ return NewSyncedEnforcer("examples/basic_model.conf",
"examples/basic_policy.csv")
+}
+
+func TestEnforcerPoolGetCaches(t *testing.T) {
+ calls := 0
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ calls++
+ return newTestPoolEnforcer()
+ }, 10)
+
+ e1, err := pool.Get("tenant1", nil)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ e2, err := pool.Get("tenant1", nil)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+
+ if calls != 1 {
+ t.Fatalf("factory calls = %d, want 1", calls)
+ }
+ if e1 != e2 {
+ t.Fatal("Get returned different enforcers for the same key")
+ }
+ if pool.Len() != 1 {
+ t.Fatalf("Len = %d, want 1", pool.Len())
+ }
+}
+
+func TestEnforcerPoolDifferentKeys(t *testing.T) {
+ calls := 0
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ calls++
+ return newTestPoolEnforcer()
+ }, 10)
+
+ e1, err := pool.Get("tenant1", nil)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ e2, err := pool.Get("tenant2", nil)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+
+ if calls != 2 {
+ t.Fatalf("factory calls = %d, want 2", calls)
+ }
+ if e1 == e2 {
+ t.Fatal("Get returned the same enforcer for different keys")
+ }
+ if pool.Len() != 2 {
+ t.Fatalf("Len = %d, want 2", pool.Len())
+ }
+}
+
+func TestEnforcerPoolInvalidate(t *testing.T) {
+ calls := 0
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ calls++
+ return newTestPoolEnforcer()
+ }, 10)
+
+ e1, err := pool.Get("tenant1", nil)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+
+ pool.Invalidate("tenant1")
+ if pool.Len() != 0 {
+ t.Fatalf("Len = %d, want 0", pool.Len())
+ }
+
+ e2, err := pool.Get("tenant1", nil)
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if calls != 2 {
+ t.Fatalf("factory calls = %d, want 2", calls)
+ }
+ if e1 == e2 {
+ t.Fatal("Get returned the invalidated enforcer")
+ }
+
+ pool.Invalidate("missing")
+}
+
+func TestEnforcerPoolInvalidateAll(t *testing.T) {
+ calls := 0
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ calls++
+ return newTestPoolEnforcer()
+ }, 10)
+
+ if _, err := pool.Get("tenant1", nil); err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if _, err := pool.Get("tenant2", nil); err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+
+ pool.OnUpdate("policy changed")
+ if pool.Len() != 0 {
+ t.Fatalf("Len = %d, want 0", pool.Len())
+ }
+
+ if _, err := pool.Get("tenant1", nil); err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if calls != 3 {
+ t.Fatalf("factory calls = %d, want 3", calls)
+ }
+}
+
+func TestEnforcerPoolLRU(t *testing.T) {
+ calls := 0
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ calls++
+ return newTestPoolEnforcer()
+ }, 2)
+
+ get := func(key string) *SyncedEnforcer {
+ e, err := pool.Get(key, nil)
+ if err != nil {
+ t.Fatalf("Get(%q): %v", key, err)
+ }
+ return e
+ }
+
+ get("a")
+ get("b")
+ get("a")
+ get("c")
+ get("b")
+ get("c")
+
+ if calls != 4 {
+ t.Fatalf("factory calls = %d, want 4", calls)
+ }
+ if pool.Len() != 2 {
+ t.Fatalf("Len = %d, want 2", pool.Len())
+ }
+}
+
+func TestEnforcerPoolUnbounded(t *testing.T) {
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ return newTestPoolEnforcer()
+ }, 0)
+
+ for i := 0; i < 50; i++ {
+ if _, err := pool.Get(strconv.Itoa(i), nil); err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ }
+ if pool.Len() != 50 {
+ t.Fatalf("Len = %d, want 50", pool.Len())
+ }
+}
+
+func TestEnforcerPoolFactoryError(t *testing.T) {
+ boom := errors.New("boom")
+ calls := 0
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ calls++
+ return nil, boom
+ }, 10)
+
+ if _, err := pool.Get("tenant1", nil); !errors.Is(err, boom) {
+ t.Fatalf("Get error = %v, want %v", err, boom)
+ }
+ if pool.Len() != 0 {
+ t.Fatalf("Len = %d, want 0", pool.Len())
+ }
+
+ if _, err := pool.Get("tenant1", nil); !errors.Is(err, boom) {
+ t.Fatalf("Get error = %v, want %v", err, boom)
+ }
+ if calls != 2 {
+ t.Fatalf("factory calls = %d, want 2", calls)
+ }
+}
+
+func TestEnforcerPoolNilFactory(t *testing.T) {
+ pool := NewEnforcerPool(nil, 10)
+ if _, err := pool.Get("tenant1", nil); err != errNilEnforcerFactory {
+ t.Fatalf("Get error = %v, want %v", err, errNilEnforcerFactory)
+ }
+}
+
+func TestEnforcerPoolNilEnforcer(t *testing.T) {
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ return nil, nil
+ }, 10)
+ if _, err := pool.Get("tenant1", nil); err != errNilEnforcer {
+ t.Fatalf("Get error = %v, want %v", err, errNilEnforcer)
+ }
+}
+
+func TestEnforcerPoolConcurrentGet(t *testing.T) {
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ return newTestPoolEnforcer()
+ }, 10)
+
+ const n = 100
+ results := make([]*SyncedEnforcer, n)
+ var wg sync.WaitGroup
+ wg.Add(n)
+ for i := 0; i < n; i++ {
+ go func(i int) {
+ defer wg.Done()
+ e, err := pool.Get("tenant1", nil)
+ if err != nil {
+ t.Errorf("Get: %v", err)
+ return
+ }
+ results[i] = e
+ }(i)
+ }
+ wg.Wait()
+
+ if t.Failed() {
+ return
+ }
+ for i := 1; i < n; i++ {
+ if results[i] != results[0] {
+ t.Fatalf("Get returned different enforcers for the same
key")
+ }
+ }
+ if pool.Len() != 1 {
+ t.Fatalf("Len = %d, want 1", pool.Len())
+ }
+}
+
+func TestEnforcerPoolInvalidateDuringLoad(t *testing.T) {
+ var calls int32
+ started := make(chan struct{})
+ release := make(chan struct{})
+ pool := NewEnforcerPool(func(key string, filter interface{})
(*SyncedEnforcer, error) {
+ if atomic.AddInt32(&calls, 1) == 1 {
+ close(started)
+ <-release
+ }
+ return newTestPoolEnforcer()
+ }, 10)
+
+ go func() {
+ <-started
+ pool.InvalidateAll()
+ close(release)
+ }()
+
+ if _, err := pool.Get("tenant1", nil); err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+ if got := atomic.LoadInt32(&calls); got != 2 {
+ t.Fatalf("factory calls = %d, want 2", got)
+ }
+ if pool.Len() != 1 {
+ t.Fatalf("Len = %d, want 1", pool.Len())
+ }
+}
+
+func TestNewFilteredEnforcerPool(t *testing.T) {
+ pool := NewFilteredEnforcerPool(
+ "examples/rbac_with_domains_model.conf",
+ func() persist.FilteredAdapter {
+ return
fileadapter.NewFilteredAdapter("examples/rbac_with_domains_policy.csv")
+ },
+ 10,
+ )
+
+ e, err := pool.Get("domain1", &fileadapter.Filter{P: []string{"",
"domain1"}})
+ if err != nil {
+ t.Fatalf("Get: %v", err)
+ }
+
+ policy, err := e.GetPolicy()
+ if err != nil {
+ t.Fatalf("GetPolicy: %v", err)
+ }
+ if len(policy) != 2 {
+ t.Fatalf("policy length = %d, want 2", len(policy))
+ }
+
+ allowed, err := e.Enforce("admin", "domain1", "data1", "read")
+ if err != nil {
+ t.Fatalf("Enforce: %v", err)
+ }
+ if !allowed {
+ t.Fatal("domain1 policy was not loaded")
+ }
+
+ allowed, err = e.Enforce("admin", "domain2", "data2", "read")
+ if err != nil {
+ t.Fatalf("Enforce: %v", err)
+ }
+ if allowed {
+ t.Fatal("domain2 policy was loaded by the domain1 enforcer")
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]