Skip to content

Extensions and builder chaining #743

@soundofspace

Description

@soundofspace

In some cases we want to be able to chain items and build the final item by combining all the pieces.

One such example is TlsConnectorDataBuilder, right now we support chaining multiple builders by having a field base_builders: Vec<Arc<TlsConnectorDataBuilder>> which is traversed in reverse when a field is not present in this builder.

This allows us to (efficiently) chain builder and to have pre-made builders (stored in arc) for certain uses. One thing this builder currently doesn't support is disable a specific field. This happens because a None value triggers and implicit fallback to the base builders, this could be fixed with an ExplicitNone, or something along those lines.

However since Extensions are now append only and only support modifications by using internal mutable (something that should only be used for specific use cases). We now do this:

// We can edit our current builder directly or create a new one if needed
let mut builder = req
    .extensions_mut()
    .get::<TlsConnectorDataBuilder>()
    .cloned()
    .unwrap_or_default();
builder.set_server_verify_mode(ServerVerifyMode::Disable);
req.extensions_mut().insert(builder);

A more efficient way of doing this would be to do something like this

// First builder we insert we now put inside an Arc first (into_shared_builder)
req.extensions_mut().insert(TlsConnectorDataBuilder::default().into_shared_builder())
// Cloning here is now much cheaper
let mut parent = req
    .extensions_mut()
    .get::<Arc<TlsConnectorDataBuilder>>()
    .cloned()
    .unwrap_or_default();
let builder = TlsConnectorDataBuilder::new().with_parent(parent);
req.extensions_mut().insert(builder);

But this pattern is pretty tedious and specific to this case. I however believe that we can implement this in a more generic way so that more components benefit from this. We have lots of options here, but an example could be

// Instead of one big struct with spit this config up many small ones eg:
ConnectorVerifyMode(Option<ServerVerifyMode>);
ConnectorKeyLogIntent(Option<KeyLogIntent>);

In an inefficient (to be tested) approach the connector would then do

let verify_mode = req.extensions().get::<ConnectorVerifyMode>();
let key_log_intent = req.extensions().get::<ConnectorKeyLogIntent>();

If values are None -> There was no explicit Value set
If value is ConnectorVerifyMode(None) -> Explicitly disable
If value is ConnectorVerifyMode(Some(value)) -> A value was set

We could provide a function utility to hide explicit vs implicit None if this is tedious to deal with.

In a more efficient approach we would provide a way to build a final config in a single iteration, or to get all parts in a single go. In the basic example we always iterate over all the extensions from end to start to get a value. If this is actually bad performance remains to be tested, but still getting everything needed in a single go could be useful for other uses case, but it does complicate things, and currently not convinced if it's worth it.

This logic could work along the lines

let (verify, keylog_intent) = req.extensions.get_all::<ConnectorVerifyMode, ConnectorKeyLogIntent, ...>();

or a much more complex setup such as .reduce, iterating over needed options...

It is also possible that we don't split up builders in small parts, but that instead that we come up with some sort of generic builder pattern that extends beyond the TlsConnectorDataBuilder use case, but borrows ideas from there, and that builds on top the assumptions we can now make for extensions, mostly readonly and append only. Depending how many things we store in extensions this pattern might be more efficient in some cases, this also has the advantage that it's pretty easy to get the Combined config vs having to get all pieces eg in debug logs, but also in normal logic

Metadata

Metadata

Assignees

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions