From 8cabd8c424e3eda80c7d7ed4b2a2a3f6dd5355f2 Mon Sep 17 00:00:00 2001 From: Erin Power Date: Tue, 8 Oct 2024 11:47:10 +0200 Subject: [PATCH] perf: Use enum_dispatch over dynamic dispatch --- Cargo.lock | 13 + Cargo.toml | 1 + benches/cluster_map.rs | 11 +- benches/shared.rs | 2 +- benches/token_router.rs | 4 +- build/Makefile | 9 +- cloudbuild.yaml | 5 - crates/test/tests/mesh.rs | 5 +- deny.toml | 1 + docs/src/SUMMARY.md | 3 +- docs/src/services/proxy/filters.md | 3 +- .../proxy/filters/writing_custom_filters.md | 277 ------------------ examples/quilkin-filter-example/.gitignore | 34 --- examples/quilkin-filter-example/Cargo.toml | 38 --- examples/quilkin-filter-example/README.md | 3 - examples/quilkin-filter-example/config.yaml | 26 -- .../quilkin-filter-example/src/generated.rs | 18 -- .../src/generated/greet.rs | 6 - .../quilkin-filter-example/src/greet.proto | 26 -- examples/quilkin-filter-example/src/main.rs | 115 -------- src/filters.rs | 50 ++-- src/filters/chain.rs | 44 +-- src/filters/factory.rs | 16 +- src/lib.rs | 1 - 24 files changed, 68 insertions(+), 643 deletions(-) delete mode 100644 docs/src/services/proxy/filters/writing_custom_filters.md delete mode 100644 examples/quilkin-filter-example/.gitignore delete mode 100644 examples/quilkin-filter-example/Cargo.toml delete mode 100644 examples/quilkin-filter-example/README.md delete mode 100644 examples/quilkin-filter-example/config.yaml delete mode 100644 examples/quilkin-filter-example/src/generated.rs delete mode 100644 examples/quilkin-filter-example/src/generated/greet.rs delete mode 100644 examples/quilkin-filter-example/src/greet.proto delete mode 100644 examples/quilkin-filter-example/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4e23580dc8..e8ad820abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,6 +692,18 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2367,6 +2379,7 @@ dependencies = [ "divan", "either", "enum-map", + "enum_dispatch", "eyre", "fixedstr", "form_urlencoded", diff --git a/Cargo.toml b/Cargo.toml index 9baf6596b9..c8e4c4dfeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,7 @@ strum_macros = "0.26" cfg-if = "1.0.0" libflate = "2.0.0" form_urlencoded = "1.2.1" +enum_dispatch = "0.3.13" gxhash = "3.4.1" papaya = { version = "0.1.3", features = ["serde"] } seize = "0.4.5" diff --git a/benches/cluster_map.rs b/benches/cluster_map.rs index 6a8b33baed..654926ef20 100644 --- a/benches/cluster_map.rs +++ b/benches/cluster_map.rs @@ -17,10 +17,10 @@ mod serde { fn serialize_to_protobuf(cm: &ClusterMap) -> Vec { let mut resources = Vec::new(); - for cluster in cm.iter() { + for (key, cluster) in cm.pin().iter() { resources.push( Resource::Cluster(Cluster { - locality: cluster.key().clone().map(From::from), + locality: key.clone().map(From::from), endpoints: cluster .endpoints .iter() @@ -110,12 +110,7 @@ mod ops { use shared::{gen_cluster_map, GenCluster}; fn compute_hash(gc: &GenCluster) -> usize { - let mut total_endpoints = 0; - - for kv in gc.cm.iter() { - total_endpoints += kv.endpoints.len(); - } - + let total_endpoints = gc.cm.pin().values().map(|v| v.endpoints.len()).sum(); assert_eq!(total_endpoints, gc.total_endpoints); total_endpoints } diff --git a/benches/shared.rs b/benches/shared.rs index e6748c19aa..7854ed77a0 100644 --- a/benches/shared.rs +++ b/benches/shared.rs @@ -676,7 +676,7 @@ pub fn gen_cluster_map(token_kind: TokenKind) -> GenCluster { // Now actually insert the endpoints, now that the order of keys is established, // annoying, but note we split out iteration versus insertion, otherwise we deadlock - let keys: Vec<_> = cm.iter().map(|kv| kv.key().clone()).collect(); + let keys: Vec<_> = cm.pin().iter().map(|(key, _)| key.clone()).collect(); let mut sets = std::collections::BTreeMap::new(); let mut token_generator = match token_kind { diff --git a/benches/token_router.rs b/benches/token_router.rs index 7afda6b581..f9d1181582 100644 --- a/benches/token_router.rs +++ b/benches/token_router.rs @@ -14,8 +14,8 @@ fn token_router(b: Bencher, token_kind: &str) { let cm = std::sync::Arc::new(gc.cm); // Calculate the amount of bytes for all the tokens - for eps in cm.iter() { - for ep in &eps.value().endpoints { + for eps in cm.pin().values() { + for ep in &eps.endpoints { for tok in &ep.metadata.known.tokens { tokens.push(tok.clone()); } diff --git a/build/Makefile b/build/Makefile index cfa805c860..c342e99642 100644 --- a/build/Makefile +++ b/build/Makefile @@ -77,7 +77,7 @@ version: @echo $(package_version) # Run all tests -test: ensure-build-image test-quilkin test-examples test-docs +test: ensure-build-image test-quilkin test-docs # In CI with split jobs that both fetch they will fail if run in parallel since # cargo will be fighting with itself for some the same host directory that is @@ -103,13 +103,6 @@ test-quilkin: ensure-build-image --network=host \ -e RUST_BACKTRACE=1 --entrypoint=cargo $(BUILD_IMAGE_TAG) test -p quilkin -p qt -# Run tests against the examples -test-examples: ensure-build-image - docker run --rm $(common_rust_args) -w /workspace/examples/quilkin-filter-example \ - --entrypoint=cargo $(BUILD_IMAGE_TAG) clippy --tests -- -D warnings - docker run --rm $(common_rust_args) -w /workspace/examples/quilkin-filter-example \ - --entrypoint=cargo $(BUILD_IMAGE_TAG) fmt -- --check - # Run tests against documentation test-docs: ensure-build-image test-docs: GITHUB_REF_NAME ?= main diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 8fa2fa28aa..03fff6a743 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -38,11 +38,6 @@ steps: id: test-quilkin waitFor: - fetch-quilkin - - name: us-docker.pkg.dev/$PROJECT_ID/ci/make-docker - dir: ./build - args: - - test-examples - id: test-examples - name: us-docker.pkg.dev/$PROJECT_ID/ci/make-docker dir: ./build args: diff --git a/crates/test/tests/mesh.rs b/crates/test/tests/mesh.rs index 45dd35fb59..985d4b2e70 100644 --- a/crates/test/tests/mesh.rs +++ b/crates/test/tests/mesh.rs @@ -189,8 +189,9 @@ trace_test!(datacenter_discovery, { #[track_caller] fn assert_config(config: &quilkin::Config, datacenter: &quilkin::config::Datacenter) { let dcs = config.datacenters().read(); - let ipv4_dc = dcs.get(&std::net::Ipv4Addr::LOCALHOST.into()); - let ipv6_dc = dcs.get(&std::net::Ipv6Addr::LOCALHOST.into()); + let pin = dcs.pin(); + let ipv4_dc = pin.get(&std::net::Ipv4Addr::LOCALHOST.into()); + let ipv6_dc = pin.get(&std::net::Ipv6Addr::LOCALHOST.into()); match (ipv4_dc, ipv6_dc) { (Some(dc), None) => assert_eq!(&*dc, datacenter), diff --git a/deny.toml b/deny.toml index e0173fa251..0c7b213560 100644 --- a/deny.toml +++ b/deny.toml @@ -57,6 +57,7 @@ version = 2 allow = ["Apache-2.0", "MIT", "ISC", "BSD-3-Clause"] exceptions = [ { crate = "adler32", allow = ["Zlib"] }, + { crate = "atomic-wait", allow = ["BSD-2-Clause"] }, # This license should not really be used for code, but here we are { crate = "notify", allow = ["CC0-1.0"] }, { crate = "ring", allow = ["OpenSSL"] }, diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d44bc222ed..700af1e247 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -26,7 +26,6 @@ - [Pass](./services/proxy/filters/pass.md) - [Timestamp](./services/proxy/filters/timestamp.md) - [Token Router](./services/proxy/filters/token_router.md) - - [Writing Custom Filters](./services/proxy/filters/writing_custom_filters.md) - [Control Message Protocol](./services/proxy/qcmp.md) - [Metrics](./services/proxy/metrics.md) @@ -55,4 +54,4 @@ # Third Party -- [Videos and Presentations](./third-party/presentations.md) \ No newline at end of file +- [Videos and Presentations](./third-party/presentations.md) diff --git a/docs/src/services/proxy/filters.md b/docs/src/services/proxy/filters.md index 0e1e10f119..f76f6e35e9 100644 --- a/docs/src/services/proxy/filters.md +++ b/docs/src/services/proxy/filters.md @@ -14,8 +14,7 @@ As an example, say we would like to perform the following steps in our processin * Do not forward (drop) the packet if its compressed length is over 512 bytes. We would create a filter corresponding to each step either by leveraging any [existing filters](#built-in-filters) -that do what we want or [writing one ourselves](./filters/writing_custom_filters.md) and connect them to form the -following filter chain: +that do what we want or and connect them to form the following filter chain: ```bash append | compress | drop diff --git a/docs/src/services/proxy/filters/writing_custom_filters.md b/docs/src/services/proxy/filters/writing_custom_filters.md deleted file mode 100644 index 5f6369476a..0000000000 --- a/docs/src/services/proxy/filters/writing_custom_filters.md +++ /dev/null @@ -1,277 +0,0 @@ -# Writing Custom Filters - -> The full source code used in this example can be found - in [`examples/`][example]. - -Quilkin provides an extensible implementation of [Filters] that allows us to -plug in custom implementations to fit our needs. This document provides an -overview of the API and how we can go about writing our own [Filters]. First -we need to create a type and implement two traits for it. - -It's not terribly important what the filter in this example does so let's write -a `Greet` filter that appends `Hello` to every packet in one direction and -`Goodbye` to packets in the opposite direction. - -```rust,no_run,noplayground -struct Greet; -``` - -> As a convention within Quilkin: Filter names are singular, they also tend to -> be a verb, rather than an adjective. -> -> **Examples** -> -> - **Greet** not "Greets" -> - **Compress** not "Compressor". - -## `Filter` - -Represents the actual [Filter][built-in-filters] instance in the pipeline. An -implementation provides a `read` and a `write` method (both are passthrough -by default) that accepts a context object and returns a response. - -Both methods are invoked by the proxy when it consults the [filter chain] -`read` is invoked when a packet is received on the local downstream port and -is to be sent to an upstream endpoint while `write` is invoked in the opposite -direction when a packet is received from an upstream endpoint and is to be -sent to a downstream client. - -```rust,no_run,noplayground -# struct Greet; -use quilkin::filters::prelude::*; - -/// Appends data to each packet -impl Filter for Greet { - fn read(&self, ctx: &mut ReadContext) -> Result<(), FilterError> { - ctx.contents.extend_from_slice(b"Hello"); - Ok(()) - } - fn write(&self, ctx: &mut WriteContext) -> Result<(), FilterError> { - ctx.contents.extend_from_slice(b"Goodbye"); - Ok(()) - } -} -``` - -## `StaticFilter` - -Represents metadata needed for your [`Filter`], most of it has to with defining -configuration, for now we can use `()` as we have no configuration currently. - -```rust,no_run,noplayground -# use quilkin::filters::prelude::*; -# struct Greet; -# impl Filter for Greet {} -impl StaticFilter for Greet { - const NAME: &'static str = "greet.v1"; - type Configuration = (); - type BinaryConfiguration = (); - - fn try_from_config(config: Option) -> Result { - Ok(Self) - } -} -``` - -## Running - -We can run the proxy using `Proxy::run` function. Let's -add a main function that does that. Quilkin relies on the [Tokio] async -runtime, so we need to import that crate and wrap our main function with it. - -We can also register custom filters in quilkin using [`FilterRegistry::register`][FilterRegistry::register] - -Add Tokio as a dependency in `Cargo.toml`. - -```toml -[dependencies] -quilkin = "0.2.0" -tokio = { version = "1", features = ["full"]} -``` - -Add a main function that starts the proxy. - -```rust,no_run,noplayground,ignore -// src/main.rs -{{#include ../../../../../examples/quilkin-filter-example/src/main.rs:run}} -``` - -Now, let's try out the proxy. The following configuration starts our extended -version of the proxy at port 7777 and forwards all packets to an upstream server -at port 4321. - -```yaml -# quilkin.yaml -version: v1alpha1 -filters: - - name: greet.v1 -clusters: - - endpoints: - - address: 127.0.0.1:4321 -``` - -Next we to setup our network of services, for this example we're going to use -the `netcat` tool to spawn a UDP echo server and interactive client for us to -send packets over the wire. - -```bash -# Start the proxy -cargo run -- & -# Start a UDP listening server on the configured port -nc -lu 127.0.0.1 4321 & -# Start an interactive UDP client that sends packet to the proxy -nc -u 127.0.0.1 7777 -``` - -Whatever we pass to the client should now show up with our modification on the -listening server's standard output. For example typing `Quilkin` in the client -prints `Hello Quilkin` on the server. - -## Configuration - -Let's extend the `Greet` filter to have a configuration that contains what -greeting to use. - -The [Serde] crate is used to describe static YAML configuration in code while -[Tonic]/[Prost] is used to describe dynamic configuration as [Protobuf] messages -when talking to a [management server]. - -### YAML Configuration - -First let's create the type for our configuration: - -1. Add the yaml parsing crates to `Cargo.toml`: - -```toml -# [dependencies] -serde = "1.0" -serde_yaml = "0.8" -``` - -2. Define a struct representing the config: - -```rust,no_run,noplayground,ignore -// src/main.rs -{{#include ../../../../../examples/quilkin-filter-example/src/main.rs:serde_config}} -``` - -3. Update the `Greet` Filter to take in `greeting` as a parameter: - -```rust,no_run,noplayground,ignore -// src/main.rs -{{#include ../../../../../examples/quilkin-filter-example/src/main.rs:filter}} -``` - -### Protobuf Configuration - -Quilkin comes with out-of-the-box support for xDS management, and as such needs -to communicate filter configuration over [Protobuf] with management servers and -clients to synchronise state across the network. So let's add the binary version -of our `Greet` configuration. - -1. Add the proto parsing crates to `Cargo.toml`: - -```toml -[dependencies] -# ... -tonic = "0.5.0" -prost = "0.7" -prost-types = "0.7" -``` - -2. Create a [Protobuf] equivalent of our YAML configuration. - -```plaintext,no_run,noplayground,ignore -// src/greet.proto -{{#include ../../../../../examples/quilkin-filter-example/src/greet.proto:proto}} -``` - -3. Generate Rust code from the proto file: - -### Generated - Recommended - -Use something like [proto-gen](https://github.com/EmbarkStudios/proto-gen) to generate Rust code for the protobuf. - -At that point it is just normal rust code and can be included from where you placed the generated code, eg. `generated`. - -```rust,no_run,noplayground,ignore -mod generated; -use generated::greet as proto; -``` - -### At build time - -There are a few ways to generate [Prost] code from proto, we will use the [prost_build] crate in this example. - -Add the following required crates to `Cargo.toml`, and then add a -[build script][build-script] to generate the following Rust code -during compilation: - -```toml -# [dependencies] -bytes = "1.0" - -# [build-dependencies] -prost-build = "0.7" -``` - -```rust,no_run,noplayground,ignore -// src/build.rs -fn main() { - // Remove if you already have `protoc` installed in your system. - std::env::set_var("PROTOC", protobuf_src::protoc()); - - prost_build::compile_protos(&["src/greet.proto"], &["src/"]).unwrap(); -} -``` - -To include the generated code, we'll use [`tonic::include_proto`]. - -```rust,no_run,noplayground,ignore -// src/main.rs -#[allow(warnings, clippy::all)] -// ANCHOR: include_proto -mod proto { - tonic::include_proto!("greet"); -} -``` - -4. Then we just need to implement [std::convert::TryFrom] for converting the protobuf message to -equivalent configuration. - -```rust,no_run,noplayground,ignore -// src/main.rs -{{#include ../../../../../examples/quilkin-filter-example/src/main.rs:TryFrom}} -``` - -Now, let's update `Greet`'s `StaticFilter` implementation to use the two -configurations. - -```rust,no_run,noplayground,ignore -// src/main.rs -{{#include ../../../../../examples/quilkin-filter-example/src/main.rs:factory}} -``` - -That's it! With these changes we have wired up static configuration for our -filter. Try it out with the following configuration: - -```yaml -# quilkin.yaml -{{#include ../../../../../examples/quilkin-filter-example/config.yaml:yaml}} -``` - -[FilterRegistry::register]: ../../../../api/quilkin/filters/struct.FilterRegistry.html#method.register -[std::convert::TryFrom]: https://doc.rust-lang.org/std/convert/trait.TryFrom.html - -[Filters]: ../filters.md -[filter chain]: ../filters.md#filters-and-filter-chain -[built-in-filters]: ../filters.md#built-in-filters -[management server]: ../../xds.md -[tokio]: https://docs.rs/tokio -[tonic]: https://docs.rs/tonic -[prost]: https://docs.rs/prost -[Protobuf]: https://protobuf.dev -[Serde]: https://docs.serde.rs/serde_yaml/index.html -[prost_build]: https://docs.rs/prost-build/0.7.0/prost_build/ -[build-script]: https://doc.rust-lang.org/cargo/reference/build-scripts.html -[example]: https://github.com/googleforgames/quilkin/tree/{{GITHUB_REF_NAME}}/examples/quilkin-filter-example diff --git a/examples/quilkin-filter-example/.gitignore b/examples/quilkin-filter-example/.gitignore deleted file mode 100644 index da48e4ceb1..0000000000 --- a/examples/quilkin-filter-example/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# -# Copyright 2021 Google LLC -# -# Licensed 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. -# - -.* -!.gitignore -!.gcloudignore -!.dockerignore - -*.iml - -### Rust template -# Generated by Cargo -# will have compiled files and executables -/target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk diff --git a/examples/quilkin-filter-example/Cargo.toml b/examples/quilkin-filter-example/Cargo.toml deleted file mode 100644 index a8b66cafd0..0000000000 --- a/examples/quilkin-filter-example/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -# -# Copyright 2021 Google LLC -# -# Licensed 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. -# - -[workspace] - -[package] -name = "quilkin-filter-example" -version = "0.1.0" -homepage = "https://github.com/googleforgames/quilkin" -repository = "https://github.com/googleforgames/quilkin" -edition = "2021" - -[dependencies] -# If lifting this example, you will want to be explicit about the Quilkin version, e.g. -# quilkin = "0.2.0" -quilkin = { path = "../../" } -tokio = { version = "1.32", features = ["full"] } -tonic = "0.10" -prost = "0.12" -prost-types = "0.12" -serde = "1" -serde_yaml = "0.9" -bytes = "1" -schemars = "0.8" -async-trait = "0.1" diff --git a/examples/quilkin-filter-example/README.md b/examples/quilkin-filter-example/README.md deleted file mode 100644 index b4a15cb622..0000000000 --- a/examples/quilkin-filter-example/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Quilkin filter example - -This is the code example for the "Writing Custom Filters" Guide, linked via the homepage. diff --git a/examples/quilkin-filter-example/config.yaml b/examples/quilkin-filter-example/config.yaml deleted file mode 100644 index a4df9ed39b..0000000000 --- a/examples/quilkin-filter-example/config.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# -# Copyright 2021 Google LLC -# -# Licensed 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. -# - -# ANCHOR: yaml -version: v1alpha1 -port: 7001 -filters: -- name: greet.v1 - config: - greeting: Hey -endpoints: -- address: 127.0.0.1:4321 -# ANCHOR_END: yaml diff --git a/examples/quilkin-filter-example/src/generated.rs b/examples/quilkin-filter-example/src/generated.rs deleted file mode 100644 index 5208bb720a..0000000000 --- a/examples/quilkin-filter-example/src/generated.rs +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed 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. - */ - -#![allow(clippy::doc_markdown, clippy::use_self)] -pub mod greet; diff --git a/examples/quilkin-filter-example/src/generated/greet.rs b/examples/quilkin-filter-example/src/generated/greet.rs deleted file mode 100644 index 98baf274ce..0000000000 --- a/examples/quilkin-filter-example/src/generated/greet.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Greet { - #[prost(string, tag = "1")] - pub greeting: ::prost::alloc::string::String, -} diff --git a/examples/quilkin-filter-example/src/greet.proto b/examples/quilkin-filter-example/src/greet.proto deleted file mode 100644 index e97bd6cae1..0000000000 --- a/examples/quilkin-filter-example/src/greet.proto +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed 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. - */ - - -// ANCHOR: proto -syntax = "proto3"; - -package greet; - -message Greet { - string greeting = 1; -} -// ANCHOR_END: proto diff --git a/examples/quilkin-filter-example/src/main.rs b/examples/quilkin-filter-example/src/main.rs deleted file mode 100644 index 35a0f3de19..0000000000 --- a/examples/quilkin-filter-example/src/main.rs +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed 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. - */ - -#[allow(warnings, clippy::all)] -mod generated; -use generated::greet as proto; - -use quilkin::filters::prelude::*; - -use serde::{Deserialize, Serialize}; - -// ANCHOR: serde_config -#[derive(Serialize, Deserialize, Debug, schemars::JsonSchema)] -struct Config { - greeting: String, -} -// ANCHOR_END: serde_config - -// ANCHOR: TryFrom -impl TryFrom for Config { - type Error = ConvertProtoConfigError; - - fn try_from(p: proto::Greet) -> Result { - Ok(Self { - greeting: p.greeting, - }) - } -} - -impl From for proto::Greet { - fn from(config: Config) -> Self { - Self { - greeting: config.greeting, - } - } -} -// ANCHOR_END: TryFrom - -// ANCHOR: filter -struct Greet { - config: Config, -} - -impl Filter for Greet { - fn read(&self, ctx: &mut ReadContext) -> Result<(), FilterError> { - ctx.contents - .prepend_from_slice(format!("{} ", self.config.greeting).as_bytes()); - Ok(()) - } - fn write(&self, ctx: &mut WriteContext) -> Result<(), FilterError> { - ctx.contents - .prepend_from_slice(format!("{} ", self.config.greeting).as_bytes()); - Ok(()) - } -} -// ANCHOR_END: filter - -// ANCHOR: factory -use quilkin::filters::StaticFilter; - -impl StaticFilter for Greet { - const NAME: &'static str = "greet.v1"; - type Configuration = Config; - type BinaryConfiguration = proto::Greet; - - fn try_from_config(config: Option) -> Result { - Ok(Self { - config: Self::ensure_config_exists(config)?, - }) - } -} -// ANCHOR_END: factory - -// ANCHOR: run -#[tokio::main] -async fn main() -> quilkin::Result<()> { - quilkin::filters::FilterRegistry::register(vec![Greet::factory()].into_iter()); - - let (_shutdown_tx, shutdown_rx) = quilkin::make_shutdown_channel(quilkin::ShutdownKind::Normal); - let proxy = quilkin::Proxy::default(); - let config = quilkin::Config::default_non_agent(); - config.filters.store(std::sync::Arc::new( - quilkin::filters::FilterChain::try_create([quilkin::config::Filter { - name: Greet::NAME.into(), - label: None, - config: None, - }])?, - )); - config.clusters.modify(|map| { - map.insert_default( - [quilkin::net::endpoint::Endpoint::new( - (std::net::Ipv4Addr::LOCALHOST, 4321).into(), - )] - .into(), - ) - }); - - proxy - .run(config.into(), Default::default(), None, shutdown_rx) - .await -} -// ANCHOR_END: run diff --git a/src/filters.rs b/src/filters.rs index c3971a5777..586010e546 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -70,41 +70,34 @@ pub use self::{ write::WriteContext, }; +use crate::test::TestFilter; + pub use self::chain::FilterChain; +#[enum_dispatch::enum_dispatch(Filter)] +pub enum FilterKind { + Capture, + Compress, + Concatenate, + Debug, + Drop, + Firewall, + LoadBalancer, + LocalRateLimit, + Pass, + Match, + Timestamp, + TokenRouter, + HashedTokenRouter, + TestFilter, +} + /// Statically safe version of [`Filter`], if you're writing a Rust filter, you /// should implement [`StaticFilter`] in addition to [`Filter`], as /// [`StaticFilter`] guarantees all of the required properties through the type /// system, allowing Quilkin take care of the virtual table boilerplate /// automatically at compile-time. -/// ``` -/// use quilkin::filters::prelude::*; -/// -/// struct Greet; -/// -/// /// Prepends data on each packet -/// impl Filter for Greet { -/// fn read(&self, ctx: &mut ReadContext) -> Result<(), FilterError> { -/// ctx.contents.prepend_from_slice(b"Hello "); -/// Ok(()) -/// } -/// fn write(&self, ctx: &mut WriteContext) -> Result<(), FilterError> { -/// ctx.contents.prepend_from_slice(b"Goodbye "); -/// Ok(()) -/// } -/// } -/// -/// impl StaticFilter for Greet { -/// const NAME: &'static str = "greet.v1"; -/// type Configuration = (); -/// type BinaryConfiguration = (); -/// -/// fn try_from_config(_: Option) -> Result { -/// Ok(Self) -/// } -/// } -/// ``` -pub trait StaticFilter: Filter + Sized +pub trait StaticFilter: Filter + Sized + Into // This where clause simply states that `Configuration`'s and // `BinaryConfiguration`'s `Error` types are compatible with `filters::Error`. where @@ -207,6 +200,7 @@ where /// `write` implementation to execute. /// * Labels /// * `filter` The name of the filter being executed. +#[enum_dispatch::enum_dispatch] pub trait Filter: Send + Sync { /// [`Filter::read`] is invoked when the proxy receives data from a /// downstream connection on the listening port. diff --git a/src/filters/chain.rs b/src/filters/chain.rs index fc642e5e39..8d9ad21b5b 100644 --- a/src/filters/chain.rs +++ b/src/filters/chain.rs @@ -425,11 +425,11 @@ mod tests { let chain = FilterChain::new(vec![ ( TestFilter::NAME.into(), - FilterInstance::new(serde_json::json!(null), Box::new(TestFilter)), + FilterInstance::new(serde_json::json!(null), TestFilter.into()), ), ( TestFilter::NAME.into(), - FilterInstance::new(serde_json::json!(null), Box::new(TestFilter)), + FilterInstance::new(serde_json::json!(null), TestFilter.into()), ), ]) .unwrap(); @@ -477,41 +477,19 @@ mod tests { #[test] fn get_configs() { - struct TestFilter2; - impl Filter for TestFilter2 {} - - let filter_chain = FilterChain::new(vec![ - ( - "TestFilter".into(), - FilterInstance::new(serde_json::json!(null), Box::new(TestFilter)), - ), - ( - "TestFilter2".into(), - FilterInstance::new( - serde_json::json!({ "k1": "v1", "k2": 2 }), - Box::new(TestFilter2), - ), - ), - ]) + let filter_chain = FilterChain::new(vec![( + "TestFilter".into(), + FilterInstance::new(serde_json::json!(null), TestFilter.into()), + )]) .unwrap(); let configs = filter_chain.iter().collect::>(); assert_eq!( - vec![ - crate::config::Filter { - name: "TestFilter".into(), - label: None, - config: None, - }, - crate::config::Filter { - name: "TestFilter2".into(), - label: None, - config: Some(serde_json::json!({ - "k1": "v1", - "k2": 2 - })) - }, - ], + vec![crate::config::Filter { + name: "TestFilter".into(), + label: None, + config: None, + },], configs ) } diff --git a/src/filters/factory.rs b/src/filters/factory.rs index f4f8ee6d1f..560096c375 100644 --- a/src/filters/factory.rs +++ b/src/filters/factory.rs @@ -18,7 +18,7 @@ use std::sync::Arc; use crate::{ config::ConfigType, - filters::{CreationError, Filter, StaticFilter}, + filters::{CreationError, FilterKind, StaticFilter}, }; /// An owned pointer to a dynamic [`FilterFactory`] instance. @@ -35,12 +35,12 @@ struct FilterInstanceData { /// The configuration used to create the filter. pub label: Option, /// The created filter. - pub filter: Box, + pub filter: FilterKind, } impl FilterInstance { /// Constructs a [`FilterInstance`]. - pub fn new(config: serde_json::Value, filter: Box) -> Self { + pub fn new(config: serde_json::Value, filter: FilterKind) -> Self { Self(Arc::new(FilterInstanceData { config, label: None, @@ -56,12 +56,12 @@ impl FilterInstance { self.0.label.as_deref() } - pub fn filter(&self) -> &dyn Filter { - &*self.0.filter + pub fn filter(&self) -> &FilterKind { + &self.0.filter } } -/// Provides the name and creation function for a given [`Filter`]. +/// Provides the name and creation function for a given [`crate::filters::Filter`]. /// pub trait FilterFactory: Sync + Send { /// name returns the configuration name for the Filter @@ -77,7 +77,7 @@ pub trait FilterFactory: Sync + Send { /// `quilkin.filters.debug_filter.v1alpha1.Debug` fn name(&self) -> &'static str; - /// Returns the schema for the configuration of the [`Filter`]. + /// Returns the schema for the configuration of the [`crate::filters::Filter`]. fn config_schema(&self) -> schemars::schema::RootSchema; /// Returns a filter based on the provided arguments. @@ -129,7 +129,7 @@ where Ok(FilterInstance::new( config_json, - Box::from(F::try_from_config(config)?), + F::try_from_config(config)?.into(), )) } diff --git a/src/lib.rs b/src/lib.rs index 1e54b8dd85..6050c0bcf4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,5 @@ mod external_doc_tests { #![doc = include_str!("../docs/src/services/proxy/filters/match.md")] #![doc = include_str!("../docs/src/services/proxy/filters/timestamp.md")] #![doc = include_str!("../docs/src/services/proxy/filters/token_router.md")] - #![doc = include_str!("../docs/src/services/proxy/filters/writing_custom_filters.md")] #![doc = include_str!("../docs/src/services/xds/providers/filesystem.md")] }