Skip to content

Commit 884b394

Browse files
authored
refactor: define new Synchronizer and Initializer interfaces for FDv2 (#517)
This PR introduces `IFDv2Initializer` and `IFDv2Synchronizer` interfaces. I based this on the Java design, but with some significant changes: ``` FDv2SourceResult Next(std::chrono::milliseconds timeout, data_model::Selector selector) ``` 1. I made this a blocking call with a timeout, because C++ doesn't have any equivalent to "anyOf" for waiting on multiple racing promises, and this was the cleanest alternative I could come up with. 2. It doesn't have the same "conditions" structure as Java, but it seems like that's only used to implement timeouts, so I think we don't need it, unless I missed something. 3. Rather than introduce a SelectorProvider interface for the initializers/synchronizers to use, I am passing the selector directly into `Next`. I think this is acceptable, as Next only return a single result, which I think will only use one selector. This avoids having to deal with the ownership complications a shared provider would entail. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk because it only adds new FDv2 interface headers and a result type without changing existing runtime behavior; main risk is future integration/implementation mismatches around the blocking `Next()` timeout/selector contract. > > **Overview** > Introduces new FDv2 source abstractions for the server SDK: a one-shot `IFDv2Initializer` (blocking `Run()` + `Close()`) and a continuous `IFDv2Synchronizer` with a blocking `Next(timeout, selector)` API. > > Adds `FDv2SourceResult` as a unified variant return type covering successful changesets, transient/terminal errors, shutdown, goodbye/rotation signals, and timeout-based wakeups (including an `fdv1_fallback` flag where applicable). Also updates `.gitignore` to ignore `.cache`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ed22857. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 6f795b2 commit 884b394

4 files changed

Lines changed: 179 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ build-dynamic
4242
build-static-debug
4343
build-dynamic-debug
4444
cmake-build-*
45+
.cache
4546

4647
# For Macs..
4748
.DS_Store
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#pragma once
2+
3+
#include <launchdarkly/data_model/fdv2_change.hpp>
4+
#include <launchdarkly/data_sources/data_source_status_error_info.hpp>
5+
6+
#include <optional>
7+
#include <string>
8+
#include <variant>
9+
10+
namespace launchdarkly::server_side::data_interfaces {
11+
12+
/**
13+
* Result returned by IFDv2Initializer::Run and IFDv2Synchronizer::Next.
14+
*
15+
* Mirrors Java's FDv2SourceResult.
16+
*/
17+
struct FDv2SourceResult {
18+
using ErrorInfo = common::data_sources::DataSourceStatusErrorInfo;
19+
20+
/**
21+
* A changeset was successfully received and is ready to apply.
22+
*/
23+
struct ChangeSet {
24+
data_model::FDv2ChangeSet change_set;
25+
/** If true, the server signaled that the client should fall back to
26+
* FDv1. */
27+
bool fdv1_fallback;
28+
};
29+
30+
/**
31+
* A transient error occurred; the source may recover.
32+
*/
33+
struct Interrupted {
34+
ErrorInfo error;
35+
bool fdv1_fallback;
36+
};
37+
38+
/**
39+
* A non-recoverable error occurred; the source should not be retried.
40+
*/
41+
struct TerminalError {
42+
ErrorInfo error;
43+
bool fdv1_fallback;
44+
};
45+
46+
/**
47+
* The source was closed cleanly (via Close()).
48+
*/
49+
struct Shutdown {};
50+
51+
/**
52+
* The server sent a goodbye; the orchestrator should rotate sources.
53+
*/
54+
struct Goodbye {
55+
std::optional<std::string> reason;
56+
bool fdv1_fallback;
57+
};
58+
59+
/**
60+
* Next() returned because the timeout expired before a result arrived.
61+
*/
62+
struct Timeout {};
63+
64+
using Value = std::variant<ChangeSet, Interrupted, TerminalError, Shutdown,
65+
Goodbye, Timeout>;
66+
67+
Value value;
68+
};
69+
70+
} // namespace launchdarkly::server_side::data_interfaces
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#pragma once
2+
3+
#include "fdv2_source_result.hpp"
4+
5+
#include <string>
6+
7+
namespace launchdarkly::server_side::data_interfaces {
8+
9+
/**
10+
* Defines a one-shot data source that runs to completion and returns a single
11+
* result. Used during the initialization phase of FDv2, before handing off to
12+
* an IFDv2Synchronizer.
13+
*/
14+
class IFDv2Initializer {
15+
public:
16+
/**
17+
* Run the initializer to completion. Blocks until a result is available.
18+
* Called at most once per instance.
19+
*
20+
* Close() may be called from another thread to unblock Run(), in which
21+
* case Run() returns FDv2SourceResult::Shutdown.
22+
*/
23+
virtual FDv2SourceResult Run() = 0;
24+
25+
/**
26+
* Unblocks any in-progress Run() call, causing it to return
27+
* FDv2SourceResult::Shutdown.
28+
*/
29+
virtual void Close() = 0;
30+
31+
/**
32+
* @return A display-suitable name of the initializer.
33+
*/
34+
[[nodiscard]] virtual std::string const& Identity() const = 0;
35+
36+
virtual ~IFDv2Initializer() = default;
37+
IFDv2Initializer(IFDv2Initializer const&) = delete;
38+
IFDv2Initializer(IFDv2Initializer&&) = delete;
39+
IFDv2Initializer& operator=(IFDv2Initializer const&) = delete;
40+
IFDv2Initializer& operator=(IFDv2Initializer&&) = delete;
41+
42+
protected:
43+
IFDv2Initializer() = default;
44+
};
45+
46+
} // namespace launchdarkly::server_side::data_interfaces
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#pragma once
2+
3+
#include "fdv2_source_result.hpp"
4+
5+
#include <launchdarkly/data_model/selector.hpp>
6+
7+
#include <chrono>
8+
#include <string>
9+
10+
namespace launchdarkly::server_side::data_interfaces {
11+
12+
/**
13+
* Defines a continuous data source that produces a stream of results. Used
14+
* during the synchronization phase of FDv2, after initialization is complete.
15+
*
16+
* The stream is started lazily on the first call to Next(). The synchronizer
17+
* runs until Close() is called.
18+
*/
19+
class IFDv2Synchronizer {
20+
public:
21+
/**
22+
* Block until the next result is available or the timeout expires.
23+
*
24+
* On the first call, the synchronizer starts its underlying connection.
25+
* Subsequent calls continue reading from the same connection.
26+
*
27+
* If the timeout expires before a result arrives, returns
28+
* FDv2SourceResult::Timeout. The orchestrator uses this to evaluate
29+
* fallback conditions.
30+
*
31+
* Close() may be called from another thread to unblock Next(), in which
32+
* case Next() returns FDv2SourceResult::Shutdown.
33+
*
34+
* @param timeout Maximum time to wait for the next result.
35+
* @param selector The selector to send with the request, reflecting any
36+
* changesets applied since the previous call.
37+
*/
38+
virtual FDv2SourceResult Next(std::chrono::milliseconds timeout,
39+
data_model::Selector selector) = 0;
40+
41+
/**
42+
* Unblocks any in-progress Next() call, causing it to return
43+
* FDv2SourceResult::Shutdown, and releases underlying resources.
44+
*/
45+
virtual void Close() = 0;
46+
47+
/**
48+
* @return A display-suitable name of the synchronizer.
49+
*/
50+
[[nodiscard]] virtual std::string const& Identity() const = 0;
51+
52+
virtual ~IFDv2Synchronizer() = default;
53+
IFDv2Synchronizer(IFDv2Synchronizer const&) = delete;
54+
IFDv2Synchronizer(IFDv2Synchronizer&&) = delete;
55+
IFDv2Synchronizer& operator=(IFDv2Synchronizer const&) = delete;
56+
IFDv2Synchronizer& operator=(IFDv2Synchronizer&&) = delete;
57+
58+
protected:
59+
IFDv2Synchronizer() = default;
60+
};
61+
62+
} // namespace launchdarkly::server_side::data_interfaces

0 commit comments

Comments
 (0)