Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On-demand VHDS implementation #8617

Merged
merged 73 commits into from
Jan 11, 2020
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f3410a9
On-demand VHDS implementation
Apr 3, 2019
1cfaa15
Fixed issues reported by check-format
Oct 15, 2019
7126c9a
Fixing formatting issues and responding to feedback
Oct 17, 2019
82a9f3d
Responding to feedback, pt. 2
Oct 19, 2019
7b6ab4e
Fixed vhds_integration_test that was failing after rebase
Oct 19, 2019
06a3d3d
responding to PR feedback, pt. 3
Oct 21, 2019
56d1a34
Fixed formatting issues
Oct 21, 2019
a60e639
Removed requested_aliases_ and switched to using updateSubscriptionIn…
Oct 25, 2019
7ea18ba
Responding to feedback
Oct 25, 2019
625e046
fixed build issue after rebase
Oct 25, 2019
01e2a7a
Responding to feedback
Oct 26, 2019
02b57ce
Responding to feedback
Oct 29, 2019
2bccc5a
Responding to feedback
Oct 29, 2019
8fbea45
Prefixing domains in vhds on-demand requests with route_config_name, …
Oct 29, 2019
2877884
Updates after rebase
Nov 13, 2019
906f7b5
Replaced vhds on-demand callbacks with an Observer interface
Nov 17, 2019
3d24223
Merge remote-tracking branch 'upstream' into vhds-on-demand-restarted
Nov 18, 2019
9bf8078
fixed build failures after merge
Nov 18, 2019
4664e04
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Nov 20, 2019
7f85917
fixed formatting
Nov 20, 2019
36352f6
Fixed spelling errors
Nov 20, 2019
41c5a62
Inlined OnDemandRouteUpdate::requestRouteConfigUpdate
Nov 20, 2019
f5adfad
Removed notifiable interface
Nov 26, 2019
64aedb7
Removed Filter::canResolveRouteAfterConfigUpdate method
Nov 26, 2019
98eb9e7
OnDemand is a first class filter now
Nov 29, 2019
d4d1dc4
Merge remote-tracking branch 'upstream' into vhds-on-demand-restarted
Nov 29, 2019
39d487c
fixed formatting
Nov 29, 2019
cd8ccb0
Rolled back changes in comments and strings where 'notification' was …
Dec 3, 2019
97794de
Merge remote-tracking branch 'upstream' into vhds-on-demand-restarted
Dec 4, 2019
2dc9ad8
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 4, 2019
0b3b608
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 5, 2019
b079d1b
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 9, 2019
2d93ef1
Removed requestAliasesResolution from grpc_mux interface
Dec 10, 2019
8488e9a
VHDS subscription is only recreated/restarted if its config changed
Dec 10, 2019
b7daa1b
vhds integration tests are passing
Dec 11, 2019
fc1bda4
fixed formatting
Dec 11, 2019
72c1669
Added tests
Dec 11, 2019
761c67b
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 11, 2019
13f33f4
Fixed spelling & removed unused method
Dec 11, 2019
500c97a
Fixed spelling
Dec 12, 2019
40d7156
Fix spelling
Dec 12, 2019
623e14f
Responded to feedback: update subscription interest when a Response w…
Dec 12, 2019
cb686b0
fixed formatting
Dec 12, 2019
ea35b38
Replaced StreamDecoderFilterCallbacks:canRequestRouteConfigUpdate wit…
Dec 13, 2019
74ec49d
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 17, 2019
ff053cb
Responded to feedback
Dec 17, 2019
98d0de6
Resource name contained in DeltaDiscoveryResponse received in respons…
Dec 17, 2019
595f777
fixed spelling error
Dec 18, 2019
c9db300
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 18, 2019
696ca8e
Fixed docs-related issues
Dec 18, 2019
55a5f7c
Responded to feedback
Dec 18, 2019
505bb16
fixed spelling
Dec 18, 2019
a296419
Fixing formatting errors
Dec 19, 2019
79d50f7
Fixing failing tests
Dec 19, 2019
dfdc9fb
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Dec 20, 2019
2f5fda1
Responded to feedback
Dec 20, 2019
01f9702
Reponded to feedback, pt. 2
Dec 20, 2019
180ef3d
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Jan 6, 2020
c398648
updated for v3alpha api
Jan 7, 2020
70a352d
Responded to feedback
Jan 7, 2020
cc350aa
Added comments re: lifecycle of RouteConfigUpdatedCallback
Jan 7, 2020
0a0e83f
Made on-demand filter configurable
Jan 8, 2020
29fcbee
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Jan 8, 2020
cbd8839
responded to feedback
Jan 8, 2020
498f96d
Added WatchMap::convertAliasWatchesToNameWatches tests
Jan 9, 2020
eefb971
Resolved two lifecycle-related issues aroun RouteConfigUpdatedCallback
Jan 9, 2020
8e7b8f2
Responded to feedback
Jan 9, 2020
75accac
Merge remote-tracking branch 'upstream/master' into vhds-on-demand-re…
Jan 9, 2020
c6e346d
Responded to feedback
Jan 9, 2020
bb5636a
Reponded to feedback
Jan 9, 2020
53293f8
Fixed formatting
Jan 9, 2020
dd2d191
Added on_demand filter tests
Jan 11, 2020
927fd44
fixing formatting issues
Jan 11, 2020
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
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,6 @@ extensions/filters/common/original_src @snowp @klarose
/*/extensions/filters/network/echo @htuch @alyssawilk
/*/extensions/filters/udp/udp_proxy @mattklein123 @danzh2010
/*/extensions/clusters/aggregate @yxue @snowp
# support for on-demand VHDS requests
/*/extensions/filters/http/on_demand @dmitri-d @htuch @lambdai
/*/extensions/filters/network/local_ratelimit @mattklein123 @junr03
2 changes: 2 additions & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ proto_library(
"//envoy/config/filter/http/ip_tagging/v2:pkg",
"//envoy/config/filter/http/jwt_authn/v2alpha:pkg",
"//envoy/config/filter/http/lua/v2:pkg",
"//envoy/config/filter/http/on_demand/v2:pkg",
"//envoy/config/filter/http/original_src/v2alpha1:pkg",
"//envoy/config/filter/http/rate_limit/v2:pkg",
"//envoy/config/filter/http/rbac/v2:pkg",
Expand Down Expand Up @@ -143,6 +144,7 @@ proto_library(
"//envoy/extensions/filters/http/ip_tagging/v3alpha:pkg",
"//envoy/extensions/filters/http/jwt_authn/v3alpha:pkg",
"//envoy/extensions/filters/http/lua/v3alpha:pkg",
"//envoy/extensions/filters/http/on_demand/v3alpha:pkg",
"//envoy/extensions/filters/http/original_src/v3alpha:pkg",
"//envoy/extensions/filters/http/ratelimit/v3alpha:pkg",
"//envoy/extensions/filters/http/rbac/v3alpha:pkg",
Expand Down
1 change: 1 addition & 0 deletions api/docs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ proto_library(
"//envoy/config/filter/http/ip_tagging/v2:pkg",
"//envoy/config/filter/http/jwt_authn/v2alpha:pkg",
"//envoy/config/filter/http/lua/v2:pkg",
"//envoy/config/filter/http/on_demand/v2:pkg",
"//envoy/config/filter/http/original_src/v2alpha1:pkg",
"//envoy/config/filter/http/rate_limit/v2:pkg",
"//envoy/config/filter/http/rbac/v2:pkg",
Expand Down
9 changes: 9 additions & 0 deletions api/envoy/config/filter/http/on_demand/v2/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# DO NOT EDIT. This file is generated by tools/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = ["@com_github_cncf_udpa//udpa/annotations:pkg"],
)
19 changes: 19 additions & 0 deletions api/envoy/config/filter/http/on_demand/v2/on_demand.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
syntax = "proto3";

package envoy.config.filter.http.on_demand.v2;

import "udpa/annotations/migrate.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.config.filter.http.on_demand.v2";
option java_outer_classname = "OnDemandProto";
option java_multiple_files = true;
option (udpa.annotations.file_migrate).move_to_package =
"envoy.extensions.filters.http.on_demand.v3alpha";

// [#protodoc-title: OnDemand]
// IP tagging :ref:`configuration overview <config_http_filters_on_demand>`.
// [#extension: envoy.filters.http.on_demand]

message OnDemand {
}
12 changes: 12 additions & 0 deletions api/envoy/extensions/filters/http/on_demand/v3alpha/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# DO NOT EDIT. This file is generated by tools/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/config/filter/http/on_demand/v2:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
syntax = "proto3";

package envoy.extensions.filters.http.on_demand.v3alpha;

import "udpa/annotations/versioning.proto";

import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.http.on_demand.v3alpha";
option java_outer_classname = "OnDemandProto";
option java_multiple_files = true;

// [#protodoc-title: OnDemand]
// IP tagging :ref:`configuration overview <config_http_filters_on_demand>`.
// [#extension: envoy.filters.http.on_demand]

message OnDemand {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.on_demand.v2.OnDemand";
}
1 change: 1 addition & 0 deletions docs/root/configuration/http/http_filters/http_filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ HTTP filters
ip_tagging_filter
jwt_authn_filter
lua_filter
on_demand_updates_filter
original_src_filter
rate_limit_filter
rbac_filter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.. _config_http_filters_on_demand:
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved

On-demand VHDS Updates
======================

The on-demand VHDS filter is used to request a :ref:`virtual host <envoy_api_msg_route.VirtualHost>`
data if it's not already present in the :ref:`Route Configuration <envoy_api_msg_RouteConfiguration>`. The
contents of the *Host* or *:authority* header is used to create the on-demand request. For an on-demand
request to be created, :ref:`VHDS <envoy_api_field_RouteConfiguration.vhds>` must be enabled and either *Host*
or *:authority* header be present.
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved

Configuration
-------------
* :ref:`v2 API reference <envoy_api_msg_config.filter.http.on_demand.v2.OnDemand>`
* This filter should be configured with the name *envoy.on_demand*.
* The filter should be placed before *envoy.router* filter in the HttpConnectionManager's filter chain.
2 changes: 2 additions & 0 deletions generated_api_shadow/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ proto_library(
"//envoy/config/filter/http/ip_tagging/v2:pkg",
"//envoy/config/filter/http/jwt_authn/v2alpha:pkg",
"//envoy/config/filter/http/lua/v2:pkg",
"//envoy/config/filter/http/on_demand/v2:pkg",
"//envoy/config/filter/http/original_src/v2alpha1:pkg",
"//envoy/config/filter/http/rate_limit/v2:pkg",
"//envoy/config/filter/http/rbac/v2:pkg",
Expand Down Expand Up @@ -143,6 +144,7 @@ proto_library(
"//envoy/extensions/filters/http/ip_tagging/v3alpha:pkg",
"//envoy/extensions/filters/http/jwt_authn/v3alpha:pkg",
"//envoy/extensions/filters/http/lua/v3alpha:pkg",
"//envoy/extensions/filters/http/on_demand/v3alpha:pkg",
"//envoy/extensions/filters/http/original_src/v3alpha:pkg",
"//envoy/extensions/filters/http/ratelimit/v3alpha:pkg",
"//envoy/extensions/filters/http/rbac/v3alpha:pkg",
Expand Down

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

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

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

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

33 changes: 33 additions & 0 deletions include/envoy/http/filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ class StreamFilterCallbacks {
virtual const ScopeTrackedObject& scope() PURE;
};

/**
* RouteConfigUpdatedCallback is used to notify an OnDemandRouteUpdate filter about completion of a
* RouteConfig update. The filter (and the associated ActiveStream) where the original on-demand
* request was originated can be destroyed before a response to an on-demand update request is
* received and updates are propagated. To handle this:
*
* OnDemandRouteUpdate filter instance holds a RouteConfigUpdatedCallbackSharedPtr to a callback.
* Envoy::Router::RdsRouteConfigProviderImpl holds a weak pointer to the RouteConfigUpdatedCallback
* above in an Envoy::Router::UpdateOnDemandCallback struct
*
* In RdsRouteConfigProviderImpl::onConfigUpdate(), before invoking the callback, a check is made to
* verify if the callback is still available.
*/
using RouteConfigUpdatedCallback = std::function<void(bool)>;
using RouteConfigUpdatedCallbackSharedPtr = std::shared_ptr<RouteConfigUpdatedCallback>;
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved

