Skip to content

Commit 3b4f002

Browse files
Ankit Kumarmeta-codesync[bot]
authored andcommitted
Implement per connection storage for extensions via pipeline state
Summary: Extensions declaring `using ConnState = T` are handed the connection's single `T`, resolved once from a type-keyed store owned by the connection and threaded through the pipeline handler factories. Reviewed By: robertroeser Differential Revision: D116684106 fbshipit-source-id: c3a93bda4cf47db13697ea87b7db2d48d0523f96
1 parent 2e69e25 commit 3b4f002

9 files changed

Lines changed: 531 additions & 19 deletions

File tree

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/thrift/server/ThriftServerConnectionFactory.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ ThriftServerConnection ThriftServerConnectionFactory::buildConnectionImpl(
330330
// order — the first sits closest to the head, the last immediately above
331331
// the tail adapter. Each factory constructs a fresh per-connection instance.
332332
for (const auto& factory : config_.thriftPipelineHandlerFactories) {
333-
thriftPipelineBuilder.addErasedHandler(factory());
333+
thriftPipelineBuilder.addErasedHandler(factory(conn.extensionStates));
334334
}
335335
// Last before the tail, and deliberately after the embedder handlers: this
336336
// terminates the connection-lifecycle messages, so everything that might
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <memory>
20+
#include <typeindex>
21+
#include <typeinfo>
22+
#include <utility>
23+
24+
#include <folly/container/F14Map.h>
25+
26+
#include <thrift/lib/cpp2/fast_thrift/rocket/common/TypeErasedPtr.h>
27+
28+
namespace apache::thrift::fast_thrift::thrift {
29+
30+
/**
31+
* Per-connection state shared between extensions, keyed by state type.
32+
*
33+
* One extension instance is constructed per connection, so an extension that
34+
* needs nothing from its peers keeps its state as a member. This store exists
35+
* for the other case: several cooperating extensions on the same connection
36+
* that must read and write one object — an identity resolved by one and
37+
* authorized against by the next.
38+
*
39+
* A slot is keyed by the state type itself, so two extensions reach the same
40+
* object exactly when they name the same type, and unrelated extensions cannot
41+
* collide however they are composed. The object is heap-owned, so a reference
42+
* handed out at construction stays valid for the connection even as later
43+
* slots are added.
44+
*
45+
* Not thread-safe, and needs not be: the store, the pipeline it belongs to, and
46+
* every extension reading it live on one connection's EventBase.
47+
*/
48+
class ExtensionStateStore {
49+
public:
50+
ExtensionStateStore() = default;
51+
52+
ExtensionStateStore(const ExtensionStateStore&) = delete;
53+
ExtensionStateStore& operator=(const ExtensionStateStore&) = delete;
54+
ExtensionStateStore(ExtensionStateStore&&) noexcept = default;
55+
ExtensionStateStore& operator=(ExtensionStateStore&&) noexcept = default;
56+
57+
/**
58+
* The connection's `T`, constructing it from `args` on the first call for
59+
* that type. Later calls return the same object and ignore `args`.
60+
*
61+
* The extension path never passes `args`: an extension's ConnState is
62+
* default-constructed, because the extension that happens to be spliced in
63+
* first is not a meaningful choice of constructor arguments. `args` is for
64+
* direct users of the store, which today means tests.
65+
*/
66+
template <typename T, typename... Args>
67+
T& getOrCreate(Args&&... args) {
68+
const std::type_index key{typeid(T)};
69+
if (auto it = slots_.find(key); it != slots_.end()) {
70+
// Sound because the slot is keyed by typeid(T): only a T was ever
71+
// stored under this key.
72+
return *static_cast<T*>(it->second.get());
73+
}
74+
auto owned = std::make_unique<T>(std::forward<Args>(args)...);
75+
T* state = owned.get();
76+
slots_.emplace(key, rocket::from_unique_ptr(std::move(owned)));
77+
return *state;
78+
}
79+
80+
/**
81+
* Whether a slot for `T` has been created. For tests and diagnostics; the
82+
* message path uses getOrCreate.
83+
*/
84+
template <typename T>
85+
bool contains() const noexcept {
86+
return slots_.contains(std::type_index{typeid(T)});
87+
}
88+
89+
std::size_t size() const noexcept { return slots_.size(); }
90+
91+
private:
92+
folly::F14FastMap<std::type_index, rocket::TypeErasedPtr> slots_;
93+
};
94+
95+
} // namespace apache::thrift::fast_thrift::thrift

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/thrift/server/common/ThriftServerConnection.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <thrift/lib/cpp2/fast_thrift/thrift/server/adapter/ThriftServerAppAdapter.h>
3131
#include <thrift/lib/cpp2/fast_thrift/thrift/server/adapter/ThriftServerCompositeAppAdapter.h>
3232
#include <thrift/lib/cpp2/fast_thrift/thrift/server/adapter/ThriftServerTransportAdapter.h>
33+
#include <thrift/lib/cpp2/fast_thrift/thrift/server/common/ExtensionStateStore.h>
3334
#include <thrift/lib/cpp2/fast_thrift/thrift/server/common/context/ThriftConnContext.h>
3435

3536
namespace apache::thrift::fast_thrift::thrift::server {
@@ -77,6 +78,12 @@ struct ThriftServerConnection {
7778
// Buffer allocator used by the thrift pipeline.
7879
channel_pipeline::SimpleBufferAllocator thriftAllocator;
7980

81+
// Per-connection state shared between this connection's extensions. Handlers
82+
// hold references into it, so it must outlive thriftPipeline — as a value
83+
// member it is destroyed after the dtor body, which is where the pipeline
84+
// goes.
85+
ExtensionStateStore extensionStates;
86+
8087
// Head of the thrift pipeline. Owns the rocket connection (transport
8188
// handler, app adapter, rocket pipeline) via its rocketConnection()
8289
// member.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Unit test for ExtensionStateStore — the per-connection slot table that lets
18+
// cooperating extensions share one object. What it must guarantee: the same
19+
// type reaches the same object, different types never alias, and a reference
20+
// handed out early survives later slots being added.
21+
22+
#include <cstddef>
23+
#include <string>
24+
#include <utility>
25+
26+
#include <gtest/gtest.h>
27+
28+
#include <thrift/lib/cpp2/fast_thrift/thrift/server/common/ExtensionStateStore.h>
29+
30+
namespace apache::thrift::fast_thrift::thrift {
31+
32+
namespace {
33+
34+
struct Identity {
35+
std::string name;
36+
};
37+
38+
struct Counters {
39+
int requests{0};
40+
};
41+
42+
struct Tracked {
43+
explicit Tracked(int& live) : live_(live) { ++live_; }
44+
~Tracked() { --live_; }
45+
Tracked(const Tracked&) = delete;
46+
Tracked& operator=(const Tracked&) = delete;
47+
48+
int& live_;
49+
};
50+
51+
// A family of distinct state types, so a test can fill the store with slots
52+
// that actually grow it — resolving one type repeatedly would reuse the single
53+
// slot it already has.
54+
template <size_t N>
55+
struct Slot {
56+
size_t value{N};
57+
};
58+
59+
template <size_t... Ns>
60+
void createSlots(ExtensionStateStore& store, std::index_sequence<Ns...>) {
61+
(store.getOrCreate<Slot<Ns>>(), ...);
62+
}
63+
64+
} // namespace
65+
66+
// The property the whole mechanism rests on: two lookups of the same type are
67+
// the same object, so one extension's write is another's read.
68+
TEST(ExtensionStateStoreTest, SameTypeResolvesToTheSameObject) {
69+
ExtensionStateStore store;
70+
71+
store.getOrCreate<Identity>().name = "svc:caller";
72+
73+
EXPECT_EQ(store.getOrCreate<Identity>().name, "svc:caller");
74+
EXPECT_EQ(&store.getOrCreate<Identity>(), &store.getOrCreate<Identity>());
75+
}
76+
77+
// Distinct types are distinct slots — the collision the type-keyed store
78+
// exists to make impossible.
79+
TEST(ExtensionStateStoreTest, DistinctTypesGetDistinctSlots) {
80+
ExtensionStateStore store;
81+
82+
store.getOrCreate<Identity>().name = "svc:caller";
83+
store.getOrCreate<Counters>().requests = 3;
84+
85+
EXPECT_EQ(store.getOrCreate<Identity>().name, "svc:caller");
86+
EXPECT_EQ(store.getOrCreate<Counters>().requests, 3);
87+
EXPECT_EQ(store.size(), 2);
88+
}
89+
90+
// Extensions take their reference at construction and keep it for the
91+
// connection, so a slot added later must not move an earlier one. The slots
92+
// added here are all distinct types, which is what makes the table grow and
93+
// rehash — repeating one type would reuse its existing slot and prove nothing.
94+
TEST(ExtensionStateStoreTest, EarlierReferencesSurviveLaterSlots) {
95+
ExtensionStateStore store;
96+
97+
Identity& identity = store.getOrCreate<Identity>();
98+
identity.name = "svc:caller";
99+
constexpr size_t kLaterSlots = 64;
100+
createSlots(store, std::make_index_sequence<kLaterSlots>{});
101+
102+
EXPECT_EQ(store.size(), kLaterSlots + 1);
103+
EXPECT_EQ(identity.name, "svc:caller");
104+
EXPECT_EQ(&identity, &store.getOrCreate<Identity>());
105+
}
106+
107+
// Only the first call constructs: a second extension naming the type joins the
108+
// existing object rather than resetting what the first one wrote.
109+
TEST(ExtensionStateStoreTest, ArgumentsApplyOnlyToTheFirstCall) {
110+
ExtensionStateStore store;
111+
112+
EXPECT_EQ(store.getOrCreate<Identity>(Identity{"first"}).name, "first");
113+
EXPECT_EQ(store.getOrCreate<Identity>(Identity{"second"}).name, "first");
114+
}
115+
116+
TEST(ExtensionStateStoreTest, ContainsReportsOnlyCreatedSlots) {
117+
ExtensionStateStore store;
118+
119+
EXPECT_FALSE(store.contains<Identity>());
120+
store.getOrCreate<Identity>();
121+
EXPECT_TRUE(store.contains<Identity>());
122+
EXPECT_FALSE(store.contains<Counters>());
123+
}
124+
125+
// The store owns its slots, so a connection's state goes away with it.
126+
TEST(ExtensionStateStoreTest, SlotsAreDestroyedWithTheStore) {
127+
int live = 0;
128+
{
129+
ExtensionStateStore store;
130+
store.getOrCreate<Tracked>(live);
131+
EXPECT_EQ(live, 1);
132+
}
133+
EXPECT_EQ(live, 0);
134+
}
135+
136+
} // namespace apache::thrift::fast_thrift::thrift

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/thrift/server/extension/ThriftExtension.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,22 @@ template <typename H>
307307
concept HasResponseCallback =
308308
HasResponseViewCallback<H> || HasResponseMutatorCallback<H>;
309309

310+
/**
311+
* True iff H shares per-connection state with its peer extensions, by defining
312+
*
313+
* using ConnState = SomeType;
314+
*
315+
* The adapter then resolves the connection's `ConnState` from the
316+
* ExtensionStateStore and passes it as H's first constructor argument, ahead of
317+
* the arguments given to addThriftExtension. Extensions naming the same type
318+
* share one object per connection; an extension that declares none is
319+
* constructed from its own arguments alone.
320+
*
321+
* The state is default-constructed — whichever extension names it first brings
322+
* it into being, so no one extension's arguments could construct it. A
323+
* ConnState that is not default-constructible is a compile error.
324+
*/
325+
template <typename H>
326+
concept HasConnState = requires { typename H::ConnState; };
327+
310328
} // namespace apache::thrift::fast_thrift::thrift

third-party/thrift/src/thrift/lib/cpp2/fast_thrift/thrift/server/extension/ThriftExtensionPipelineHandler.h

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ const std::string& extensionName() {
115115
*
116116
* Lifetime: one instance per connection (constructed by the pipeline handler
117117
* factory); H is owned by value and its constructor arguments are forwarded
118-
* from addThriftExtension.
118+
* from addThriftExtension. An H declaring a ConnState additionally receives
119+
* that connection's shared state as its first constructor argument, resolved
120+
* once here so the extension never handles the store itself.
119121
*/
120122
template <typename H>
121123
class ThriftExtensionPipelineHandler {
@@ -132,8 +134,9 @@ class ThriftExtensionPipelineHandler {
132134

133135
public:
134136
template <typename... Args>
135-
explicit ThriftExtensionPipelineHandler(Args&&... args)
136-
: handler_(std::forward<Args>(args)...) {}
137+
explicit ThriftExtensionPipelineHandler(
138+
ExtensionStateStore& store, Args&&... args)
139+
: handler_(makeHandler(store, std::forward<Args>(args)...)) {}
137140

138141
channel_pipeline::Result onRead(
139142
ThriftPipelineHandlerContext& ctx,
@@ -353,6 +356,25 @@ class ThriftExtensionPipelineHandler {
353356
void handlerRemoved(ThriftPipelineHandlerContext&) noexcept {}
354357

355358
private:
359+
// Returned as a prvalue so H is built directly into handler_ — H needs no
360+
// move constructor, and its shared state is resolved exactly once per
361+
// connection, before any callback can run.
362+
template <typename... Args>
363+
static H makeHandler(ExtensionStateStore& store, Args&&... args) {
364+
if constexpr (HasConnState<H>) {
365+
static_assert(
366+
std::is_default_constructible_v<typename H::ConnState>,
367+
"H::ConnState must be default-constructible: the connection's state "
368+
"is created by whichever extension names it first, so there is no "
369+
"one extension whose arguments could construct it.");
370+
return H(
371+
store.getOrCreate<typename H::ConnState>(),
372+
std::forward<Args>(args)...);
373+
} else {
374+
return H(std::forward<Args>(args)...);
375+
}
376+
}
377+
356378
H handler_;
357379
// Latched from the setup messages so the payload-less ConnectionClosed can
358380
// still present the connection. Non-owning: the connection-context handler
@@ -363,4 +385,35 @@ class ThriftExtensionPipelineHandler {
363385
bool established_{false};
364386
};
365387

388+
/**
389+
* Build a factory that splices extension H into a connection's thrift pipeline.
390+
*
391+
* The peer of makeThriftPipelineHandlerFactory for the extension API: it wraps
392+
* H in the adapter and threads the connection's ExtensionStateStore into it, so
393+
* an H declaring a ConnState is handed that state at construction. `args` are
394+
* copied and forwarded to every per-connection instance, after the state.
395+
*/
396+
template <typename H, typename... Args>
397+
ThriftPipelineHandlerFactory makeThriftExtensionHandlerFactory(
398+
channel_pipeline::HandlerId id, Args... args) {
399+
using Adapter = ThriftExtensionPipelineHandler<H>;
400+
// The adapter is framework-owned, so this can only fire if the adapter itself
401+
// stops being a pipeline handler — the same named diagnostic the native path
402+
// gets from makeThriftPipelineHandlerFactory, rather than a failure deep
403+
// inside makeHandlerNode.
404+
static_assert(
405+
channel_pipeline::InboundHandler<Adapter, ThriftPipelineHandlerContext> ||
406+
channel_pipeline::
407+
OutboundHandler<Adapter, ThriftPipelineHandlerContext> ||
408+
channel_pipeline::
409+
DuplexHandler<Adapter, ThriftPipelineHandlerContext>,
410+
"ThriftExtensionPipelineHandler<H> must satisfy the Inbound, Outbound, or "
411+
"Duplex handler concept over ThriftPipelineHandlerContext");
412+
return [id, args...](ExtensionStateStore& store) {
413+
return channel_pipeline::detail::
414+
makeHandlerNode<Adapter, ThriftServerEventType>(
415+
id, std::make_unique<Adapter>(store, args...));
416+
};
417+
}
418+
366419
} // namespace apache::thrift::fast_thrift::thrift::server

0 commit comments

Comments
 (0)