Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
22be928
wip: first commit
Dr-Emann Jan 23, 2026
447836a
benchmarks
Dr-Emann Jan 24, 2026
79c3a9d
no need to run the automaton before we go through the possible matche…
Dr-Emann Jan 24, 2026
6788eac
un-traitify MatcherVisitor
Dr-Emann Jan 24, 2026
32188a1
extract inner prefilter
Dr-Emann Jan 24, 2026
d12a0db
add btree prefilter
Dr-Emann Jan 25, 2026
12aa563
add fst impl
Dr-Emann Jan 25, 2026
b173d30
make btree the only inner prefilter
Dr-Emann Jan 25, 2026
9c2ddab
chore: cleanup
Dr-Emann Jan 26, 2026
639fa3c
feat: much cleaner debug thanks to bstr
Dr-Emann Jan 26, 2026
c1b227a
perf: pre-compute all prefix patterns
Dr-Emann Jan 27, 2026
aca8d96
simplfy iterator
Dr-Emann Jan 27, 2026
d64cd2a
chore: clippy findings
Dr-Emann Jan 27, 2026
669016a
docs and more
Dr-Emann Jan 27, 2026
85b5b05
feat: make online
Dr-Emann Jan 30, 2026
37c893d
add can_prefilter method
Dr-Emann Jan 30, 2026
9318af0
optimize skipping to smaller prefixes
Dr-Emann Jan 30, 2026
e9d8138
add some more helper functions
Dr-Emann Feb 2, 2026
4dadcf0
refactor: move matcher types into a module
Dr-Emann Feb 2, 2026
35a533b
refactor some more things
Dr-Emann Feb 2, 2026
c4135a3
cargo fmt
Dr-Emann Feb 2, 2026
cd165e5
cargo clippy pedantic fixes
Dr-Emann Feb 2, 2026
792e0e4
Add more tests
Dr-Emann Feb 2, 2026
b4c3062
Clippy in tests
Dr-Emann Feb 2, 2026
eb41633
update Cargo.toml with fields for publishing
Dr-Emann Feb 2, 2026
42d3a37
enable release-plz
Dr-Emann Feb 2, 2026
25ca519
ci: updates for package repo
Dr-Emann Feb 2, 2026
2081e88
chore: release v1.0.0
Dr-Emann Feb 2, 2026
4148e9f
Update docs
Dr-Emann Feb 2, 2026
fc8ca19
chore: release v1.0.2
Dr-Emann Feb 2, 2026
b17bfdb
feat: add RouterPrefilter::clear method
Dr-Emann Feb 3, 2026
3c7dc98
chore: release v1.1.0
Dr-Emann Feb 3, 2026
c030897
benchmarks: better demonstrate matching scaling
Dr-Emann Feb 3, 2026
36121d2
feat: allow enabling a prefilter on a string field
Dr-Emann Jan 23, 2026
597df9e
perf: optimize insert
Dr-Emann Feb 6, 2026
1586e96
chore: release v1.1.1
Dr-Emann Feb 6, 2026
c4003e6
fix: handle duplicate key insertions
Dr-Emann Feb 6, 2026
864382b
docs: fix compilation of readme example by using as the library doc c…
Dr-Emann Feb 6, 2026
5188109
feat: implement more iterator traits for RouterPrefilterIter
Dr-Emann Feb 6, 2026
6fe2ab8
chore: release v1.2.0
Dr-Emann Feb 6, 2026
caf79c9
perf: replace BTreeMap-based implmementation with a radix trie
Dr-Emann Feb 7, 2026
4fb8e67
chore: release v1.3.0
Dr-Emann Feb 8, 2026
c204bff
chore: release v1.3.0
Dr-Emann Feb 8, 2026
2b2359f
bench: add worst-case benches for prefilters
Dr-Emann Feb 5, 2026
23da506
chore(deps): update rand to published 0.10 version in dev dependencies
Dr-Emann Feb 13, 2026
6aaf65e
chore: release v1.3.1
Dr-Emann Feb 13, 2026
93b651e
Add 'crates/atc_router_prefilter/' from commit '6aaf65e95ca5043ae8e6c…
Dr-Emann Feb 13, 2026
48bf2a8
update references to include `atc_` prefix
Dr-Emann Feb 13, 2026
8ac70c9
chore: use forked router_prefilter library
Dr-Emann Feb 13, 2026
0e1ecc5
Merge branch 'main' into prefilter
Dr-Emann Feb 13, 2026
0ab013e
docs: add more documentation on how to call the visitor functions
Dr-Emann Feb 16, 2026
c091452
chore: release v1.3.2
Dr-Emann Feb 16, 2026
6aac61a
Merge commit '0ab013edda34d8fb44d083ad7419a2091b99ed60' into prefilter
Dr-Emann Feb 16, 2026
2016282
docs: update readme with added API
Dr-Emann Feb 17, 2026
942388c
style: cargo fmt
Dr-Emann Feb 26, 2026
f480e32
docs: add docs about what intersecting with prefix expansion means/does
Dr-Emann Mar 3, 2026
1045392
tests: add unit tests for intersect_prefix_expansions_into function
Dr-Emann Mar 3, 2026
545b5c1
chore: release v1.3.3
Dr-Emann Mar 3, 2026
fa91218
Merge commit '545b5c14f5544522f968caf022e26fa97ac8380e' into prefilter
Dr-Emann Mar 4, 2026
f651064
extra comment explaining why we reverse
Dr-Emann Mar 4, 2026
37a274d
fix: don't panic for incorrect prefilter enable calls
Dr-Emann Mar 13, 2026
446287a
fix: clippy warning for cloning a Copy type
Dr-Emann Mar 13, 2026
4bcbb5c
tests: enable prefilter on http.path routers in existing tests
Dr-Emann Mar 13, 2026
22b3bb2
tests: add tests for errors from enabling prefilter
Dr-Emann Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ serde = { version = "1.0", features = ["derive"], optional = true }
serde_regex = { version = "1.1", optional = true }
fnv = "1"
bitflags = { version = "2.6", optional = true }
atc_router_prefilter = { version = "1.0", path = "crates/atc_router_prefilter" }