/**
* Stream decoder filter callbacks add additional callbacks that allow a decoding filter to restart
* decoding if they decide to hold data (e.g. for buffering or rate limiting).
Expand Down Expand Up @@ -436,6 +452,23 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks {
* @return The socket options to be applied to the upstream request.
*/
virtual Network::Socket::OptionsSharedPtr getUpstreamSocketOptions() const PURE;

/**
* Schedules a request for a RouteConfiguration update from the management server.
* @param route_config_updated_cb callback to be called when the configuration update has been
* propagated to the worker thread.
*/
virtual void
requestRouteConfigUpdate(RouteConfigUpdatedCallbackSharedPtr route_config_updated_cb) PURE;

/**
*
* @return absl::optional<Router::ConfigConstSharedPtr>. Contains a value if a non-scoped RDS
* route config provider is used. Scoped RDS provides are not supported at the moment, as
* retrieval of a route configuration in their case requires passing of http request headers
* as a parameter.
*/
virtual absl::optional<Router::ConfigConstSharedPtr> routeConfig() PURE;
};
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down
11 changes: 11 additions & 0 deletions include/envoy/router/rds.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <memory>

#include "envoy/config/route/v3alpha/route.pb.h"
#include "envoy/http/filter.h"
#include "envoy/router/router.h"

