Skip to content

feat: load policy with a single LRANGE instead of one LINDEX per rule - #4

Merged
hsluoyz merged 1 commit into
apache:masterfrom
qjc1997:feat/load-policy-lrange
Oct 9, 2026
Merged

hsluoyz merged 1 commit into
apache:masterfrom
qjc1997:feat/load-policy-lrange

Conversation

@qjc1997

@qjc1997 qjc1997 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

What

Adapter.load_policy used LLEN followed by one LINDEX per rule:

length = self.client.llen(self.key)
for i in range(length):
    line = self.client.lindex(self.key, i)

This PR fetches the whole list with a single LRANGE key 0 -1 instead. Parsing is unchanged (json.loads → CasbinRule → persist.load_policy_line), and nothing on the write path changes.

Why

Latency. Each LINDEX is a separate client↔Redis round trip, so loading N rules costs N+1 round trips. The Redis server-side work is negligible; the time goes into waiting on the network. Measured against a managed Redis (~0.75 ms RTT) with ~2,950 rules:

time
LLEN + N × LINDEX (current) 2.22 s
the same LINDEX calls pipelined (1 round trip) 0.026 s
1 × LRANGE key 0 -1 (this PR) 0.002 s

load_policy runs when an enforcer is created and again every time policy is reloaded (watcher callback, explicit reloads), so applications pay this cost repeatedly.

Consistency. LLEN + LINDEX is not atomic. If another process removes a rule (LREM) while the loop is running, the list shrinks, the last LINDEX calls return None, and json.loads(None) raises a TypeError; inserts or removals in the middle can also skip or duplicate rules. LRANGE is a single command, so it reads a consistent snapshot of the list.

This also brings the Python adapter in line with the Go redis-adapter, whose LoadPolicy already reads the list with a single LRANGE.

Tests

Added test_load_policy_single_round_trip: it reloads the policy with lindex patched to fail and checks that lrange is called exactly once with (key, 0, -1) and that the loaded policy enforces correctly. It fails on current master (LINDEX used) and passes with this change. The full suite passes locally (10 tests), and black --check is clean.

load_policy issued LLEN and then one LINDEX per rule, so loading N rules
took N+1 client/Redis round trips. With a few thousand rules over a
~1 ms network that is several seconds per load, almost all of it spent
waiting on round trips. A single LRANGE fetches the whole list in one
round trip and returns an atomic snapshot, so a concurrent LREM can no
longer shrink the list mid-load and make LINDEX return None.

This matches the Go redis-adapter, which already loads with LRANGE.
@hsluoyz hsluoyz closed this Oct 9, 2026
@hsluoyz hsluoyz reopened this Oct 9, 2026
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 37948353873

Coverage increased (+0.07%) to 98.305%

Details

  • Coverage increased (+0.07%) from the base build.
  • Patch coverage: 12 of 12 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 236
Covered Lines: 232
Line Coverage: 98.31%
Coverage Strength: 2.95 hits per line

💛 - Coveralls

@hsluoyz
hsluoyz merged commit 0d1efc1 into apache:master Oct 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants