Skip to content

Commit

Permalink
Add method router.
Browse files Browse the repository at this point in the history
  • Loading branch information
CathalMullan committed Dec 7, 2024
1 parent 9e7b5b5 commit e8c0692
Show file tree
Hide file tree
Showing 56 changed files with 62,058 additions and 8,713 deletions.
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Architecture
53 changes: 53 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ hashbrown = "0.15"
smallvec = { version = "1.13", features = ["const_generics", "union"] }

[dev-dependencies]
wayfind-rails-macro = { path = "examples/rails-macro" }

# Testing
# NOTE: Keep in sync with `cargo-insta` Nix package.
insta = "=1.41.1"
Expand All @@ -100,6 +102,13 @@ similar-asserts = "1.6"
# Encoding
percent-encoding = "2.3"

# Serde
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Regex
fancy-regex = "0.14"

# Benchmarking
divan = "0.1"
criterion = { version = "0.5", features = ["html_reports"] }
Expand Down
62 changes: 31 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,26 +384,26 @@ use wayfind::{Router, RouteBuilder};

const ROUTER_DISPLAY: &str = "
/
├─ user [*]
├─ user [8]
│ ╰─ /
│ ├─ createWithList [*]
│ ├─ createWithList [9]
│ ├─ log
│ │ ├─ out [*]
│ │ ╰─ in [*]
│ ╰─ {username} [*]
├─ pet [*]
│ │ ├─ out [11]
│ │ ╰─ in [10]
│ ╰─ {username} [12]
├─ pet [0]
│ ╰─ /
│ ├─ findBy
│ │ ├─ Status [*]
│ │ ╰─ Tags [*]
│ ╰─ {petId} [*]
│ ╰─ /uploadImage [*]
│ │ ├─ Status [1]
│ │ ╰─ Tags [2]
│ ╰─ {petId} [3]
│ ╰─ /uploadImage [4]
├─ store/
│ ├─ inventory [*]
│ ╰─ order [*]
│ ├─ inventory [5]
│ ╰─ order [6]
│ ╰─ /
│ ╰─ {orderId} [*]
╰─ {*catch_all} [*]
│ ╰─ {orderId} [7]
╰─ {*catch_all} [13]
";