namespace Envoy {
Expand Down Expand Up @@ -54,6 +55,16 @@ class RouteConfigProvider {
*/
virtual void
validateConfig(const envoy::config::route::v3alpha::RouteConfiguration& config) const PURE;

/**
* Callback used to request an update to the route configuration from the management server.
* @param for_domain supplies the domain name that virtual hosts must match on
* @param route_config_updated_cb callback to be called when the configuration update has been
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved
* propagated to worker threads
*/
virtual void requestVirtualHostsUpdate(
const std::string& for_domain, Event::Dispatcher& thread_local_dispatcher,
std::weak_ptr<Http::RouteConfigUpdatedCallback> route_config_updated_cb) PURE;
};

using RouteConfigProviderPtr = std::unique_ptr<RouteConfigProvider>;
Expand Down
14 changes: 14 additions & 0 deletions include/envoy/router/route_config_update_receiver.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ class RouteConfigUpdateReceiver {
*/
virtual const std::string& configVersion() PURE;

/**
* @return bool return whether VHDS configuration has been changed in the last RDS update.
*/
// TODO(dmitri-d): Consider splitting RouteConfigUpdateReceiver into a RouteConfig state and a
// last update state. The latter could be passed to callbacks as a parameter, which would make the
// intent and the lifecycle of the "last update state" less muddled.
virtual bool vhdsConfigurationChanged() const PURE;
htuch marked this conversation as resolved.
Show resolved Hide resolved

/**
* @return uint64_t the hash value of RouteConfiguration.
*/
Expand All @@ -76,6 +84,12 @@ class RouteConfigUpdateReceiver {
* @return SystemTime the time of the last update.
*/
virtual SystemTime lastUpdated() const PURE;

/**
* @return the union of all resource names and aliases (if any) received with the last VHDS
* update.
*/
virtual const std::set<std::string>& resourceIdsInLastVhdsUpdate() PURE;
};

using RouteConfigUpdatePtr = std::unique_ptr<RouteConfigUpdateReceiver>;
Expand Down
1 change: 1 addition & 0 deletions source/common/config/delta_subscription_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class DeltaSubscriptionImpl : public Subscription, public SubscriptionCallbacks

// Config::Subscription
void start(const std::set<std::string>& resource_names) override;

void updateResourceInterest(const std::set<std::string>& update_to_these_names) override;

// Config::SubscriptionCallbacks (all pass through to callbacks_!)
Expand Down
4 changes: 4 additions & 0 deletions source/common/config/delta_subscription_state.cc
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ void DeltaSubscriptionState::handleGoodResponse(
throw EnvoyException(
fmt::format("duplicate name {} found among added/updated resources", resource.name()));
}
// DeltaDiscoveryResponses for unresolved aliases don't contain an actual resource
if (!resource.has_resource() && resource.aliases_size() > 0) {
continue;
}
if (message.type_url() != resource.resource().type_url()) {
throw EnvoyException(fmt::format("type URL {} embedded in an individual Any does not match "
"the message-wide type URL {} in DeltaDiscoveryResponse {}",
Expand Down
3 changes: 3 additions & 0 deletions source/common/config/delta_subscription_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class DeltaSubscriptionState : public Logger::Loggable<Logger::Id::config> {
// Update which resources we're interested in subscribing to.
void updateSubscriptionInterest(const std::set<std::string>& cur_added,
const std::set<std::string>& cur_removed);
void addAliasesToResolve(const std::set<std::string>& aliases);

// Whether there was a change in our subscription interest we have yet to inform the server of.
bool subscriptionUpdatePending() const;
Expand Down Expand Up @@ -79,6 +80,8 @@ class DeltaSubscriptionState : public Logger::Loggable<Logger::Id::config> {
void setResourceVersion(const std::string& resource_name, const std::string& resource_version);
void setResourceWaitingForServer(const std::string& resource_name);
void setLostInterestInResource(const std::string& resource_name);
void
populateDiscoveryRequest(envoy::service::discovery::v3alpha::DeltaDiscoveryResponse& request);

// A map from resource name to per-resource version. The keys of this map are exactly the resource
// names we are currently interested in. Those in the waitingForServer state currently don't have
Expand Down
11 changes: 11 additions & 0 deletions source/common/config/new_grpc_mux_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ void NewGrpcMuxImpl::onDiscoveryResponse(
message->system_version_info(), message->type_url());
return;
}

// When an on-demand request is made a Watch is created using an alias, as the resource name isn't
// known at that point. When an update containing aliases comes back, we update Watches with
// resource names.
for (const auto& r : message->resources()) {
if (r.aliases_size() > 0) {
AddedRemoved converted = sub->second->watch_map_.convertAliasWatchesToNameWatches(r);
sub->second->sub_state_.updateSubscriptionInterest(converted.added_, converted.removed_);
}
}

kickOffAck(sub->second->sub_state_.handleResponse(*message));
}

Expand Down
32 changes: 19 additions & 13 deletions source/common/config/new_grpc_mux_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ class NewGrpcMuxImpl
GrpcMuxCallbacks&) override;
void start() override;

struct SubscriptionStuff {
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved
SubscriptionStuff(const std::string& type_url, std::chrono::milliseconds init_fetch_timeout,
Event::Dispatcher& dispatcher, const LocalInfo::LocalInfo& local_info)
: sub_state_(type_url, watch_map_, local_info, init_fetch_timeout, dispatcher),
init_fetch_timeout_(init_fetch_timeout) {}

WatchMap watch_map_;
DeltaSubscriptionState sub_state_;
const std::chrono::milliseconds init_fetch_timeout_;

SubscriptionStuff(const SubscriptionStuff&) = delete;
dmitri-d marked this conversation as resolved.
Show resolved Hide resolved
SubscriptionStuff& operator=(const SubscriptionStuff&) = delete;
};

// for use in tests only
const absl::flat_hash_map<std::string, std::unique_ptr<SubscriptionStuff>>& subscriptions() {
return subscriptions_;
}

private:
Watch* addWatch(const std::string& type_url, const std::set<std::string>& resources,
SubscriptionCallbacks& callbacks, std::chrono::milliseconds init_fetch_timeout);
Expand Down Expand Up @@ -94,19 +113,6 @@ class NewGrpcMuxImpl
// description of how it interacts with pause() and resume().
PausableAckQueue pausable_ack_queue_;

struct SubscriptionStuff {
SubscriptionStuff(const std::string& type_url, std::chrono::milliseconds init_fetch_timeout,
Event::Dispatcher& dispatcher, const LocalInfo::LocalInfo& local_info)
: sub_state_(type_url, watch_map_, local_info, init_fetch_timeout, dispatcher),
init_fetch_timeout_(init_fetch_timeout) {}

WatchMap watch_map_;
DeltaSubscriptionState sub_state_;
const std::chrono::milliseconds init_fetch_timeout_;

SubscriptionStuff(const SubscriptionStuff&) = delete;
SubscriptionStuff& operator=(const SubscriptionStuff&) = delete;
};
// Map key is type_url.
absl::flat_hash_map<std::string, std::unique_ptr<SubscriptionStuff>> subscriptions_;

Expand Down
Loading