[dev-dependencies]
criterion = "0"

[lib]
crate-type = ["lib", "cdylib", "staticlib"]
bench = false

[features]
default = ["ffi"]
Expand All @@ -42,3 +44,10 @@ harness = false
[[bench]]
name = "match_mix"
harness = false

[[bench]]
name = "worst_case"
harness = false

[workspace]
members = ["crates/*"]
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ ATC Router library for Kong.
* [new](#new)
* [add\_matcher](#add_matcher)
* [remove\_matcher](#remove_matcher)
* [enable\_prefilter](#enable_prefilter)
* [disable\_prefilter](#disable_prefilter)
* [execute](#execute)
* [get\_fields](#get_fields)
* [validate](#validate)
Expand Down Expand Up @@ -165,6 +167,36 @@ matcher does not exist.

[Back to TOC](#table-of-contents)

### enable\_prefilter

**syntax:** *res, err = r:enable_prefilter(field)*

**context:** *any*

Enables prefiltering on the specified `field`. The field must be of type `String`
in the router's schema. When enabled, the router uses the prefilter to narrow down
candidate matchers before performing full evaluation, which can improve match
performance.

Calling this method requires building a prefilter for all current matchers: it may be
expensive. It is expected that this method will be called only once for a router. The
prefilter will be kept up to date with any added and removed matchers, until this method
is called again with a different field name, or the disable_prefilter method is called.

If an error occurred, `nil` and a string describing the error will be returned.

[Back to TOC](#table-of-contents)

### disable\_prefilter

**syntax:** *r:disable_prefilter()*

**context:** *any*

Disables prefiltering on the router, reverting to the default matching behavior.

[Back to TOC](#table-of-contents)

### execute

**syntax:** *res, err = r:execute(context)*
Expand Down
51 changes: 39 additions & 12 deletions benches/build.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use atc_router::ast::Type;
use atc_router::router::Router;
use atc_router::schema::Schema;
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use uuid::Uuid;

// To run this benchmark, execute the following command:
// ```shell
// cargo bench --bench build
// ```

const N: usize = 1000;
const N: usize = 5000;

fn make_uuid(a: usize) -> String {
format!("8cb2a7d0-c775-4ed9-989f-{:012}", a)
Expand All @@ -24,23 +24,50 @@ fn criterion_benchmark(c: &mut Criterion) {
let uuid = make_uuid(i);
let uuid = Uuid::try_from(uuid.as_str()).unwrap();

let expr = format!("((a > 0 || a < {}) && a != 0) && a == 1", N + 1);
let expr = format!(
"((a > 0 || a < {0}) && a != 0) && a == 1 && b == \"{0}\"",
N + 1
);

data.push((priority, uuid, expr))
}

let mut schema = Schema::default();
schema.add_field("a", Type::Int);
schema.add_field("b", Type::String);

c.bench_function("Build Router", |b| {
b.iter_with_large_drop(|| {
let mut router = Router::new(&schema);
for v in &data {
router.add_matcher(v.0, v.1, &v.2).unwrap();
}
router
});
});
let mut g = c.benchmark_group("build router");
for n in [1, 10, 100, 500, 1000, 3000, N] {
g.throughput(Throughput::Elements(n as u64));
g.bench_with_input(
BenchmarkId::new("without prefilter", n),
&data[..n],
|b, data| {
b.iter_with_large_drop(|| {
let mut router = Router::new(&schema);
for v in data {
router.add_matcher(v.0, v.1, &v.2).unwrap();
}
router
});
},
);

g.bench_with_input(
BenchmarkId::new("with prefilter", n),
&data[..n],
|b, data| {
b.iter_with_large_drop(|| {
let mut router = Router::new(&schema);
router.enable_prefilter("b");
for v in data {
router.add_matcher(v.0, v.1, &v.2).unwrap();
}
router
});
},
);
}
}

criterion_group!(benches, criterion_benchmark);
Expand Down
58 changes: 32 additions & 26 deletions benches/match_mix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ use atc_router::ast::{Type, Value};
use atc_router::context::Context;
use atc_router::router::Router;
use atc_router::schema::Schema;
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use uuid::Uuid;

// To run this benchmark, execute the following command:
// ```shell
// cargo bench --bench match_mix
// ```

const N: usize = 100_000;

fn make_uuid(a: usize) -> String {
Expand All @@ -26,8 +21,8 @@ fn criterion_benchmark(c: &mut Criterion) {

for i in 0..N {
let expr = format!(
r#"(http.path == "hello{}" && http.version == "1.1") || {} || {} || {}"#,
i, "!((a == 2) && (a == 9))", "!(a == 1)", "(a == 3 && a == 4) && !(a == 5)"
r#"(http.path == "hello{}" && http.version == "1.1") && ({} || {} || {}) && {}"#,
i, "!((a == 2) && (a == 9))", "!(a == 1)", "(a == 3 && a == 4)", "!(a == 5)"
);

let uuid = make_uuid(i);
Expand All @@ -36,33 +31,44 @@ fn criterion_benchmark(c: &mut Criterion) {
router.add_matcher(N - i, uuid, &expr).unwrap();
}

let mut g = c.benchmark_group("match_mix");

let mut ctx = Context::new(&schema);

// match benchmark
ctx.add_value("http.path", Value::String("hello49999".to_string()));
ctx.add_value("http.version", Value::String("1.1".to_string()));
ctx.add_value("a", Value::Int(3_i64));
const NUMBERS: &[usize] = &[0, 10, 1_000, 10_000, 40_000, 70_000, N - 1, N + 1];
for &i in NUMBERS {
ctx.reset();
ctx.add_value("http.path", Value::String(format!("hello{}", i)));
ctx.add_value("http.version", Value::String("1.1".to_string()));
ctx.add_value("a", Value::Int(3_i64));

c.bench_function("Match", |b| {
b.iter(|| {
let is_match = router.execute(&mut ctx);
assert!(is_match);
let expected_match = i < N;
g.bench_with_input(BenchmarkId::new("without prefilter", i), &i, |b, _| {
b.iter(|| {
let is_match = router.execute(&mut ctx);
assert_eq!(is_match, expected_match);
});
});
});
}

ctx.reset();
router.enable_prefilter("http.path");

// not match benchmark
ctx.add_value("http.path", Value::String("hello49999".to_string()));
ctx.add_value("http.version", Value::String("1.1".to_string()));
ctx.add_value("a", Value::Int(5_i64)); // not match
for &i in NUMBERS {
ctx.reset();
ctx.add_value("http.path", Value::String(format!("hello{}", i)));
ctx.add_value("http.version", Value::String("1.1".to_string()));
ctx.add_value("a", Value::Int(3_i64));

c.bench_function("Doesn't Match", |b| {
b.iter(|| {
let not_match = !router.execute(&mut ctx);
assert!(not_match);
let expected_match = i < N;
g.bench_with_input(BenchmarkId::new("with prefilter", i), &i, |b, _| {
b.iter(|| {
let is_match = router.execute(&mut ctx);
assert_eq!(is_match, expected_match);
});
});
});
}
g.finish();
}

criterion_group!(benches, criterion_benchmark);
Expand Down
104 changes: 104 additions & 0 deletions benches/worst_case.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use atc_router::ast::{Type, Value};
use atc_router::context::Context;
use atc_router::router::Router;
use atc_router::schema::Schema;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use uuid::Uuid;

fn make_uuid(a: usize) -> String {
format!("8cb2a7d0-c775-4ed9-989f-{:012}", a)
}

fn worst_case(c: &mut Criterion) {
const MAX: usize = 2_000;
let mut schema = Schema::default();
schema.add_field("http.path", Type::String);

// This is the worst case for the prefilter, we end up finding prefixes as
// '/a', '/a/a', '/a/a/a', etc, which means a string that starts with `/a/a/a/...` also
// starts with `/a/a/...`, and also starts with `/a/...` etc.
let all_matchers: Vec<(usize, Uuid, String)> = (0..MAX)
.map(|i| {
let expression_str = format!("http.path == r#\"{}\"#", "/a".repeat(i));
(MAX - i, make_uuid(i).parse().unwrap(), expression_str)
})
.collect();

let mut g = c.benchmark_group("worst-case build");
for n in [1, 10, 100, 500, 1000, MAX] {
// Get the _last_ matchers, to avoid both growing the number of matchers, and the size of
// the matchers
let matchers = all_matchers.windows(n).last().unwrap();
g.throughput(Throughput::Elements(n as u64));
g.bench_with_input(
BenchmarkId::new("without prefilter", n),
matchers,
|b, matchers| {
b.iter_with_large_drop(|| {
let mut router = Router::new(&schema);
for &(priority, uuid, ref expression) in matchers {
router.add_matcher(priority, uuid, expression).unwrap();
}
router
});
},
);

g.bench_with_input(
BenchmarkId::new("with prefilter", n),
matchers,
|b, matchers| {
b.iter_with_large_drop(|| {
let mut router = Router::new(&schema);
router.enable_prefilter("http.path");
for &(priority, uuid, ref expression) in matchers {
router.add_matcher(priority, uuid, expression).unwrap();
}
router
});
},
);
}
g.finish();

let mut router = Router::new(&schema);
for (priority, uuid, expression) in all_matchers {
router.add_matcher(priority, uuid, &expression).unwrap();
}
let mut ctx = Context::new(&schema);
let mut g = c.benchmark_group("worst-case match");
for n in [1, 10, 100, 500, 1000, MAX] {
ctx.reset();
ctx.add_value("http.path", Value::String("/a".repeat(n - 1)));
g.bench_with_input(
BenchmarkId::new("without prefilter", n),
&router,
|b, router| {
b.iter(|| {
let found = router.execute(&mut ctx);
assert!(found);
found
})
},
);
}
router.enable_prefilter("http.path");
for n in [1, 10, 100, 500, 1000, MAX] {
ctx.reset();
ctx.add_value("http.path", Value::String("/a".repeat(n - 1)));
g.bench_with_input(
BenchmarkId::new("with prefilter", n),
&router,
|b, router| {
b.iter(|| {
let found = router.execute(&mut ctx);
assert!(found);
found
})
},
);
}
}

criterion_group!(benches, worst_case);
criterion_main!(benches);
1 change: 1 addition & 0 deletions crates/atc_router_prefilter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
Loading
Loading