fn main() -> Result<(), Box<dyn Error>> {
Expand Down Expand Up @@ -493,7 +493,7 @@ Benchmarks, especially micro-benchmarks, should be taken with a grain of salt.

### Benchmarks

All benchmarks ran on a M1 Pro laptop running Asahi Linux.
All benchmarks ran on a M1 Pro laptop.

Check out our [codspeed results](https://codspeed.io/DuskSystems/wayfind/benchmarks) for a more accurate set of timings.

Expand All @@ -512,29 +512,29 @@ In a router of 130 routes, benchmark matching 4 paths.

| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size |
|:-----------------|----------:|------------:|-----------:|--------------:|-------------:|
| wayfind | 398.23 ns | 5 | 329 B | 5 | 329 B |
| path-tree | 579.66 ns | 5 | 480 B | 5 | 512 B |
| matchit | 582.72 ns | 5 | 480 B | 5 | 512 B |
| xitca-router | 660.95 ns | 8 | 864 B | 8 | 896 B |
| ntex-router | 2.2411 µs | 19 | 1.312 KB | 19 | 1.344 KB |
| route-recognizer | 3.2239 µs | 161 | 8.569 KB | 161 | 8.601 KB |
| routefinder | 6.2734 µs | 68 | 5.088 KB | 68 | 5.12 KB |
| actix-router | 21.459 µs | 215 | 14 KB | 215 | 14.03 KB |
| wayfind | 341.71 ns | 4 | 265 B | 4 | 265 B |
| matchit | 376.67 ns | 4 | 416 B | 4 | 448 B |
| xitca-router | 415.14 ns | 7 | 800 B | 7 | 832 B |
| path-tree | 442.06 ns | 4 | 416 B | 4 | 448 B |
| ntex-router | 1.7614 µs | 18 | 1.248 KB | 18 | 1.28 KB |
| route-recognizer | 2.0531 µs | 160 | 8.505 KB | 160 | 8.537 KB |
| routefinder | 4.7332 µs | 67 | 5.024 KB | 67 | 5.056 KB |
| actix-router | 17.897 µs | 214 | 13.93 KB | 214 | 13.96 KB |

#### `path-tree` inspired benches

In a router of 320 routes, benchmark matching 80 paths.

| Library | Time | Alloc Count | Alloc Size | Dealloc Count | Dealloc Size |
|:-----------------|----------:|------------:|-----------:|--------------:|-------------:|
| wayfind | 5.3596 µs | 60 | 3.847 KB | 60 | 3.847 KB |
| path-tree | 8.6949 µs | 60 | 8.727 KB | 60 | 8.75 KB |
| matchit | 10.130 µs | 141 | 19.09 KB | 141 | 19.11 KB |
| xitca-router | 11.984 µs | 210 | 26.79 KB | 210 | 26.81 KB |
| ntex-router | 36.829 µs | 202 | 20.82 KB | 202 | 20.84 KB |
| route-recognizer | 70.734 µs | 2873 | 192.9 KB | 2873 | 206.1 KB |
| routefinder | 87.920 µs | 526 | 49.68 KB | 526 | 49.71 KB |
| actix-router | 189.66 µs | 2202 | 130.1 KB | 2202 | 130.1 KB |
| wayfind | 5.1040 µs | 59 | 2.567 KB | 59 | 2.567 KB |
| matchit | 6.4678 µs | 140 | 17.81 KB | 140 | 17.83 KB |
| path-tree | 7.0941 µs | 59 | 7.447 KB | 59 | 7.47 KB |
| xitca-router | 7.5814 µs | 209 | 25.51 KB | 209 | 25.53 KB |
| ntex-router | 31.100 µs | 201 | 19.54 KB | 201 | 19.56 KB |
| route-recognizer | 54.829 µs | 2872 | 191.7 KB | 2872 | 204.8 KB |
| routefinder | 75.961 µs | 525 | 48.4 KB | 525 | 48.43 KB |
| actix-router | 152.62 µs | 2201 | 128.8 KB | 2201 | 128.8 KB |

## License

Expand Down
100 changes: 100 additions & 0 deletions Routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Routing

Order:

- authority (radix trie, punycode-decode, constraints)
- path (required, radix trie, percent-decode, constraints)
- query (hashmap of vec of params, may or not be present (optional support), static, dynamic)
- method (hashmap per method + hashset for catch-all ?)
- headers (hashmap of vec of headers may or not be present (optional support), static, dynamic)

Questions:
- for path and header, should the key be forced static? or would dynamic keys work?
- for path + header, should we allow

Longer Term (maybe never):

- version (HTTP/1.1, 2, 3, ...)
- scheme (http/https/wss/grpc)
- body (present vs absent) (maybe not a good idea)
- extensions (complex) (probably not needed, but a good escape hatch - would make conflict logic impossible).

## Lookups

Each sub-router will have it's own ID associated with it. (just an incrementing atomic).

Then we'll have a top-level data hashmap (which will use a no-hash method) to chain each sub-id together.

If a given sub-router isn't used, we'll use '*' as a replacement.

So IDs chain will lookup like: `*-353-*-28-*`

Before we go ahead and test this - let's think about each action, and how they work together.

Remember - inserts and deletes can be slow/careful, so long as the method search is fast.

For the time being, let's expose the internal IDs.
WIll make testing much easier.

We should habe a way to identify if there is 'more to come' from a route before returning errors.
So no '?' usage.

e.g.
1. insert "/path" -> OK
2. insert "/path" GET -> path conflicts, but OK because GET makes it unique.
3. insert "/path" -> path conflicts, ERR since nothing else can make this work.

For method routing, we have a tri-state to handle:
- no method filter
- any method
- one or more methods

I'd like in the future for the 'no X filter' states to be handled via type-state.
But then we lose out at our display layer?
Maybe we should collapse the any and none into one state.

### Inserts

So we'd first do the typical radix trie insert.
We traverse the trie, and if there's a conflict, we return that ID instead of our new one.
Expanded routes are handled internally by the prefix trie, so `/({name})` would actually result in 2 seperate nodes.
Need to be careful to handle conflicts across expanded routes.

Then method insert will take the prior path ID, then use it in a hashmap.
If no method is provided, we store a "*" instead of the given method.
If multiple methods are provided, and insert many.
(for deletes, similar logic to the expanded).

FIXME: If the path insert successed, but method insert failed, then what?

Then if the above 2 succeed, we create a data ID chain, and try and insert it.
NOTE: Don't think a conflict can occur at this point?

Then we're done.

### Deletes

So for path, we handle the typical approach.
For safety, we do a search up-front, to grab all expanded routes, and ensure the IDs match.
If they don't, error.
Then we do the actual delete and trie compression, returning the deleted path ID.

Then we do the same for method.
We look up all routes that match, ensure the inserted approach was the same
(e.g. if we insert /hello GET, /hello PUT, we can't delete both via /hello [GET, PUT], needs to be same as input)
Then we remove from the given hashmaps, returning the deleted method ID.

Then we simply create the data ID chain, and delete from the data map.

And we're done.

TODO: Consider ONLY performing searches first for ALL routes, then deleting after we verify everything is OK.

### Searches

This should be the easiest (and cheapest!) action.
We take the user provided path, and do a radix trie lookup.

Then we take the method, and either do a hashmap lookup, or replace it with "*".

Then we make a ID chain, and do a lookup for data.
21 changes: 10 additions & 11 deletions benches/gitlab_criterion.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use codspeed_criterion_compat::{criterion_group, criterion_main, BatchSize, Criterion};
use gitlab_routes::routes;
use gitlab_routes::{constraints, routes};
use std::hint::black_box;

pub mod gitlab_routes;
Expand All @@ -19,11 +19,9 @@ fn insert_benchmark(criterion: &mut Criterion) {
bencher.iter_batched(
|| router.clone(),
|mut router| {
constraints(&mut router);
for route in black_box(routes()) {
let route = wayfind::RouteBuilder::new()
.route(black_box(route))
.build()
.unwrap();
let route = route.build().unwrap();
router.insert(black_box(&route), true).unwrap();
}
},
Expand All @@ -39,19 +37,18 @@ fn delete_benchmark(criterion: &mut Criterion) {

group.bench_function("gitlab delete benchmarks/wayfind", |bencher| {
let mut router = wayfind::Router::new();
constraints(&mut router);

for route in routes() {
let route = wayfind::RouteBuilder::new().route(route).build().unwrap();
let route = route.build().unwrap();
router.insert(&route, true).unwrap();
}

bencher.iter_batched(
|| router.clone(),
|mut router| {
for route in black_box(routes()) {
let route = wayfind::RouteBuilder::new()
.route(black_box(route))
.build()
.unwrap();
let route = route.build().unwrap();
router.delete(black_box(&route)).unwrap();
}
},
Expand All @@ -67,8 +64,10 @@ fn display_benchmark(criterion: &mut Criterion) {

group.bench_function("gitlab display benchmarks/wayfind", |bencher| {
let mut router = wayfind::Router::new();
constraints(&mut router);

for route in routes() {
let route = wayfind::RouteBuilder::new().route(route).build().unwrap();
let route = route.build().unwrap();
router.insert(&route, true).unwrap();
}

Expand Down
Loading

0 comments on commit e8c0692

Please sign in to comment.