From c977bd41f72f32dc44eb370f4833393dfc5c9149 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 2 Feb 2022 17:11:36 -0800 Subject: [PATCH 01/78] Improvements to experimental Async API --- LiteCore/Support/Actor.cc | 9 +- LiteCore/Support/Actor.hh | 17 +- LiteCore/Support/Async.cc | 25 +- LiteCore/Support/Async.hh | 317 +++++++---------------- LiteCore/tests/ActorTest.cc | 284 ++++++++++++++++++++ Networking/BLIP/docs/Async.md | 107 ++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 14 +- 7 files changed, 515 insertions(+), 258 deletions(-) create mode 100644 LiteCore/tests/ActorTest.cc create mode 100644 Networking/BLIP/docs/Async.md diff --git a/LiteCore/Support/Actor.cc b/LiteCore/Support/Actor.cc index 929bcee95..776f49d20 100644 --- a/LiteCore/Support/Actor.cc +++ b/LiteCore/Support/Actor.cc @@ -11,11 +11,12 @@ // #include "Actor.hh" +#include "Async.hh" #include "Logging.hh" #include -namespace litecore { namespace actor { +namespace litecore::actor { void Actor::caughtException(const std::exception &x) { Warn("Caught exception in Actor %s: %s", actorName().c_str(), x.what()); @@ -41,5 +42,9 @@ namespace litecore { namespace actor { cond->notify_one(); } + void Actor::wakeAsyncContext(AsyncContext *context) { + _mailbox.enqueue("wakeAsyncContext", ACTOR_BIND_METHOD0(context, &AsyncContext::_next)); + } + -} } +} diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index fd97f12e2..e7eab9cf8 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -28,10 +28,6 @@ #include "Stopwatch.hh" #endif -#ifdef ACTORS_SUPPORT_ASYNC -#include "Async.hh" -#endif - namespace litecore { namespace actor { class Actor; @@ -146,18 +142,9 @@ namespace litecore { namespace actor { } -#ifdef ACTORS_SUPPORT_ASYNC - /** Body of an async method: Creates an Provider from the lambda given, - then returns an Async that refers to that provider. */ - template - Async _asyncBody(const LAMBDA &bodyFn) { - return Async(this, bodyFn); - } + Actor* _enclosingActor() {return this;} - void wakeAsyncContext(AsyncContext *context) { - _mailbox.enqueue(ACTOR_BIND_METHOD0(context, &AsyncContext::next)); - } -#endif + void wakeAsyncContext(AsyncContext *context); private: friend class ThreadedMailbox; diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 522884b29..235425dba 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -12,13 +12,13 @@ #include "Async.hh" #include "Actor.hh" +#include "betterassert.hh" -namespace litecore { namespace actor { +namespace litecore::actor { - - bool AsyncState::_asyncCall(const AsyncBase &a, int lineNo) { - _calling = a._context; - _continueAt = lineNo; + bool AsyncState::_asyncCall(const AsyncBase &a, int curLine) { + _awaiting = a._context; + _currentLine = curLine; return !a.ready(); } @@ -47,36 +47,35 @@ namespace litecore { namespace actor { _observer = p; } - void AsyncContext::start() { + void AsyncContext::_start() { _waitingSelf = this; if (_actor && _actor != Actor::currentActor()) _actor->wakeAsyncContext(this); // Start on my Actor's queue else - next(); + _next(); } void AsyncContext::_wait() { _waitingActor = Actor::currentActor(); // retain my actor while I'm waiting - _calling->setObserver(this); + _awaiting->setObserver(this); } void AsyncContext::wakeUp(AsyncContext *async) { - assert(async == _calling); + assert(async == _awaiting); if (_waitingActor) { fleece::Retained waitingActor = std::move(_waitingActor); waitingActor->wakeAsyncContext(this); // queues the next() call on its Mailbox } else { - next(); + _next(); } } void AsyncContext::_gotResult() { _ready = true; - auto observer = _observer; - _observer = nullptr; + auto observer = move(_observer); if (observer) observer->wakeUp(this); _waitingSelf = nullptr; } -} } +} diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index b8ed5aa42..39fc46a97 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -13,134 +13,33 @@ #pragma once #include "RefCounted.hh" #include -#include #include +#include #include +#include -namespace litecore { namespace actor { - class Actor; - - /* - Async represents a result of type T that may not be available yet. This concept is - also referred to as a "future". You can create one by first creating an AsyncProvider, - which is also known as a "promise", then calling its `asyncValue` method: - - Async getIntFromServer() { - Retained> intProvider = Async::provider(); - sendServerRequestFor(intProvider); - return intProvider->asyncValue(); - } - - You can simplify this somewhat: - - Async getIntFromServer() { - auto intProvider = Async::provider(); // `auto` is your friend - sendServerRequestFor(intProvider); - return intProvider; // implicit conversion to Async - } - - The AsyncProvider reference has to be stored somewhere until the result is available. - Then you call its setResult() method: - - int result = valueReceivedFromServer(); - intProvider.setResult(result); - - Async has a `ready` method that tells whether the result is available, and a `result` - method that returns the result (or aborts if it's not available.) However, it does not - provide any way to block and wait for the result. That's intentional: we don't want - blocking! Instead, the way you work with async results is within an _asynchronous - function_. - - ASYNCHRONOUS FUNCTIONS - - An asynchronous function is a function that can resolve Async values in a way that appears - synchronous, but without actually blocking. It always returns an Async result (or void), - since if the Async value it's resolving isn't available, the function itself has to return - without (yet) providing a result. Here's what one looks like: - - Async anAsyncFunction() { - BEGIN_ASYNC_RETURNING(T) - ... - return t; - END_ASYNC() - } - - If the function doesn't return a result, it looks like this: - - void aVoidAsyncFunction() { - BEGIN_ASYNC() - ... - END_ASYNC() - } - - In between BEGIN and END you can "unwrap" Async values, such as those returned by other - asynchronous functions, by calling asyncCall(). The first parameter is the variable to - assign the result to, and the second is the expression returning the async result: - - asyncCall(int n, someOtherAsyncFunction()); - - `asyncCall` is a macro that hides some very weird control flow. What happens is that, if - the Async value isn't yet available, `asyncCall` causes the enclosing function to return. - (Obviously it returns an unavailable Async value.) It also registers as an observer of - the value, so when its result does become available, the enclosing function _resumes_ - right where it left off, assigns the result to the variable, and continues. +namespace litecore::actor { + using fleece::RefCounted; + using fleece::Retained; - ASYNC CALLS AND VARIABLE SCOPE - - `asyncCall()` places some odd restrictions on your code. Most importantly, a variable declared - between the BEGIN/END cannot have a scope that extends across an `asyncCall`: - - int foo = ....; - asyncCall(int n, someOtherAsyncFunction()); // ERROR: Cannot jump from switch... - - This is because the macro expansion of `asyncCall()` includes a `switch` label, and it's not - possible to jump to that label (when resuming the async flow of control) skipping the variable - declaration. - - If you want to use a variable across `asyncCall` scopes, you must declare it _before_ the - BEGIN_ASYNC -- its scope then includes the entire async function: - - int foo; - BEGIN_ASYNC_RETURNING(T) - ... - foo = ....; - asyncCall(int n, someOtherAsyncFunction()); // OK! - foo += n; - - Or if the variable isn't used after the next `asyncCall`, just use braces to limit its scope: - - { - int foo = ....; - } - asyncCall(int n, someOtherAsyncFunction()); // OK! - - THREADING - - By default, an async method resumes immediately when the Async value it's waiting for becomes - available. That means when the provider's `setResult` method is called, or when the - async method returning that value finally returns a result. This is reasonable in single- - threaded code. - - `asyncCall` is aware of Actors, however. So if an async Actor method waits, it will be resumed - on that Actor's execution context. This ensures that the Actor's code runs single-threaded, as - expected. + class Actor; - */ #define BEGIN_ASYNC_RETURNING(T) \ - return _asyncBody([=](AsyncState &_async_state_) mutable -> T { \ - switch (_async_state_.continueAt()) { \ + return Async(_enclosingActor(), [=](AsyncState &_async_state_) mutable -> std::optional { \ + switch (_async_state_.currentLine()) { \ default: #define BEGIN_ASYNC() \ - _asyncBody([=](AsyncState &_async_state_) mutable -> void { \ - switch (_async_state_.continueAt()) { \ + Async(_enclosingActor(), [=](AsyncState &_async_state_) mutable -> void { \ + switch (_async_state_.currentLine()) { \ default: #define asyncCall(VAR, CALL) \ if (_async_state_._asyncCall(CALL, __LINE__)) return {}; \ case __LINE__: \ - VAR = _async_state_.asyncResult(); _async_state_.reset(); + VAR = _async_state_.asyncResult>();\ + _async_state_.reset(); #define END_ASYNC() \ } \ @@ -156,51 +55,52 @@ namespace litecore { namespace actor { /** The state data passed to the lambda of an async function. */ class AsyncState { public: - uint32_t continueAt() const {return _continueAt;} + int currentLine() const {return _currentLine;} + void reset() {_awaiting = nullptr;} - bool _asyncCall(const AsyncBase &a, int lineNo); + bool _asyncCall(const AsyncBase &a, int curLine); template T&& asyncResult() { - return ((AsyncProvider*)_calling.get())->extractResult(); + return dynamic_cast*>(_awaiting.get())->extractResult(); } - void reset() {_calling = nullptr;} - protected: - fleece::Retained _calling; // What I'm blocked awaiting - uint32_t _continueAt {0}; // label/line# to continue lambda at + Retained _awaiting; // What my fn body is suspended awaiting + int _currentLine {0}; // label/line# to continue lambda at }; +#pragma mark - ASYNCPROVIDER: + + // Maintains the context/state of an async operation and its observer. // Abstract base class of AsyncProvider. - class AsyncContext : public fleece::RefCounted, protected AsyncState { + class AsyncContext : public RefCounted, protected AsyncState { public: bool ready() const {return _ready;} void setObserver(AsyncContext *p); void wakeUp(AsyncContext *async); protected: - AsyncContext(Actor *actor); + AsyncContext(Actor*); ~AsyncContext(); - void start(); + void _start(); void _wait(); void _gotResult(); - virtual void next() =0; + virtual void _next() =0; - bool _ready {false}; // True when result is ready - fleece::Retained _observer; // Dependent context waiting on me - Actor *_actor; // Owning actor, if any - fleece::Retained _waitingActor; // Actor that's waiting, if any - fleece::Retained _waitingSelf; // Keeps `this` from being freed + Retained _observer; // Dependent context waiting on me + Actor* _actor; // Owning actor, if any + Retained _waitingActor; // Actor that's waiting, if any + Retained _waitingSelf; // Keeps `this` from being freed + std::atomic _ready {false}; // True when result is ready #if DEBUG public: static std::atomic_int gInstanceCount; #endif - template friend class Async; friend class Actor; }; @@ -211,49 +111,36 @@ namespace litecore { namespace actor { class AsyncProvider : public AsyncContext { public: template - explicit AsyncProvider(Actor *actor, const LAMBDA body) + explicit AsyncProvider(Actor *actor, LAMBDA body) :AsyncContext(actor) - ,_body(body) + ,_body(std::move(body)) { } - static fleece::Retained create() { - return new AsyncProvider; - } + static Retained create() {return new AsyncProvider;} - Async asyncValue() { - return Async(this); - } + Async asyncValue() {return Async(this);} - void setResult(const T &result) { - _result = result; - _gotResult(); - } - - const T& result() const { - assert(_ready); - return _result; - } - - T&& extractResult() { - assert(_ready); - return std::move(_result); - } + void setResult(const T &result) {_result = result; _gotResult();} + void setResult(T &&result) {_result = std::move(result); _gotResult();} + const T& result() const {precondition(_result);return *_result;} + T&& extractResult() {precondition(_result); + return *std::move(_result);} private: - AsyncProvider() - :AsyncContext(nullptr) - { } + AsyncProvider() :AsyncContext(nullptr) { } - void next() override { + void _next() override { _result = _body(*this); - if (_calling) + if (_awaiting) { _wait(); - else + } else { + precondition(_result); _gotResult(); + } } - std::function _body; // The async function body - T _result {}; // My result + std::function(AsyncState&)> _body; // The async function body + std::optional _result; // My result }; @@ -262,44 +149,38 @@ namespace litecore { namespace actor { class AsyncProvider : public AsyncContext { public: template - explicit AsyncProvider(Actor *actor, const LAMBDA &body) + explicit AsyncProvider(Actor *actor, LAMBDA body) :AsyncContext(actor) - ,_body(body) + ,_body(std::move(body)) { } - static fleece::Retained create() { - return new AsyncProvider; - } + static Retained create() {return new AsyncProvider;} private: - AsyncProvider() - :AsyncContext(nullptr) - { } + AsyncProvider() :AsyncContext(nullptr) { } - void next() override { + void _next() override { _body(*this); - if (_calling) + if (_awaiting) _wait(); else _gotResult(); } - std::function _body; // The async function body + std::function _body; // The async function body }; +#pragma mark - ASYNC: + + // base class of Async class AsyncBase { public: - explicit AsyncBase(const fleece::Retained &context) - :_context(context) - { } - - bool ready() const {return _context->ready();} - + explicit AsyncBase(const Retained &c) :_context(c) { } + bool ready() const {return _context->ready();} protected: - fleece::Retained _context; // The AsyncProvider that owns my value - + Retained _context; // The AsyncProvider that owns my value friend class AsyncState; }; @@ -310,58 +191,49 @@ namespace litecore { namespace actor { public: using ResultType = T; - Async(AsyncProvider *provider) - :AsyncBase(provider) - { } - - Async(const fleece::Retained> &provider) - :AsyncBase(provider) - { } + Async(AsyncProvider *provider) :AsyncBase(provider) { } + Async(const Retained> &provider) :AsyncBase(provider) { } template - Async(Actor *actor, const LAMBDA& bodyFn) - :Async( new AsyncProvider(actor, bodyFn) ) + Async(Actor *actor, LAMBDA bodyFn) + :Async( new AsyncProvider(actor, std::move(bodyFn)) ) { - _context->start(); + _context->_start(); } - /** Returns a new AsyncProvider. */ - static fleece::Retained> provider() { - return AsyncProvider::create(); - } + /// Returns a new AsyncProvider. + static Retained> provider() {return AsyncProvider::create();} const T& result() const {return ((AsyncProvider*)_context.get())->result();} - /** Invokes the callback when this Async's result becomes ready, - or immediately if it's ready now. */ + /// Invokes the callback when the result becomes ready, or immediately if it's ready now. template - void wait(LAMBDA callback) { + void then(LAMBDA callback) { if (ready()) callback(result()); else - (void) new AsyncWaiter(_context, callback); + (void) new AsyncWaiter(_context, std::move(callback)); } - - // Internal class used by wait(), above + private: + // Internal class used by `then()`, above class AsyncWaiter : public AsyncContext { public: template AsyncWaiter(AsyncContext *context, LAMBDA callback) :AsyncContext(nullptr) - ,_callback(callback) + ,_callback(std::move(callback)) { - _waitingSelf = this; - _calling = context; + _waitingSelf = this; // retain myself while waiting + _awaiting = context; _wait(); } - protected: - void next() override { + void _next() override { _callback(asyncResult()); - _waitingSelf = nullptr; + _callback = nullptr; + _waitingSelf = nullptr; // release myself when done } - private: std::function _callback; }; @@ -372,32 +244,29 @@ namespace litecore { namespace actor { template <> class Async : public AsyncBase { public: - Async(AsyncProvider *provider) - :AsyncBase(provider) - { } - - Async(const fleece::Retained> &provider) - :AsyncBase(provider) - { } + Async(AsyncProvider *provider) :AsyncBase(provider) { } + Async(const Retained> &provider) :AsyncBase(provider) { } - static fleece::Retained> provider() { - return AsyncProvider::create(); - } + static Retained> provider() {return AsyncProvider::create();} template Async(Actor *actor, const LAMBDA& bodyFn) :Async( new AsyncProvider(actor, bodyFn) ) { - _context->start(); + _context->_start(); } }; - /** Body of an async function: Creates an AsyncProvider from the lambda given, - then returns an Async that refers to that provider. */ - template - Async _asyncBody(const LAMBDA &bodyFn) { - return Async(nullptr, bodyFn); - } + /// Pulls the result type out of an Async type. + /// If `T` is `Async`, or a reference thereto, then `async_result_type` is X. + template + using async_result_type = typename std::remove_reference_t::ResultType; + + + // Used by `BEGIN_ASYNC` macros. Returns the lexically enclosing actor instance, else NULL. + // (How? Outside of an Actor method, `_enclosingActor()` refers to the function below. + // In an Actor method, it refers to a method with the same name that returns `this`.) + static inline Actor* _enclosingActor() {return nullptr;} -} } +} diff --git a/LiteCore/tests/ActorTest.cc b/LiteCore/tests/ActorTest.cc new file mode 100644 index 000000000..3ca3031c6 --- /dev/null +++ b/LiteCore/tests/ActorTest.cc @@ -0,0 +1,284 @@ +// +// ActorTest.cc +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "Async.hh" +#include "Actor.hh" +#include "LiteCoreTest.hh" +#include "Logging.hh" + +using namespace std; +using namespace litecore::actor; + + +static Retained> aProvider, bProvider; + +static Async provideA() { + return aProvider; +} + +static Async provideB() { + return bProvider; +} + +static Async provideSum() { + string a, b; + BEGIN_ASYNC_RETURNING(string) + asyncCall(a, provideA()); + asyncCall(b, provideB()); + return a + b; + END_ASYNC() +} + + +static Async provideSumPlus() { + string a; + BEGIN_ASYNC_RETURNING(string) + asyncCall(a, provideSum()); + return a + "!"; + END_ASYNC() +} + + +static Async provideImmediately() { + BEGIN_ASYNC_RETURNING(string) + return "immediately"; + END_ASYNC() +} + + +static Async provideLoop() { + string n; + int sum = 0; + int i = 0; + BEGIN_ASYNC_RETURNING(int) + for (i = 0; i < 10; i++) { + asyncCall(n, provideSum()); + //fprintf(stderr, "n=%f, i=%d, sum=%f\n", n, i, sum); + sum += n.size() * i; + } + return sum; + END_ASYNC() +} + + +static string provideNothingResult; + +static void provideNothing() { + string a, b; + BEGIN_ASYNC() + asyncCall(a, provideA()); + asyncCall(b, provideB()); + provideNothingResult = a + b; + END_ASYNC() +} + + + +TEST_CASE("Async", "[Async]") { + aProvider = Async::provider(); + bProvider = Async::provider(); + { + Async sum = provideSum(); + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + bProvider->setResult(" there"); + REQUIRE(sum.ready()); + REQUIRE(sum.result() == "hi there"); + } + aProvider = bProvider = nullptr; + CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("Async, other order", "[Async]") { + aProvider = Async::provider(); + bProvider = Async::provider(); + { + Async sum = provideSum(); + REQUIRE(!sum.ready()); + bProvider->setResult(" there"); // this time provideB() finishes first + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(sum.ready()); + REQUIRE(sum.result() == "hi there"); + } + aProvider = bProvider = nullptr; + CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("AsyncWaiter", "[Async]") { + aProvider = Async::provider(); + bProvider = Async::provider(); + { + Async sum = provideSum(); + string result; + sum.then([&](string s) { + result = s; + }); + REQUIRE(!sum.ready()); + REQUIRE(result == ""); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + REQUIRE(result == ""); + bProvider->setResult(" there"); + REQUIRE(sum.ready()); + REQUIRE(result == "hi there"); + } + aProvider = bProvider = nullptr; + CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("Async, 2 levels", "[Async]") { + aProvider = Async::provider(); + bProvider = Async::provider(); + { + Async sum = provideSumPlus(); + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + bProvider->setResult(" there"); + REQUIRE(sum.ready()); + REQUIRE(sum.result() == "hi there!"); + } + aProvider = bProvider = nullptr; + CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("Async, loop", "[Async]") { + aProvider = Async::provider(); + bProvider = Async::provider(); + { + Async sum = provideLoop(); + for (int i = 1; i <= 10; i++) { + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + aProvider = Async::provider(); + bProvider->setResult(" there"); + bProvider = Async::provider(); + } + REQUIRE(sum.ready()); + REQUIRE(sum.result() == 360); + } + aProvider = bProvider = nullptr; + CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("Async, immediately", "[Async]") { + { + Async im = provideImmediately(); + REQUIRE(im.ready()); + REQUIRE(im.result() == "immediately"); + } + CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("Async void fn", "[Async]") { + aProvider = Async::provider(); + bProvider = Async::provider(); + provideNothingResult = ""; + { + provideNothing(); + REQUIRE(provideNothingResult == ""); + aProvider->setResult("hi"); + REQUIRE(provideNothingResult == ""); + bProvider->setResult(" there"); + REQUIRE(provideNothingResult == "hi there"); + } + aProvider = bProvider = nullptr; + CHECK(AsyncContext::gInstanceCount == 0); +} + + +#pragma mark - WITH ACTORS: + + +static Async downloader(string url) { + auto provider = Async::provider(); + std::thread t([=] { + std::this_thread::sleep_for(1s); + provider->setResult("Contents of " + url); + }); + t.detach(); + return provider; +} + + +static string waitFor(Async &async) { + optional contents; + async.then([&](string c) { + contents = c; + }); + while (!contents) { + this_thread::sleep_for(10ms); + } + return *contents; +} + + +class TestActor : public Actor { +public: + TestActor() :Actor(kC4Cpp_DefaultLog) { } + + Async download(string url) { + string contents; + BEGIN_ASYNC_RETURNING(string) + CHECK(currentActor() == this); + asyncCall(contents, downloader(url)); + CHECK(currentActor() == this); + return contents; + END_ASYNC() + } + + Async download(string url1, string url2) { + optional> dl1, dl2; + string contents; + BEGIN_ASYNC_RETURNING(string) + CHECK(currentActor() == this); + dl1 = download(url1); + dl2 = download(url2); + asyncCall(contents, *dl1); + CHECK(currentActor() == this); + asyncCall(string contents2, *dl2); + return contents + " and " + contents2; + END_ASYNC() + } +}; + + +TEST_CASE("Async on thread", "[Async]") { + auto asyncContents = downloader("couchbase.com"); + string contents = waitFor(asyncContents); + CHECK(contents == "Contents of couchbase.com"); +} + + +TEST_CASE("Async Actor", "[Async]") { + auto actor = make_retained(); + auto asyncContents = actor->download("couchbase.org"); + string contents = waitFor(asyncContents); + CHECK(contents == "Contents of couchbase.org"); +} + + +TEST_CASE("Async Actor Twice", "[Async]") { + auto actor = make_retained(); + auto asyncContents = actor->download("couchbase.org", "couchbase.biz"); + string contents = waitFor(asyncContents); + CHECK(contents == "Contents of couchbase.org and Contents of couchbase.biz"); +} diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md new file mode 100644 index 000000000..e7232983c --- /dev/null +++ b/Networking/BLIP/docs/Async.md @@ -0,0 +1,107 @@ +# The Async API + +## Asynchronous Values (Futures) + +`Async` represents a value of type `T` that may not be available yet. This concept is also referred to as a ["future"](https://en.wikipedia.org/wiki/Futures_and_promises). + +You can create one by first creating an `AsyncProvider`, which is also known as a "promise", then calling its `asyncValue` method: + +```c++ +Async getIntFromServer() { + Retained> intProvider = Async::provider(); + sendServerRequestFor(intProvider); + return intProvider->asyncValue(); +} +``` + +You can simplify this somewhat: + +```c++ +Async getIntFromServer() { + auto intProvider = Async::provider(); // `auto` is your friend + sendServerRequestFor(intProvider); + return intProvider; // implicit conversion to Async +} +``` + +The `AsyncProvider` reference has to be stored somewhere until the result is available. Then you call its `setResult()` method: + +```c++ +int result = valueReceivedFromServer(); +intProvider.setResult(result); +``` + +`Async` has a `ready` method that tells whether the result is available, and a `result` method that returns the result (or aborts if it's not available.) However, it does not provide any way to block and wait for the result. That's intentional: we don't want blocking! Instead, the way you work with async results is within an _asynchronous function_. + +## Asynchronous Functions + +An asynchronous function is a function that can resolve `Async` values in a way that appears synchronous, but without actually blocking. It always returns an `Async` result (or void), since if the `Async` value it's resolving isn't available, the function itself has to return without (yet) providing a result. + +> This is very much modeled on the "async/await" feature found in many other languages, like C#, JavaScript and Swift. C++20 has it too, under the name "coroutines", but we can't use C++20 yet. + +Here's what an async function looks like: + +```c++ + Async anAsyncFunction() { + BEGIN_ASYNC_RETURNING(T) + ... + return t; + END_ASYNC() + } +``` + +If the function doesn't return a result, and a caller won't need to know when it finishes, it doesn't need a return value at all, and looks like this: + +```c++ +void aVoidAsyncFunction() { + BEGIN_ASYNC() + ... + END_ASYNC() +} +``` + +In between `BEGIN_ASYNC` and `END_ASYNC` you can "unwrap" `Async` values, such as those returned by other asynchronous functions, by using the `asyncCall()` macro. The first parameter is the variable to assign the result to, and the second is the expression returning the async result: + +```c++ +asyncCall(int n, someOtherAsyncFunction()); +``` + +> TMI: `asyncCall` is a macro that hides some very weird control flow. It first evaluates the second parameter to get its `Async` value. If that value isn't yet available, `asyncCall` _causes the enclosing function to return_. (Obviously it returns an unavailable `Async` value.) It also registers as an observer of the async value, so when its result does become available, the enclosing function _resumes_ right where it left off (🤯), assigns the result to the variable, and continues. + +## Async Calls and Variables' Scope + +The weirdness inside `asyncCall()` places some odd restrictions on your code. Most importantly, **a variable declared between `BEGIN_ASYNC()` and `END_ASYNC()` cannot have a scope that extends across an `asyncCall`**: + +```c++ +int foo = ....; +asyncCall(int n, someOtherAsyncFunction()); // ERROR: "Cannot jump from switch..." +``` + +> TMI: This is because the macro expansion of an async function wraps a `switch` statement around its body, and the expansion of `asyncCall()` contains a `case` label. The C++ language does not allow a jump to a `case` to skip past a variable declaration. + +If you want to use a variable across `asyncCall` scopes, you must **declare it _before_ the `BEGIN_ASYNC`** -- its scope then includes the entire async function: + +```c++ +int foo; +BEGIN_ASYNC_RETURNING(T) +... +foo = ....; +asyncCall(int n, someOtherAsyncFunction()); // OK! +foo += n; +``` + +Or if the variable isn't used after the next `asyncCall`, just use braces to limit its scope: + +```c++ +{ + int foo = ....; +} +asyncCall(int n, someOtherAsyncFunction()); // OK! +``` + +## Threading + +By default, an async method resumes immediately when the `Async` value it's waiting for becomes available. That means when the provider's `setResult` method is called, or when the async method returning that value finally returns a result, the waiting method will synchronously resume. This is reasonable in single- threaded code. + +`asyncCall` is aware of [Actors](Actors.md), however. So if an async Actor method waits, it will be resumed on that Actor's execution context. This ensures that the Actor's code runs single-threaded, as expected. + diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 375c4f610..370266747 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -219,6 +219,8 @@ 27727C55230F279D0082BCC9 /* HTTPLogic.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27727C53230F279D0082BCC9 /* HTTPLogic.cc */; }; 27766E161982DA8E00CAA464 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; 2776AA272087FF6B004ACE85 /* LegacyAttachments.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2776AA252087FF6B004ACE85 /* LegacyAttachments.cc */; }; + 277C5C4627AB1EB4001BE212 /* ActorTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277C5C4527AB1EB4001BE212 /* ActorTest.cc */; }; + 277C5C4727AB1F02001BE212 /* Async.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2744B336241854F2005A194D /* Async.cc */; }; 2783DF991D27436700F84E6E /* c4ThreadingTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2783DF981D27436700F84E6E /* c4ThreadingTest.cc */; }; 2787EB271F4C91B000DB97B0 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; 2787EB291F4C929C00DB97B0 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; @@ -1198,6 +1200,7 @@ 2776AA262087FF6B004ACE85 /* LegacyAttachments.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LegacyAttachments.hh; sourceTree = ""; }; 2777146C1C5D6BDB003C0287 /* static_lib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = static_lib.xcconfig; sourceTree = ""; }; 2779CC6E1E85E4FC00F0D251 /* ReplicatorTypes.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ReplicatorTypes.hh; sourceTree = ""; }; + 277C5C4527AB1EB4001BE212 /* ActorTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ActorTest.cc; sourceTree = ""; }; 277CB6251D0DED5E00702E56 /* Fleece.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Fleece.xcodeproj; path = fleece/Fleece.xcodeproj; sourceTree = ""; }; 277D19C9194E295B008E91EB /* Error.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Error.hh; sourceTree = ""; }; 277FEE5721ED10FA00B60E3C /* ReplicatorSGTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ReplicatorSGTest.cc; sourceTree = ""; }; @@ -1815,6 +1818,7 @@ children = ( 275FF6D11E4947E1005F90DD /* c4BaseTest.cc */, 274D18EC2617DFE40018D39C /* c4DocumentTest_Internal.cc */, + 277C5C4527AB1EB4001BE212 /* ActorTest.cc */, 277015081D523E2E008BADD7 /* DataFileTest.cc */, 27E0CA9F1DBEB0BA0089A9C0 /* DocumentKeysTest.cc */, 272B1BEA1FB1513100F56620 /* FTSTest.cc */, @@ -2058,20 +2062,20 @@ 2744B36124185D24005A194D /* Actors */ = { isa = PBXGroup; children = ( - 2744B335241854F2005A194D /* ActorProperty.hh */, - 2744B336241854F2005A194D /* Async.cc */, + 2744B340241854F2005A194D /* Actor.hh */, 2744B337241854F2005A194D /* Actor.cc */, 2744B338241854F2005A194D /* ThreadedMailbox.hh */, 2744B339241854F2005A194D /* GCDMailbox.hh */, 2744B33A241854F2005A194D /* ThreadedMailbox.cc */, 2744B33B241854F2005A194D /* GCDMailbox.cc */, 2744B33F241854F2005A194D /* ActorProperty.cc */, - 2744B340241854F2005A194D /* Actor.hh */, - 2744B341241854F2005A194D /* Async.hh */, 2744B342241854F2005A194D /* Channel.cc */, 2744B344241854F2005A194D /* Channel.hh */, 274C1DC325C4A79C00B0EEAC /* ChannelManifest.cc */, 274C1DC425C4A79C00B0EEAC /* ChannelManifest.hh */, + 2744B341241854F2005A194D /* Async.hh */, + 2744B336241854F2005A194D /* Async.cc */, + 2744B335241854F2005A194D /* ActorProperty.hh */, ); name = Actors; sourceTree = ""; @@ -3929,6 +3933,7 @@ 274D17C22615445B0018D39C /* DBAccessTestWrapper.cc in Sources */, 27FA09A01D6FA380005888AA /* DataFileTest.cc in Sources */, 27E0CAA01DBEB0BA0089A9C0 /* DocumentKeysTest.cc in Sources */, + 277C5C4627AB1EB4001BE212 /* ActorTest.cc in Sources */, 27505DDD256335B000123115 /* VersionVectorTest.cc in Sources */, 27456AFD1DC9507D00A38B20 /* SequenceTrackerTest.cc in Sources */, 274EDDFA1DA322D4003AD158 /* QueryParserTest.cc in Sources */, @@ -4305,6 +4310,7 @@ 27D74A7A1D4D3F2300D806E0 /* Backup.cpp in Sources */, 276683B61DC7DD2E00E3F187 /* SequenceTracker.cc in Sources */, 27F0426C2196264900D7C6FA /* SQLiteDataFile+Indexes.cc in Sources */, + 277C5C4727AB1F02001BE212 /* Async.cc in Sources */, 27FA568924AD0F8E00B2F1F8 /* Pusher+Revs.cc in Sources */, 278963621D7A376900493096 /* EncryptedStream.cc in Sources */, 72A3AF891F424EC0001E16D4 /* PrebuiltCopier.cc in Sources */, From 88bfcaf068aaff659e822ad639d6e14e28e3279c Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Feb 2022 11:52:21 -0800 Subject: [PATCH 02/78] Lots more Async work! --- LiteCore/Support/Actor.hh | 2 +- LiteCore/Support/Async.cc | 65 ++-- LiteCore/Support/Async.hh | 360 ++++++++++++------ LiteCore/tests/{ActorTest.cc => AsyncTest.cc} | 184 ++++++--- Networking/BLIP/docs/Async.md | 165 ++++++-- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 + 6 files changed, 561 insertions(+), 217 deletions(-) rename LiteCore/tests/{ActorTest.cc => AsyncTest.cc} (59%) diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index e7eab9cf8..b0178cb15 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -142,7 +142,7 @@ namespace litecore { namespace actor { } - Actor* _enclosingActor() {return this;} + Actor* _thisActor() {return this;} void wakeAsyncContext(AsyncContext *context); diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 235425dba..46ae45b36 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -16,50 +16,52 @@ namespace litecore::actor { - bool AsyncState::_asyncCall(const AsyncBase &a, int curLine) { + bool AsyncState::_await(const AsyncBase &a, int curLine) { _awaiting = a._context; _currentLine = curLine; return !a.ready(); } -#if DEBUG - std::atomic_int AsyncContext::gInstanceCount; -#endif - - - AsyncContext::AsyncContext(Actor *actor) - :_actor(actor) - { -#if DEBUG - ++gInstanceCount; -#endif - } - - AsyncContext::~AsyncContext() { -#if DEBUG - --gInstanceCount; -#endif - } - void AsyncContext::setObserver(AsyncContext *p) { - assert(!_observer); + precondition(!_observer); _observer = p; } + void AsyncContext::_start() { - _waitingSelf = this; + _waitingSelf = this; // Retain myself while waiting if (_actor && _actor != Actor::currentActor()) - _actor->wakeAsyncContext(this); // Start on my Actor's queue + _actor->wakeAsyncContext(this); // Schedule the body to run on my Actor's queue else - _next(); + _next(); // or run it synchronously } + + // AsyncProvider overrides this, and calls it last. + // Async::AsyncWaiter overrides this and doesn't call it at all. + void AsyncContext::_next() { + if (_awaiting) + _wait(); + else + _gotResult(); + } + + void AsyncContext::_wait() { - _waitingActor = Actor::currentActor(); // retain my actor while I'm waiting + _waitingActor = Actor::currentActor(); // retain the actor that's waiting _awaiting->setObserver(this); } + + void AsyncContext::_waitOn(AsyncContext *context) { + assert(!_awaiting); + _waitingSelf = this; // retain myself while waiting + _awaiting = context; + _wait(); + } + + void AsyncContext::wakeUp(AsyncContext *async) { assert(async == _awaiting); if (_waitingActor) { @@ -70,12 +72,19 @@ namespace litecore::actor { } } + void AsyncContext::_gotResult() { _ready = true; - auto observer = move(_observer); - if (observer) + if (auto observer = move(_observer)) observer->wakeUp(this); - _waitingSelf = nullptr; + _waitingSelf = nullptr; // release myself now that I'm done + } + + + AsyncBase::AsyncBase(AsyncContext *context, bool) + :AsyncBase(context) + { + _context->_start(); } } diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 39fc46a97..2368a4cf5 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -12,6 +12,7 @@ #pragma once #include "RefCounted.hh" +#include "InstanceCounted.hh" #include #include #include @@ -21,26 +22,41 @@ namespace litecore::actor { using fleece::RefCounted; using fleece::Retained; - class Actor; +// *** For full documentation, read Networking/BLIP/docs/Async.md *** + + +#pragma mark - ASYNC/AWAIT MACROS: + +/// Put this at the top of an async function/method that returns `Async`, +/// but below declarations of any variables that need to be in scope for the whole method. #define BEGIN_ASYNC_RETURNING(T) \ - return Async(_enclosingActor(), [=](AsyncState &_async_state_) mutable -> std::optional { \ + return Async(_thisActor(), [=](AsyncState &_async_state_) mutable -> std::optional { \ switch (_async_state_.currentLine()) { \ default: +/// Put this at the top of an async method that returns `void`. +/// See `BEGIN_ASYNC_RETURNING` for details. #define BEGIN_ASYNC() \ - Async(_enclosingActor(), [=](AsyncState &_async_state_) mutable -> void { \ + Async(_thisActor(), [=](AsyncState &_async_state_) mutable -> void { \ switch (_async_state_.currentLine()) { \ default: -#define asyncCall(VAR, CALL) \ - if (_async_state_._asyncCall(CALL, __LINE__)) return {}; \ +/// Use this in an async method to resolve an `Async<>` value, blocking until it's available. +/// `VAR` is the name of the variable to which to assign the result. +/// `EXPR` is the expression (usually a call to an async method) returning an `Async` value, +/// where `T` can be assigned to `VAR`. +/// If the `Async` value's result is already available, it is immediately assigned to `VAR` and +/// execution continues. +/// Otherwise, this method is suspended until the result becomes available. +#define AWAIT(VAR, EXPR) \ + if (_async_state_._await(EXPR, __LINE__)) return {}; \ case __LINE__: \ - VAR = _async_state_.asyncResult>();\ - _async_state_.reset(); + VAR = _async_state_.awaited>()->extractResult(); +/// Put this at the very end of an async function/method. #define END_ASYNC() \ } \ }); @@ -52,22 +68,17 @@ namespace litecore::actor { template class AsyncProvider; - /** The state data passed to the lambda of an async function. */ + // The state data passed to the lambda of an async function. Internal use only. class AsyncState { public: int currentLine() const {return _currentLine;} void reset() {_awaiting = nullptr;} - - bool _asyncCall(const AsyncBase &a, int curLine); - - template - T&& asyncResult() { - return dynamic_cast*>(_awaiting.get())->extractResult(); - } + bool _await(const AsyncBase &a, int curLine); + template Retained> awaited(); protected: Retained _awaiting; // What my fn body is suspended awaiting - int _currentLine {0}; // label/line# to continue lambda at + int _currentLine {0}; // label/line# to continue body function at }; @@ -75,21 +86,27 @@ namespace litecore::actor { // Maintains the context/state of an async operation and its observer. - // Abstract base class of AsyncProvider. - class AsyncContext : public RefCounted, protected AsyncState { + // Abstract base class of AsyncProvider and Async::AsyncWaiter. + class AsyncContext : public RefCounted, protected AsyncState, + public fleece::InstanceCountedIn + { public: bool ready() const {return _ready;} - void setObserver(AsyncContext *p); - void wakeUp(AsyncContext *async); + + template const T& result(); + template T&& extractResult(); protected: - AsyncContext(Actor*); - ~AsyncContext(); + friend class AsyncBase; + friend class Actor; + + explicit AsyncContext(Actor *actor = nullptr) :_actor(actor) { } void _start(); void _wait(); + void _waitOn(AsyncContext*); void _gotResult(); - virtual void _next() =0; + virtual void _next(); // Overridden by AsyncProvider and Async::AsyncWaiter. Retained _observer; // Dependent context waiting on me Actor* _actor; // Owning actor, if any @@ -97,176 +114,295 @@ namespace litecore::actor { Retained _waitingSelf; // Keeps `this` from being freed std::atomic _ready {false}; // True when result is ready -#if DEBUG - public: - static std::atomic_int gInstanceCount; -#endif - template friend class Async; - friend class Actor; + private: + void setObserver(AsyncContext *p); + void wakeUp(AsyncContext *async); }; - /** An asynchronously-provided result, seen from the producer side. */ + /** An asynchronously-provided result, seen from the producer's side. */ template class AsyncProvider : public AsyncContext { public: - template - explicit AsyncProvider(Actor *actor, LAMBDA body) - :AsyncContext(actor) - ,_body(std::move(body)) - { } + using ResultType = T; - static Retained create() {return new AsyncProvider;} + /// Creates a new empty AsyncProvider. + static Retained create() {return new AsyncProvider;} + + static Retained createReady(T&& r) {return new AsyncProvider(std::move(r));} + /// Creates the client-side view of the result. Async asyncValue() {return Async(this);} - void setResult(const T &result) {_result = result; _gotResult();} - void setResult(T &&result) {_result = std::move(result); _gotResult();} + /// Resolves the value by storing the result and waking any waking clients. + void setResult(const T &result) {precondition(!_result); + _result = result; _gotResult();} + /// Resolves the value by move-storing the result and waking any waking clients. + void setResult(T &&result) {precondition(!_result); + _result = std::move(result); + _gotResult();} + + /// Equivalent to `setResult` but constructs the T value directly inside the provider. + template >> + void emplaceResult(Args&&... args) { + precondition(!_result); + _result.emplace(args...); + _gotResult(); + } + + /// Returns the result, which must be available. + const T& result() const & {precondition(_result);return *_result;} + T&& result() && {return extractResult();} - const T& result() const {precondition(_result);return *_result;} + /// Moves the result to the caller. Result must be available. T&& extractResult() {precondition(_result); return *std::move(_result);} private: - AsyncProvider() :AsyncContext(nullptr) { } + friend class Async; + + using Body = std::function(AsyncState&)>; + + AsyncProvider() = default; + + explicit AsyncProvider(T&& result) + :_result(std::move(result)) + {_ready = true;} + + AsyncProvider(Actor *actor, Body &&body) + :AsyncContext(actor) + ,_body(std::move(body)) + { } void _next() override { _result = _body(*this); - if (_awaiting) { - _wait(); - } else { - precondition(_result); - _gotResult(); - } + assert(_result || _awaiting); + AsyncContext::_next(); } - std::function(AsyncState&)> _body; // The async function body - std::optional _result; // My result + Body _body; // The async function body + std::optional _result; // My result }; + // Specialization of AsyncProvider for use in functions with no return value (void). template <> class AsyncProvider : public AsyncContext { public: - template - explicit AsyncProvider(Actor *actor, LAMBDA body) - :AsyncContext(actor) - ,_body(std::move(body)) - { } - static Retained create() {return new AsyncProvider;} private: + friend class Async; + + using Body = std::function; + AsyncProvider() :AsyncContext(nullptr) { } + AsyncProvider(Actor *actor, Body &&body) + :AsyncContext(actor) + ,_body(std::move(body)) + { } + void _next() override { _body(*this); - if (_awaiting) - _wait(); - else - _gotResult(); + AsyncContext::_next(); } - std::function _body; // The async function body + Body _body; // The async function body }; #pragma mark - ASYNC: + /// Compile-time utility that pulls the result type out of an Async type. + /// If `T` is `Async`, or a reference thereto, then `async_result_type` is X. + template + using async_result_type = typename std::remove_reference_t::ResultType; + + namespace { + // Magic template gunk. `unwrap_async` removes a layer of `Async<...>` from a type: + // - `unwrap_async` is `string`. + // - `unwrap_async> is `string`. + template T _unwrap_async(T*); + template T _unwrap_async(Async*); + template using unwrap_async = decltype(_unwrap_async((T*)nullptr)); + } + + // base class of Async class AsyncBase { public: - explicit AsyncBase(const Retained &c) :_context(c) { } + /// Returns true once the result is available. bool ready() const {return _context->ready();} protected: - Retained _context; // The AsyncProvider that owns my value friend class AsyncState; + explicit AsyncBase(Retained &&context) :_context(std::move(context)) { } + explicit AsyncBase(AsyncContext *context, bool); + + Retained _context; // The AsyncProvider that owns my value }; - /** An asynchronously-provided result, seen from the client side. */ + /** An asynchronously-provided result, seen from the consumer's side. */ template class Async : public AsyncBase { public: using ResultType = T; + using AwaitReturnType = Async; + + /// Returns a new AsyncProvider. + static Retained> makeProvider() {return AsyncProvider::create();} + Async(T&& t) :AsyncBase(AsyncProvider::createReady(std::move(t))) { } Async(AsyncProvider *provider) :AsyncBase(provider) { } Async(const Retained> &provider) :AsyncBase(provider) { } - template - Async(Actor *actor, LAMBDA bodyFn) - :Async( new AsyncProvider(actor, std::move(bodyFn)) ) - { - _context->_start(); - } + Async(Actor *actor, typename AsyncProvider::Body bodyFn) + :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) + { } - /// Returns a new AsyncProvider. - static Retained> provider() {return AsyncProvider::create();} + /// Returns the result. (Will abort if the result is not yet available.) + const T& result() const & {return _context->result();} + T&& result() const && {return _context->result();} + + /// Returns the result. (Will abort if the result is not yet available.) + T&& extractResult() const {return _context->extractResult();} + + /// Invokes the callback when the result becomes ready (immediately if it's already ready.) + /// The callback should take a single parameter of type `T`, `T&` or `T&&`. + /// The callback's return type may be: + /// - `void` -- the `then` method will return `void`. + /// - `X` -- the `then` method will return `Async`, which will resolve to the callback's + /// return value after the callback returns. + /// - `Async` -- the `then` method will return `Async`. After the callback + /// returns, _and_ its returned async value becomes ready, the returned + /// async value will resolve to that value. + /// + /// Examples: + /// - `a.then([](T) -> void { ... });` + /// - `Async x = a.then([](T) -> X { ... });` + /// - `Async x = a.then([](T) -> Async { ... });` + template + auto then(LAMBDA callback) { + using U = unwrap_async>; // return type w/o Async<> + return _then(callback); + } - const T& result() const {return ((AsyncProvider*)_context.get())->result();} + private: + class AsyncWaiter; // defined below + + // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. + template + typename Async::AwaitReturnType _then(std::function callback) { + auto provider = Async::provider(); + if (ready()) { + provider->setResult(callback(extractResult())); + } else { + (void) new AsyncWaiter(_context, [provider,callback](T&& result) { + provider->setResult(callback(std::move(result))); + }); + } + return provider->asyncValue(); + } - /// Invokes the callback when the result becomes ready, or immediately if it's ready now. - template - void then(LAMBDA callback) { + // Implements `then` where the lambda returns void. (Specialization of above method.) + template<> + void _then(std::function callback) { if (ready()) - callback(result()); + callback(extractResult()); else (void) new AsyncWaiter(_context, std::move(callback)); } - private: - // Internal class used by `then()`, above - class AsyncWaiter : public AsyncContext { - public: - template - AsyncWaiter(AsyncContext *context, LAMBDA callback) - :AsyncContext(nullptr) - ,_callback(std::move(callback)) - { - _waitingSelf = this; // retain myself while waiting - _awaiting = context; - _wait(); - } - protected: - void _next() override { - _callback(asyncResult()); - _callback = nullptr; - _waitingSelf = nullptr; // release myself when done + // Implements `then` where the lambda returns `Async`. + template + Async _then(std::function(T&&)> callback) { + if (ready()) { + // If I'm ready, just call the callback and pass on the Async it returns: + return callback(extractResult()); + } else { + // Otherwise wait for my result... + auto provider = Async::makeProvider(); + (void) new AsyncWaiter(_context, [provider,callback](T&& result) { + // Invoke the callback, then wait to resolve the Async it returns: + Async u = callback(std::move(result)); + u.then([=](U &&uresult) { + // Then finally resolve the async I returned: + provider->setResult(std::move(uresult)); + }); + }); + return provider->asyncValue(); } - private: - std::function _callback; - }; + } }; - // Specialization of Async<> for functions with no result + // Specialization of Async<> for `void` type; not used directly. template <> class Async : public AsyncBase { public: + using AwaitReturnType = void; + + static Retained> makeProvider() {return AsyncProvider::create();} + Async(AsyncProvider *provider) :AsyncBase(provider) { } Async(const Retained> &provider) :AsyncBase(provider) { } - static Retained> provider() {return AsyncProvider::create();} - - template - Async(Actor *actor, const LAMBDA& bodyFn) - :Async( new AsyncProvider(actor, bodyFn) ) - { - _context->_start(); - } + Async(Actor *actor, typename AsyncProvider::Body bodyFn) + :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) + { } }; - /// Pulls the result type out of an Async type. - /// If `T` is `Async`, or a reference thereto, then `async_result_type` is X. - template - using async_result_type = typename std::remove_reference_t::ResultType; + // Implementation gunk... // Used by `BEGIN_ASYNC` macros. Returns the lexically enclosing actor instance, else NULL. - // (How? Outside of an Actor method, `_enclosingActor()` refers to the function below. - // In an Actor method, it refers to a method with the same name that returns `this`.) - static inline Actor* _enclosingActor() {return nullptr;} + // (How? Outside of an Actor method, `_thisActor()` refers to the function below. + // In an Actor method, it refers to `Actor::_thisActor()`, which returns `this`.) + static inline Actor* _thisActor() {return nullptr;} + + + template + Retained> AsyncState::awaited() { + // Downcasts `_awaiting` to the specific type of AsyncProvider, and clears it. + (void)dynamic_cast&>(*_awaiting); // runtime type-check + return reinterpret_cast>&&>(_awaiting); + } + + template + const T& AsyncContext::result() { + return dynamic_cast*>(this)->result(); + } + + template + T&& AsyncContext::extractResult() { + return dynamic_cast*>(this)->extractResult(); + } + + // Internal class used by `Async::then()`, above + template + class Async::AsyncWaiter : public AsyncContext { + public: + using Callback = std::function; + + AsyncWaiter(AsyncContext *context, Callback &&callback) + :AsyncContext(nullptr) + ,_callback(std::move(callback)) + { + _waitOn(context); + } + protected: + void _next() override { + _callback(awaited()->extractResult()); + _callback = nullptr; + _waitingSelf = nullptr; // release myself when done + // Does not call inherited method! + } + private: + Callback _callback; + }; } diff --git a/LiteCore/tests/ActorTest.cc b/LiteCore/tests/AsyncTest.cc similarity index 59% rename from LiteCore/tests/ActorTest.cc rename to LiteCore/tests/AsyncTest.cc index 3ca3031c6..bce56f7aa 100644 --- a/LiteCore/tests/ActorTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -1,5 +1,5 @@ // -// ActorTest.cc +// AsyncTest.cc // // Copyright 2022-Present Couchbase, Inc. // @@ -19,6 +19,32 @@ using namespace std; using namespace litecore::actor; +template +static T waitFor(Async &async) { + C4Log("Waiting..."); + optional result; + async.then([&](T &&c) { + result = c; + }); + while (!result) { + this_thread::sleep_for(10ms); + } + C4Log("...done waiting"); + return *result; +} + + +static Async downloader(string url) { + auto provider = Async::makeProvider(); + std::thread t([=] { + std::this_thread::sleep_for(1s); + provider->setResult("Contents of " + url); + }); + t.detach(); + return provider; +} + + static Retained> aProvider, bProvider; static Async provideA() { @@ -30,10 +56,14 @@ static Async provideB() { } static Async provideSum() { + Log("provideSum: entry"); string a, b; BEGIN_ASYNC_RETURNING(string) - asyncCall(a, provideA()); - asyncCall(b, provideB()); + Log("provideSum: awaiting A"); + AWAIT(a, provideA()); + Log("provideSum: awaiting B"); + AWAIT(b, provideB()); + Log("provideSum: returning"); return a + b; END_ASYNC() } @@ -42,7 +72,7 @@ static Async provideSum() { static Async provideSumPlus() { string a; BEGIN_ASYNC_RETURNING(string) - asyncCall(a, provideSum()); + AWAIT(a, provideSum()); return a + "!"; END_ASYNC() } @@ -61,7 +91,7 @@ static Async provideLoop() { int i = 0; BEGIN_ASYNC_RETURNING(int) for (i = 0; i < 10; i++) { - asyncCall(n, provideSum()); + AWAIT(n, provideSum()); //fprintf(stderr, "n=%f, i=%d, sum=%f\n", n, i, sum); sum += n.size() * i; } @@ -75,8 +105,8 @@ static string provideNothingResult; static void provideNothing() { string a, b; BEGIN_ASYNC() - asyncCall(a, provideA()); - asyncCall(b, provideB()); + AWAIT(a, provideA()); + AWAIT(b, provideB()); provideNothingResult = a + b; END_ASYNC() } @@ -84,8 +114,8 @@ static void provideNothing() { TEST_CASE("Async", "[Async]") { - aProvider = Async::provider(); - bProvider = Async::provider(); + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); { Async sum = provideSum(); REQUIRE(!sum.ready()); @@ -96,13 +126,12 @@ TEST_CASE("Async", "[Async]") { REQUIRE(sum.result() == "hi there"); } aProvider = bProvider = nullptr; - CHECK(AsyncContext::gInstanceCount == 0); } TEST_CASE("Async, other order", "[Async]") { - aProvider = Async::provider(); - bProvider = Async::provider(); + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); { Async sum = provideSum(); REQUIRE(!sum.ready()); @@ -113,17 +142,26 @@ TEST_CASE("Async, other order", "[Async]") { REQUIRE(sum.result() == "hi there"); } aProvider = bProvider = nullptr; - CHECK(AsyncContext::gInstanceCount == 0); +} + + +TEST_CASE("Async, emplaceResult") { + auto p = Async::makeProvider(); + auto v = p->asyncValue(); + REQUIRE(!v.ready()); + p->emplaceResult('*', 6); + REQUIRE(v.ready()); + CHECK(v.result() == "******"); } TEST_CASE("AsyncWaiter", "[Async]") { - aProvider = Async::provider(); - bProvider = Async::provider(); + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); { Async sum = provideSum(); string result; - sum.then([&](string s) { + sum.then([&](string &&s) { result = s; }); REQUIRE(!sum.ready()); @@ -136,13 +174,12 @@ TEST_CASE("AsyncWaiter", "[Async]") { REQUIRE(result == "hi there"); } aProvider = bProvider = nullptr; - CHECK(AsyncContext::gInstanceCount == 0); } TEST_CASE("Async, 2 levels", "[Async]") { - aProvider = Async::provider(); - bProvider = Async::provider(); + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); { Async sum = provideSumPlus(); REQUIRE(!sum.ready()); @@ -153,28 +190,26 @@ TEST_CASE("Async, 2 levels", "[Async]") { REQUIRE(sum.result() == "hi there!"); } aProvider = bProvider = nullptr; - CHECK(AsyncContext::gInstanceCount == 0); } TEST_CASE("Async, loop", "[Async]") { - aProvider = Async::provider(); - bProvider = Async::provider(); + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); { Async sum = provideLoop(); for (int i = 1; i <= 10; i++) { REQUIRE(!sum.ready()); aProvider->setResult("hi"); REQUIRE(!sum.ready()); - aProvider = Async::provider(); + aProvider = Async::makeProvider(); bProvider->setResult(" there"); - bProvider = Async::provider(); + bProvider = Async::makeProvider(); } REQUIRE(sum.ready()); REQUIRE(sum.result() == 360); } aProvider = bProvider = nullptr; - CHECK(AsyncContext::gInstanceCount == 0); } @@ -184,13 +219,12 @@ TEST_CASE("Async, immediately", "[Async]") { REQUIRE(im.ready()); REQUIRE(im.result() == "immediately"); } - CHECK(AsyncContext::gInstanceCount == 0); } TEST_CASE("Async void fn", "[Async]") { - aProvider = Async::provider(); - bProvider = Async::provider(); + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); provideNothingResult = ""; { provideNothing(); @@ -201,36 +235,70 @@ TEST_CASE("Async void fn", "[Async]") { REQUIRE(provideNothingResult == "hi there"); } aProvider = bProvider = nullptr; - CHECK(AsyncContext::gInstanceCount == 0); } -#pragma mark - WITH ACTORS: +TEST_CASE("Async then returning void", "[Async]") { + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); + optional result; + provideSum().then([&](string &&s) { + Log("--Inside then fn; s = \"%s\"", s.c_str()); + result = s; + }); -static Async downloader(string url) { - auto provider = Async::provider(); - std::thread t([=] { - std::this_thread::sleep_for(1s); - provider->setResult("Contents of " + url); + Log("--Providing aProvider"); + aProvider->setResult("hi"); + Log("--Providing bProvider"); + bProvider->setResult(" there"); + CHECK(result == "hi there"); + + aProvider = bProvider = nullptr; +} + + +TEST_CASE("Async then returning T", "[Async]") { + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); + + Async size = provideSum().then([](string &&s) { + Log("--Inside then fn; s = \"%s\", returning %zu", s.c_str(), s.size()); + return s.size(); }); - t.detach(); - return provider; + + Log("--Providing aProvider"); + aProvider->setResult("hi"); + Log("--Providing bProvider"); + bProvider->setResult(" there"); + CHECK(waitFor(size) == 8); + + aProvider = bProvider = nullptr; } -static string waitFor(Async &async) { - optional contents; - async.then([&](string c) { - contents = c; +TEST_CASE("Async then returning async T", "[Async]") { + aProvider = Async::makeProvider(); + bProvider = Async::makeProvider(); + + Async dl = provideSum().then([](string &&s) { + Log("--Inside then fn; s = \"%s\", returning %zu", s.c_str(), s.size()); + return downloader(s); }); - while (!contents) { - this_thread::sleep_for(10ms); - } - return *contents; + + Log("--Providing aProvider"); + aProvider->setResult("hi"); + Log("--Providing bProvider"); + bProvider->setResult(" there"); + CHECK(waitFor(dl) == "Contents of hi there"); + + aProvider = bProvider = nullptr; } +#pragma mark - WITH ACTORS: + + class TestActor : public Actor { public: TestActor() :Actor(kC4Cpp_DefaultLog) { } @@ -239,7 +307,7 @@ class TestActor : public Actor { string contents; BEGIN_ASYNC_RETURNING(string) CHECK(currentActor() == this); - asyncCall(contents, downloader(url)); + AWAIT(contents, downloader(url)); CHECK(currentActor() == this); return contents; END_ASYNC() @@ -252,12 +320,24 @@ class TestActor : public Actor { CHECK(currentActor() == this); dl1 = download(url1); dl2 = download(url2); - asyncCall(contents, *dl1); + AWAIT(contents, *dl1); CHECK(currentActor() == this); - asyncCall(string contents2, *dl2); + AWAIT(string contents2, *dl2); return contents + " and " + contents2; END_ASYNC() } + + void testThen(string url) { + BEGIN_ASYNC() + downloader(url).then([=](string &&s) { + // When `then` is used inside an Actor method, the lambda must be called on its queue: + CHECK(currentActor() == this); + testThenResult = move(s); + }); + END_ASYNC() + } + + optional testThenResult; }; @@ -282,3 +362,11 @@ TEST_CASE("Async Actor Twice", "[Async]") { string contents = waitFor(asyncContents); CHECK(contents == "Contents of couchbase.org and Contents of couchbase.biz"); } + +TEST_CASE("Async Actor with then", "[Async]") { + auto actor = make_retained(); + actor->testThen("couchbase.xxx"); + while (!actor->testThenResult) + this_thread::sleep_for(10ms); + CHECK(actor->testThenResult == "Contents of couchbase.xxx"); +} diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md index e7232983c..ad41c07e8 100644 --- a/Networking/BLIP/docs/Async.md +++ b/Networking/BLIP/docs/Async.md @@ -1,41 +1,99 @@ # The Async API +(Last updated Feb 4 2022 by Jens) + ## Asynchronous Values (Futures) -`Async` represents a value of type `T` that may not be available yet. This concept is also referred to as a ["future"](https://en.wikipedia.org/wiki/Futures_and_promises). +`Async` represents a value of type `T` that may not be available yet. This concept is also referred to as a ["future"](https://en.wikipedia.org/wiki/Futures_and_promises). You can keep it around like a normal value type, but you can’t get the underlying value until it becomes available. + +> You can think of `Async` as sort of like `std::optional`, but you can’t store a value in it yourself, only wait until something else (the *producer*) does. + +An `Async` has a matching object, `AsyncProvider`, that belongs to whomever is responsible for creating that `T` value. (This is sometimes referred to as a “promise”.) The producer keeps it around, probably in a `Retained<>` wrapper, until such time as the result becomes available, then calls `setResult` on it to store the value into its matching `Async`. -You can create one by first creating an `AsyncProvider`, which is also known as a "promise", then calling its `asyncValue` method: +#### Example + +Here’s a rather dumbed-down example of sending a request to a server and getting a response: ```c++ Async getIntFromServer() { - Retained> intProvider = Async::provider(); - sendServerRequestFor(intProvider); + _curProvider = Async::makeProvider(); + sendServerRequest(); return intProvider->asyncValue(); } ``` -You can simplify this somewhat: +Internally, this uses an `AsyncProvider` reference to keep track of the current request (I told you this was dumbed down!), so when the response arrives it can store it into the provider and thereby into the caller’s `Async` value: ```c++ -Async getIntFromServer() { - auto intProvider = Async::provider(); // `auto` is your friend - sendServerRequestFor(intProvider); - return intProvider; // implicit conversion to Async +static Retained> _curProvider; + +static void receivedResponseFromServer(int result) { + _curProvider.setResult(result); + _curProvider = nullptr; } ``` -The `AsyncProvider` reference has to be stored somewhere until the result is available. Then you call its `setResult()` method: +On the calling side you can start the request, go on your merry way, and then later get the value once it’s ready: + +```c++ +Async request = getIntFromServer(); // returns immediately! +//... do other stuff ... + +//... later, when the response is available: +int i = request.result(); +cout << "Server says: " << i << "!\n"; +``` + +Only … when is “later”, exactly? How do you know? + +## Getting The Result With `then` + +You can’t call `Async::result()` before the result is available, or Bad Stuff happens, like a fatal exception. We don’t want anything to block; that’s the point of async! + +There’s a safe `ready()` method that returns `true` after the result is available. But obviously it would be a bad idea to do something like `while (!request.ready()) { }` … + +So how do you wait for the result? **You don’t.** Instead you let the Async call you, by calling its `then` method to register a lambda function that will be called with the result when it’s available: + +```c++ +Async request = getIntFromServer(); +request.then([](int i) { + std::cout << "The result is " << i << "!\n"; +}); +``` + +What if you need that lambda to return a value? That value won’t be available until later when the lambda runs, but you can get it now as an `Async`: + +```c++ +Async message = getIntFromServer().then([](int i) { + return "The result is " + std::stoi(i) + "!"; +}); +``` + +This works even if the inner lambda itself returns an `Async`: ```c++ -int result = valueReceivedFromServer(); -intProvider.setResult(result); +extern Async storeIntOnServer(int); + +Async message = getIntFromServer().then([](int i) { + return storeIntOnServer(i + 1); +}); ``` -`Async` has a `ready` method that tells whether the result is available, and a `result` method that returns the result (or aborts if it's not available.) However, it does not provide any way to block and wait for the result. That's intentional: we don't want blocking! Instead, the way you work with async results is within an _asynchronous function_. +In this situation it can be useful to chain multiple `then` calls: + +```c++ +Async message = getIntFromServer().then([](int i) { + return storeIntOnServer(i + 1); +}).then([](Status s) { + return status == Ok ? "OK!" : "Failure"; +}); +``` ## Asynchronous Functions -An asynchronous function is a function that can resolve `Async` values in a way that appears synchronous, but without actually blocking. It always returns an `Async` result (or void), since if the `Async` value it's resolving isn't available, the function itself has to return without (yet) providing a result. +An asynchronous function is a function that can resolve `Async` values in a way that *appears* synchronous, but without actually blocking. It lets you write code that looks more linear, without a bunch of “…then…”s in it. The bad news is that it’s reliant on some weird macros that uglify your code a bit. + +An async function always returns an `Async` result, or void, since if an `Async` value it's resolving isn't available, the function itself has to return without (yet) providing a result. > This is very much modeled on the "async/await" feature found in many other languages, like C#, JavaScript and Swift. C++20 has it too, under the name "coroutines", but we can't use C++20 yet. @@ -60,48 +118,99 @@ void aVoidAsyncFunction() { } ``` -In between `BEGIN_ASYNC` and `END_ASYNC` you can "unwrap" `Async` values, such as those returned by other asynchronous functions, by using the `asyncCall()` macro. The first parameter is the variable to assign the result to, and the second is the expression returning the async result: +In between `BEGIN_ASYNC` and `END_ASYNC` you can "unwrap" `Async` values, such as those returned by other asynchronous functions, by using the `AWAIT()` macro. The first parameter is the variable to assign the result to, and the second is the expression returning the async result: ```c++ -asyncCall(int n, someOtherAsyncFunction()); +AWAIT(n, someOtherAsyncFunction()); ``` -> TMI: `asyncCall` is a macro that hides some very weird control flow. It first evaluates the second parameter to get its `Async` value. If that value isn't yet available, `asyncCall` _causes the enclosing function to return_. (Obviously it returns an unavailable `Async` value.) It also registers as an observer of the async value, so when its result does become available, the enclosing function _resumes_ right where it left off (🤯), assigns the result to the variable, and continues. +This means “call `someOtherAsyncFunction()` [which returns an `Async`], *suspend this function* until that `Async`’s result becomes available, then assign the result to `n`.” + +The weird part is that “suspend” doesn’t actually mean “block the current thread.” Instead it temporarily returns from the function (giving the caller an Async value as a placeholder), but when the value becomes available it resumes the function where it left off. This reduces the need for multiple threads, and in an Actor it lets you handle other messages while the current one is suspended. + +> TMI: `AWAIT` is a macro that hides some very weird control flow. It first evaluates the second parameter to get its `Async` value. If that value isn't yet available, `AWAIT` _causes the enclosing function to return_. (Obviously it returns an unavailable `Async` value.) It also registers as an observer of the async value, so when its result does become available, the enclosing function _resumes_ right at the line where it left off (🤯), assigns the result to the variable, and continues. -## Async Calls and Variables' Scope +### Variable Scope Inside ASYNC functions -The weirdness inside `asyncCall()` places some odd restrictions on your code. Most importantly, **a variable declared between `BEGIN_ASYNC()` and `END_ASYNC()` cannot have a scope that extends across an `asyncCall`**: +The weirdness inside `AWAIT()` places some restrictions on your code. Most importantly, **a variable declared between `BEGIN_ASYNC()` and `END_ASYNC()` cannot have a scope that extends across an `AWAIT`**: ```c++ +BEGIN_ASYNC() int foo = ....; -asyncCall(int n, someOtherAsyncFunction()); // ERROR: "Cannot jump from switch..." +AWAIT(int n, someOtherAsyncFunction()); // ERROR: "Cannot jump from switch..." ``` -> TMI: This is because the macro expansion of an async function wraps a `switch` statement around its body, and the expansion of `asyncCall()` contains a `case` label. The C++ language does not allow a jump to a `case` to skip past a variable declaration. +> TMI: This is because the macro expansion of an async function wraps a `switch` statement around its body, and the expansion of `AWAIT()` contains a `case` label. The C++ language does not allow a jump to a `case` to skip past a variable declaration. -If you want to use a variable across `asyncCall` scopes, you must **declare it _before_ the `BEGIN_ASYNC`** -- its scope then includes the entire async function: +If you want to use a variable across `AWAIT` calls, you must **declare it _before_ the `BEGIN_ASYNC`** -- its scope then includes the entire async function: ```c++ int foo; -BEGIN_ASYNC_RETURNING(T) +BEGIN_ASYNC() ... foo = ....; -asyncCall(int n, someOtherAsyncFunction()); // OK! +AWAIT(int n, someOtherAsyncFunction()); // OK! foo += n; ``` -Or if the variable isn't used after the next `asyncCall`, just use braces to limit its scope: +Or if the variable isn't used after the next `AWAIT`, just use braces to limit its scope: ```c++ +BEGIN_ASYNC() { int foo = ....; } -asyncCall(int n, someOtherAsyncFunction()); // OK! +AWAIT(int n, someOtherAsyncFunction()); // OK! ``` ## Threading -By default, an async method resumes immediately when the `Async` value it's waiting for becomes available. That means when the provider's `setResult` method is called, or when the async method returning that value finally returns a result, the waiting method will synchronously resume. This is reasonable in single- threaded code. +By default, an async function starts immediately (as you’d expect) and runs until it either returns a value, or blocks in an AWAIT call on an Async value that isn’t ready. In the latter case, it returns to the caller, but its Async result isn’t ready yet. + +When the `Async` value it's waiting for becomes available, the blocked function resumes *immediately*. That means: when the provider's `setResult` method is called, or when the async method returning that value finally returns a result, the waiting method will synchronously resume. + +### Async and Actors + +These are reasonable behaviors in single- threaded code … not for [Actors](Actors.md), though. An Actors is single-threaded, so code belonging to an Actor should only run “on the Actor’s queue”, i.e. when no other code belonging to that Actor is running. + +Fortunately, `BEGIN_ASYNC` and `AWAIT` are aware of Actors, and have special behaviors when the function they’re used in is a method of an Actor subclass: + +* `BEGIN_ASYNC` checks whether the current thread is already running as that Actor. If not, it doesn’t start yet, but schedules the function on the Actor’s queue. +* When `AWAIT` resumes the function, it schedules it on the Actor's queue. + +### Easier Actors Without Async -`asyncCall` is aware of [Actors](Actors.md), however. So if an async Actor method waits, it will be resumed on that Actor's execution context. This ensures that the Actor's code runs single-threaded, as expected. +One benefit of this is that Actor methods using `BEGIN/END_ASYNC` don’t need the usual idiom where the public method enqueues a call to a matching private method: + +```c++ +// The old way ... In Twiddler.hh: +class Twiddler : public Actor { +public: + void twiddle(int n) {enqueue(FUNCTION_TO_QUEUE(Twiddler::_twiddle), n);} +private: + void _twiddle(int n); +}; + +// The old way ... In Twiddler.cc: +void Twiddler::_twiddle(int n) { + ... actual implementation ... +} +``` + +Instead, BEGIN_ASYNC lets the public method enqueue itself and return, without the need for a separate method: + +```c++ +// The new way ... In Twiddler.hh: +class Twiddler : public Actor { +public: + void twiddle(int n); +}; + +// The new way ... In Twiddler.cc: +void Twiddler::twiddle(int n) { + BEGIN_ASYNC() + ... actual implementation ... + END_ASYNC() +} +``` diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 370266747..522109f7b 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1201,6 +1201,7 @@ 2777146C1C5D6BDB003C0287 /* static_lib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = static_lib.xcconfig; sourceTree = ""; }; 2779CC6E1E85E4FC00F0D251 /* ReplicatorTypes.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ReplicatorTypes.hh; sourceTree = ""; }; 277C5C4527AB1EB4001BE212 /* ActorTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ActorTest.cc; sourceTree = ""; }; + 277C5C4827AC4E0E001BE212 /* Async.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Async.md; sourceTree = ""; }; 277CB6251D0DED5E00702E56 /* Fleece.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Fleece.xcodeproj; path = fleece/Fleece.xcodeproj; sourceTree = ""; }; 277D19C9194E295B008E91EB /* Error.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Error.hh; sourceTree = ""; }; 277FEE5721ED10FA00B60E3C /* ReplicatorSGTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ReplicatorSGTest.cc; sourceTree = ""; }; @@ -2914,6 +2915,7 @@ isa = PBXGroup; children = ( 27DF3BC326867F2600A57A1E /* Actors.md */, + 277C5C4827AC4E0E001BE212 /* Async.md */, 27DF3BC426867F2600A57A1E /* logo.png */, 27DF3BC526867F2600A57A1E /* BLIP Protocol.md */, ); From 00cfb3012bf38f8c61b9ae1c3c3082eae5e4b346 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Feb 2022 14:41:51 -0800 Subject: [PATCH 03/78] More Async --- LiteCore/Support/Async.cc | 22 ++ LiteCore/Support/Async.hh | 14 +- LiteCore/tests/AsyncTest.cc | 297 ++++++++++------------- Xcode/LiteCore.xcodeproj/project.pbxproj | 8 +- 4 files changed, 166 insertions(+), 175 deletions(-) diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 46ae45b36..bf5f046fa 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -16,6 +16,28 @@ namespace litecore::actor { + + void AsyncState::asyncVoidFn(Actor *actor, std::function body) { + if (actor && actor != Actor::currentActor()) { + // Need to run this on the Actor's queue, so schedule it: + auto provider = retained(new AsyncProvider(actor, std::move(body))); + provider->_start(); + } else { + // It's OK to call the body synchronously. As an optimization, pass it a plain + // stack-based AsyncState instead of a heap-allocated AsyncProvider: + AsyncState state; + body(state); + if (state._awaiting) { + // Body didn't finish, is "blocked" in an AWAIT(), so now set up a proper context: + auto provider = retained(new AsyncProvider(actor, + std::move(body), + std::move(state))); + provider->_wait(); + } + } + } + + bool AsyncState::_await(const AsyncBase &a, int curLine) { _awaiting = a._context; _currentLine = curLine; diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 2368a4cf5..9ff760bb8 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -40,7 +40,7 @@ namespace litecore::actor { /// Put this at the top of an async method that returns `void`. /// See `BEGIN_ASYNC_RETURNING` for details. #define BEGIN_ASYNC() \ - Async(_thisActor(), [=](AsyncState &_async_state_) mutable -> void { \ + return AsyncState::asyncVoidFn(_thisActor(), [=](AsyncState &_async_state_) mutable -> void { \ switch (_async_state_.currentLine()) { \ default: @@ -76,6 +76,8 @@ namespace litecore::actor { bool _await(const AsyncBase &a, int curLine); template Retained> awaited(); + static void asyncVoidFn(Actor *actor, std::function body); + protected: Retained _awaiting; // What my fn body is suspended awaiting int _currentLine {0}; // label/line# to continue body function at @@ -97,6 +99,7 @@ namespace litecore::actor { template T&& extractResult(); protected: + friend class AsyncState; friend class AsyncBase; friend class Actor; @@ -194,6 +197,7 @@ namespace litecore::actor { private: friend class Async; + friend class AsyncState; using Body = std::function; @@ -204,6 +208,12 @@ namespace litecore::actor { ,_body(std::move(body)) { } + AsyncProvider(Actor *actor, Body &&body, AsyncState &&state) + :AsyncProvider(actor, std::move(body)) + { + ((AsyncState&)*this) = std::move(state); + } + void _next() override { _body(*this); AsyncContext::_next(); @@ -239,7 +249,7 @@ namespace litecore::actor { protected: friend class AsyncState; explicit AsyncBase(Retained &&context) :_context(std::move(context)) { } - explicit AsyncBase(AsyncContext *context, bool); + explicit AsyncBase(AsyncContext *context, bool); // calls context->_start() Retained _context; // The AsyncProvider that owns my value }; diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index bce56f7aa..51894d302 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -45,107 +45,101 @@ static Async downloader(string url) { } -static Retained> aProvider, bProvider; +class AsyncTest { +public: + Retained> aProvider = Async::makeProvider(); + Retained> bProvider = Async::makeProvider(); -static Async provideA() { - return aProvider; -} + Async provideA() { + return aProvider; + } -static Async provideB() { - return bProvider; -} + Async provideB() { + return bProvider; + } -static Async provideSum() { - Log("provideSum: entry"); - string a, b; - BEGIN_ASYNC_RETURNING(string) - Log("provideSum: awaiting A"); - AWAIT(a, provideA()); - Log("provideSum: awaiting B"); - AWAIT(b, provideB()); - Log("provideSum: returning"); - return a + b; - END_ASYNC() -} + Async provideSum() { + Log("provideSum: entry"); + string a, b; + BEGIN_ASYNC_RETURNING(string) + Log("provideSum: awaiting A"); + AWAIT(a, provideA()); + Log("provideSum: awaiting B"); + AWAIT(b, provideB()); + Log("provideSum: returning"); + return a + b; + END_ASYNC() + } -static Async provideSumPlus() { - string a; - BEGIN_ASYNC_RETURNING(string) - AWAIT(a, provideSum()); - return a + "!"; - END_ASYNC() -} + Async provideSumPlus() { + string a; + BEGIN_ASYNC_RETURNING(string) + AWAIT(a, provideSum()); + return a + "!"; + END_ASYNC() + } -static Async provideImmediately() { - BEGIN_ASYNC_RETURNING(string) - return "immediately"; - END_ASYNC() -} + Async provideImmediately() { + BEGIN_ASYNC_RETURNING(string) + return "immediately"; + END_ASYNC() + } -static Async provideLoop() { - string n; - int sum = 0; - int i = 0; - BEGIN_ASYNC_RETURNING(int) - for (i = 0; i < 10; i++) { - AWAIT(n, provideSum()); - //fprintf(stderr, "n=%f, i=%d, sum=%f\n", n, i, sum); - sum += n.size() * i; + Async provideLoop() { + string n; + int sum = 0; + int i = 0; + BEGIN_ASYNC_RETURNING(int) + for (i = 0; i < 10; i++) { + AWAIT(n, provideSum()); + //fprintf(stderr, "n=%f, i=%d, sum=%f\n", n, i, sum); + sum += n.size() * i; + } + return sum; + END_ASYNC() } - return sum; - END_ASYNC() -} -static string provideNothingResult; + string provideNothingResult; -static void provideNothing() { - string a, b; - BEGIN_ASYNC() - AWAIT(a, provideA()); - AWAIT(b, provideB()); - provideNothingResult = a + b; - END_ASYNC() -} + void provideNothing() { + string a, b; + BEGIN_ASYNC() + AWAIT(a, provideA()); + AWAIT(b, provideB()); + provideNothingResult = a + b; + END_ASYNC() + } +}; -TEST_CASE("Async", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - { - Async sum = provideSum(); - REQUIRE(!sum.ready()); - aProvider->setResult("hi"); - REQUIRE(!sum.ready()); - bProvider->setResult(" there"); - REQUIRE(sum.ready()); - REQUIRE(sum.result() == "hi there"); - } - aProvider = bProvider = nullptr; +TEST_CASE_METHOD(AsyncTest, "Async", "[Async]") { + Async sum = provideSum(); + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + bProvider->setResult(" there"); + REQUIRE(sum.ready()); + REQUIRE(sum.result() == "hi there"); } -TEST_CASE("Async, other order", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - { - Async sum = provideSum(); - REQUIRE(!sum.ready()); - bProvider->setResult(" there"); // this time provideB() finishes first - REQUIRE(!sum.ready()); - aProvider->setResult("hi"); - REQUIRE(sum.ready()); - REQUIRE(sum.result() == "hi there"); - } - aProvider = bProvider = nullptr; +TEST_CASE_METHOD(AsyncTest, "Async, other order", "[Async]") { + Async sum = provideSum(); + REQUIRE(!sum.ready()); + bProvider->setResult(" there"); // this time provideB() finishes first + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(sum.ready()); + REQUIRE(sum.result() == "hi there"); } -TEST_CASE("Async, emplaceResult") { +TEST_CASE_METHOD(AsyncTest, "Async, emplaceResult") { auto p = Async::makeProvider(); auto v = p->asyncValue(); REQUIRE(!v.ready()); @@ -155,93 +149,67 @@ TEST_CASE("Async, emplaceResult") { } -TEST_CASE("AsyncWaiter", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - { - Async sum = provideSum(); - string result; - sum.then([&](string &&s) { - result = s; - }); - REQUIRE(!sum.ready()); - REQUIRE(result == ""); - aProvider->setResult("hi"); - REQUIRE(!sum.ready()); - REQUIRE(result == ""); - bProvider->setResult(" there"); - REQUIRE(sum.ready()); - REQUIRE(result == "hi there"); - } - aProvider = bProvider = nullptr; +TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { + Async sum = provideSum(); + string result; + sum.then([&](string &&s) { + result = s; + }); + REQUIRE(!sum.ready()); + REQUIRE(result == ""); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + REQUIRE(result == ""); + bProvider->setResult(" there"); + REQUIRE(sum.ready()); + REQUIRE(result == "hi there"); +} + + +TEST_CASE_METHOD(AsyncTest, "Async, 2 levels", "[Async]") { + Async sum = provideSumPlus(); + REQUIRE(!sum.ready()); + aProvider->setResult("hi"); + REQUIRE(!sum.ready()); + bProvider->setResult(" there"); + REQUIRE(sum.ready()); + REQUIRE(sum.result() == "hi there!"); } -TEST_CASE("Async, 2 levels", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - { - Async sum = provideSumPlus(); +TEST_CASE_METHOD(AsyncTest, "Async, loop", "[Async]") { + Async sum = provideLoop(); + for (int i = 1; i <= 10; i++) { REQUIRE(!sum.ready()); aProvider->setResult("hi"); REQUIRE(!sum.ready()); + aProvider = Async::makeProvider(); bProvider->setResult(" there"); - REQUIRE(sum.ready()); - REQUIRE(sum.result() == "hi there!"); + bProvider = Async::makeProvider(); } - aProvider = bProvider = nullptr; + REQUIRE(sum.ready()); + REQUIRE(sum.result() == 360); } -TEST_CASE("Async, loop", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - { - Async sum = provideLoop(); - for (int i = 1; i <= 10; i++) { - REQUIRE(!sum.ready()); - aProvider->setResult("hi"); - REQUIRE(!sum.ready()); - aProvider = Async::makeProvider(); - bProvider->setResult(" there"); - bProvider = Async::makeProvider(); - } - REQUIRE(sum.ready()); - REQUIRE(sum.result() == 360); - } - aProvider = bProvider = nullptr; +TEST_CASE_METHOD(AsyncTest, "Async, immediately", "[Async]") { + Async im = provideImmediately(); + REQUIRE(im.ready()); + REQUIRE(im.result() == "immediately"); } -TEST_CASE("Async, immediately", "[Async]") { - { - Async im = provideImmediately(); - REQUIRE(im.ready()); - REQUIRE(im.result() == "immediately"); - } -} - - -TEST_CASE("Async void fn", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - provideNothingResult = ""; - { - provideNothing(); - REQUIRE(provideNothingResult == ""); - aProvider->setResult("hi"); - REQUIRE(provideNothingResult == ""); - bProvider->setResult(" there"); - REQUIRE(provideNothingResult == "hi there"); - } - aProvider = bProvider = nullptr; +TEST_CASE_METHOD(AsyncTest, "Async void fn", "[Async]") { + provideNothing(); + REQUIRE(provideNothingResult == ""); + aProvider->setResult("hi"); + REQUIRE(provideNothingResult == ""); + bProvider->setResult(" there"); + REQUIRE(provideNothingResult == "hi there"); } -TEST_CASE("Async then returning void", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - +TEST_CASE_METHOD(AsyncTest, "Async then returning void", "[Async]") { optional result; provideSum().then([&](string &&s) { Log("--Inside then fn; s = \"%s\"", s.c_str()); @@ -253,15 +221,10 @@ TEST_CASE("Async then returning void", "[Async]") { Log("--Providing bProvider"); bProvider->setResult(" there"); CHECK(result == "hi there"); - - aProvider = bProvider = nullptr; } -TEST_CASE("Async then returning T", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - +TEST_CASE_METHOD(AsyncTest, "Async then returning T", "[Async]") { Async size = provideSum().then([](string &&s) { Log("--Inside then fn; s = \"%s\", returning %zu", s.c_str(), s.size()); return s.size(); @@ -272,15 +235,10 @@ TEST_CASE("Async then returning T", "[Async]") { Log("--Providing bProvider"); bProvider->setResult(" there"); CHECK(waitFor(size) == 8); - - aProvider = bProvider = nullptr; } -TEST_CASE("Async then returning async T", "[Async]") { - aProvider = Async::makeProvider(); - bProvider = Async::makeProvider(); - +TEST_CASE_METHOD(AsyncTest, "Async then returning async T", "[Async]") { Async dl = provideSum().then([](string &&s) { Log("--Inside then fn; s = \"%s\", returning %zu", s.c_str(), s.size()); return downloader(s); @@ -291,17 +249,15 @@ TEST_CASE("Async then returning async T", "[Async]") { Log("--Providing bProvider"); bProvider->setResult(" there"); CHECK(waitFor(dl) == "Contents of hi there"); - - aProvider = bProvider = nullptr; } #pragma mark - WITH ACTORS: -class TestActor : public Actor { +class AsyncTestActor : public Actor { public: - TestActor() :Actor(kC4Cpp_DefaultLog) { } + AsyncTestActor() :Actor(kC4Cpp_DefaultLog) { } Async download(string url) { string contents; @@ -333,10 +289,12 @@ class TestActor : public Actor { // When `then` is used inside an Actor method, the lambda must be called on its queue: CHECK(currentActor() == this); testThenResult = move(s); + testThenReady = true; }); END_ASYNC() } + atomic testThenReady = false; optional testThenResult; }; @@ -349,7 +307,7 @@ TEST_CASE("Async on thread", "[Async]") { TEST_CASE("Async Actor", "[Async]") { - auto actor = make_retained(); + auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org"); string contents = waitFor(asyncContents); CHECK(contents == "Contents of couchbase.org"); @@ -357,16 +315,17 @@ TEST_CASE("Async Actor", "[Async]") { TEST_CASE("Async Actor Twice", "[Async]") { - auto actor = make_retained(); + auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org", "couchbase.biz"); string contents = waitFor(asyncContents); CHECK(contents == "Contents of couchbase.org and Contents of couchbase.biz"); } TEST_CASE("Async Actor with then", "[Async]") { - auto actor = make_retained(); + auto actor = make_retained(); actor->testThen("couchbase.xxx"); - while (!actor->testThenResult) + CHECK(!actor->testThenReady); + while (!actor->testThenReady) this_thread::sleep_for(10ms); CHECK(actor->testThenResult == "Contents of couchbase.xxx"); } diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 522109f7b..b985b4ce9 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -219,7 +219,7 @@ 27727C55230F279D0082BCC9 /* HTTPLogic.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27727C53230F279D0082BCC9 /* HTTPLogic.cc */; }; 27766E161982DA8E00CAA464 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; 2776AA272087FF6B004ACE85 /* LegacyAttachments.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2776AA252087FF6B004ACE85 /* LegacyAttachments.cc */; }; - 277C5C4627AB1EB4001BE212 /* ActorTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277C5C4527AB1EB4001BE212 /* ActorTest.cc */; }; + 277C5C4627AB1EB4001BE212 /* AsyncTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 277C5C4527AB1EB4001BE212 /* AsyncTest.cc */; }; 277C5C4727AB1F02001BE212 /* Async.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2744B336241854F2005A194D /* Async.cc */; }; 2783DF991D27436700F84E6E /* c4ThreadingTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2783DF981D27436700F84E6E /* c4ThreadingTest.cc */; }; 2787EB271F4C91B000DB97B0 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27766E151982DA8E00CAA464 /* Security.framework */; }; @@ -1200,7 +1200,7 @@ 2776AA262087FF6B004ACE85 /* LegacyAttachments.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LegacyAttachments.hh; sourceTree = ""; }; 2777146C1C5D6BDB003C0287 /* static_lib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = static_lib.xcconfig; sourceTree = ""; }; 2779CC6E1E85E4FC00F0D251 /* ReplicatorTypes.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ReplicatorTypes.hh; sourceTree = ""; }; - 277C5C4527AB1EB4001BE212 /* ActorTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ActorTest.cc; sourceTree = ""; }; + 277C5C4527AB1EB4001BE212 /* AsyncTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AsyncTest.cc; sourceTree = ""; }; 277C5C4827AC4E0E001BE212 /* Async.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Async.md; sourceTree = ""; }; 277CB6251D0DED5E00702E56 /* Fleece.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Fleece.xcodeproj; path = fleece/Fleece.xcodeproj; sourceTree = ""; }; 277D19C9194E295B008E91EB /* Error.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Error.hh; sourceTree = ""; }; @@ -1819,7 +1819,7 @@ children = ( 275FF6D11E4947E1005F90DD /* c4BaseTest.cc */, 274D18EC2617DFE40018D39C /* c4DocumentTest_Internal.cc */, - 277C5C4527AB1EB4001BE212 /* ActorTest.cc */, + 277C5C4527AB1EB4001BE212 /* AsyncTest.cc */, 277015081D523E2E008BADD7 /* DataFileTest.cc */, 27E0CA9F1DBEB0BA0089A9C0 /* DocumentKeysTest.cc */, 272B1BEA1FB1513100F56620 /* FTSTest.cc */, @@ -3935,7 +3935,7 @@ 274D17C22615445B0018D39C /* DBAccessTestWrapper.cc in Sources */, 27FA09A01D6FA380005888AA /* DataFileTest.cc in Sources */, 27E0CAA01DBEB0BA0089A9C0 /* DocumentKeysTest.cc in Sources */, - 277C5C4627AB1EB4001BE212 /* ActorTest.cc in Sources */, + 277C5C4627AB1EB4001BE212 /* AsyncTest.cc in Sources */, 27505DDD256335B000123115 /* VersionVectorTest.cc in Sources */, 27456AFD1DC9507D00A38B20 /* SequenceTrackerTest.cc in Sources */, 274EDDFA1DA322D4003AD158 /* QueryParserTest.cc in Sources */, From 84ead1a96f90f13625ac6d75f448f8a6bf7664ce Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 9 Feb 2022 17:22:33 -0800 Subject: [PATCH 04/78] Async cleanup/refactoring --- LiteCore/Support/Actor.cc | 5 - LiteCore/Support/Actor.hh | 11 +- LiteCore/Support/Async.cc | 170 +++++++++++----- LiteCore/Support/Async.hh | 369 +++++++++++++++++++++------------- LiteCore/tests/AsyncTest.cc | 103 +++++----- Networking/BLIP/docs/Async.md | 94 ++++++++- 6 files changed, 492 insertions(+), 260 deletions(-) diff --git a/LiteCore/Support/Actor.cc b/LiteCore/Support/Actor.cc index 776f49d20..acaae1988 100644 --- a/LiteCore/Support/Actor.cc +++ b/LiteCore/Support/Actor.cc @@ -42,9 +42,4 @@ namespace litecore::actor { cond->notify_one(); } - void Actor::wakeAsyncContext(AsyncContext *context) { - _mailbox.enqueue("wakeAsyncContext", ACTOR_BIND_METHOD0(context, &AsyncContext::_next)); - } - - } diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index b0178cb15..2f8bf9114 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -31,7 +31,6 @@ namespace litecore { namespace actor { class Actor; - class AsyncContext; //// Some support code for asynchronize(), from http://stackoverflow.com/questions/42124866 @@ -144,18 +143,22 @@ namespace litecore { namespace actor { Actor* _thisActor() {return this;} - void wakeAsyncContext(AsyncContext *context); - private: friend class ThreadedMailbox; friend class GCDMailbox; - friend class AsyncContext; + friend class AsyncObserver; template friend class ActorBatcher; template friend class ActorCountBatcher; void _waitTillCaughtUp(std::mutex*, std::condition_variable*, bool*); + /** Calls a method on _some other object_ on my mailbox's queue. */ + template + void enqueueOther(const char* methodName, Rcvr* other, void (Rcvr::*fn)(Args...), Args... args) { + _mailbox.enqueue(methodName, ACTOR_BIND_METHOD(other, fn, args)); + } + Mailbox _mailbox; }; diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index bf5f046fa..daecccd20 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -12,101 +12,161 @@ #include "Async.hh" #include "Actor.hh" +#include "Logging.hh" #include "betterassert.hh" namespace litecore::actor { + using namespace std; +#pragma mark - ASYNC FN STATE: - void AsyncState::asyncVoidFn(Actor *actor, std::function body) { - if (actor && actor != Actor::currentActor()) { + + // Called from `BEGIN_ASYNC()`. This is an optimization for a void-returning function that + // avoids allocating an AsyncProvider if the function body never has to block. + void AsyncFnState::asyncVoidFn(Actor *thisActor, function fnBody) { + if (thisActor && thisActor != Actor::currentActor()) { // Need to run this on the Actor's queue, so schedule it: - auto provider = retained(new AsyncProvider(actor, std::move(body))); - provider->_start(); + (new AsyncProvider(thisActor, move(fnBody)))->_start(); } else { - // It's OK to call the body synchronously. As an optimization, pass it a plain - // stack-based AsyncState instead of a heap-allocated AsyncProvider: - AsyncState state; - body(state); + // It's OK to call the body synchronously. As an optimization, call it directly with a + // stack-based AsyncFnState, instead of from a heap-allocated AsyncProvider: + AsyncFnState state(nullptr, thisActor); + fnBody(state); if (state._awaiting) { - // Body didn't finish, is "blocked" in an AWAIT(), so now set up a proper context: - auto provider = retained(new AsyncProvider(actor, - std::move(body), - std::move(state))); - provider->_wait(); + // Body didn't finish (is "blocked" in an `AWAIT()`), so set up a proper provider: + (new AsyncProvider(thisActor, move(fnBody), move(state)))->_wait(); } } } - - bool AsyncState::_await(const AsyncBase &a, int curLine) { - _awaiting = a._context; + + // copy state from `other`, except `_owningProvider` + void AsyncFnState::updateFrom(AsyncFnState &other) { + _owningActor = other._owningActor; + _awaiting = move(other._awaiting); + _currentLine = other._currentLine; + } + + + // called by `AWAIT()` macro + bool AsyncFnState::_await(const AsyncBase &a, int curLine) { + _awaiting = a._provider; _currentLine = curLine; return !a.ready(); } - void AsyncContext::setObserver(AsyncContext *p) { - precondition(!_observer); - _observer = p; - } +#pragma mark - ASYNC OBSERVER: - void AsyncContext::_start() { - _waitingSelf = this; // Retain myself while waiting - if (_actor && _actor != Actor::currentActor()) - _actor->wakeAsyncContext(this); // Schedule the body to run on my Actor's queue - else - _next(); // or run it synchronously + void AsyncObserver::notifyAsyncResultAvailable(AsyncProviderBase *ctx, Actor *actor) { + if (actor && actor != Actor::currentActor()) { + // Schedule a call on my Actor: + actor->enqueueOther("AsyncObserver::asyncResultAvailable", this, + &AsyncObserver::asyncResultAvailable, retained(ctx)); + } else { + // ... or call it synchronously: + asyncResultAvailable(ctx); + } } - // AsyncProvider overrides this, and calls it last. - // Async::AsyncWaiter overrides this and doesn't call it at all. - void AsyncContext::_next() { - if (_awaiting) - _wait(); - else - _gotResult(); +#pragma mark - ASYNC PROVIDER BASE: + + + AsyncProviderBase::AsyncProviderBase(Actor *actorOwningFn) + :_fnState(new AsyncFnState(this, actorOwningFn)) + { } + + + AsyncProviderBase::~AsyncProviderBase() { + if (!_ready) + WarnError("AsyncProvider %p deleted without ever getting a value!", (void*)this); } - void AsyncContext::_wait() { - _waitingActor = Actor::currentActor(); // retain the actor that's waiting - _awaiting->setObserver(this); + void AsyncProviderBase::_start() { + notifyAsyncResultAvailable(nullptr, _fnState->_owningActor); } - void AsyncContext::_waitOn(AsyncContext *context) { - assert(!_awaiting); - _waitingSelf = this; // retain myself while waiting - _awaiting = context; - _wait(); + void AsyncProviderBase::_wait() { + _fnState->_awaiting->setObserver(this); } - void AsyncContext::wakeUp(AsyncContext *async) { - assert(async == _awaiting); - if (_waitingActor) { - fleece::Retained waitingActor = std::move(_waitingActor); - waitingActor->wakeAsyncContext(this); // queues the next() call on its Mailbox - } else { - _next(); + void AsyncProviderBase::setObserver(AsyncObserver *o) { + { + unique_lock _lock(_mutex); + precondition(!_observer.observer); + // Presumably I wasn't ready when the caller decided to call `setObserver` on me; + // but I might have become ready in between then and now, so check for that. + if (!_ready) { + _observer = {o, Actor::currentActor()}; + return; + } } + // if I am ready, call the observer now: + o->notifyAsyncResultAvailable(this, Actor::currentActor()); } - void AsyncContext::_gotResult() { - _ready = true; - if (auto observer = move(_observer)) - observer->wakeUp(this); - _waitingSelf = nullptr; // release myself now that I'm done + void AsyncProviderBase::_gotResult() { + Observer obs = {}; + { + unique_lock _lock(_mutex); + precondition(!_ready); + _ready = true; + swap(obs, _observer); + } + if (obs.observer) + obs.observer->notifyAsyncResultAvailable(this, obs.observerActor); + + // If I am the result of an async fn, it must have finished, so forget its state: + _fnState = nullptr; } - AsyncBase::AsyncBase(AsyncContext *context, bool) +#pragma mark - ASYNC BASE: + + + AsyncBase::AsyncBase(AsyncProviderBase *context, bool) :AsyncBase(context) { - _context->_start(); + _provider->_start(); + } + + + // Simple class that observes an AsyncProvider and can block until it's ready. + class BlockingObserver : public AsyncObserver { + public: + BlockingObserver(AsyncProviderBase *provider) + :_provider(provider) + { } + + void wait() { + unique_lock lock(_mutex); + _provider->setObserver(this); + _cond.wait(lock, [&]{return _provider->ready();}); + } + private: + void asyncResultAvailable(Retained) override { + unique_lock lock(_mutex); + _cond.notify_one(); + } + + mutex _mutex; + condition_variable _cond; + AsyncProviderBase* _provider; + }; + + + void AsyncBase::blockUntilReady() { + if (!ready()) { + precondition(Actor::currentActor() == nullptr); // would deadlock if called by an Actor + BlockingObserver obs(_provider); + obs.wait(); + } } } diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 9ff760bb8..b9e0c6a1f 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -15,6 +15,7 @@ #include "InstanceCounted.hh" #include #include +#include #include #include #include @@ -33,14 +34,14 @@ namespace litecore::actor { /// Put this at the top of an async function/method that returns `Async`, /// but below declarations of any variables that need to be in scope for the whole method. #define BEGIN_ASYNC_RETURNING(T) \ - return Async(_thisActor(), [=](AsyncState &_async_state_) mutable -> std::optional { \ + return Async(_thisActor(), [=](AsyncFnState &_async_state_) mutable -> std::optional { \ switch (_async_state_.currentLine()) { \ default: /// Put this at the top of an async method that returns `void`. /// See `BEGIN_ASYNC_RETURNING` for details. #define BEGIN_ASYNC() \ - return AsyncState::asyncVoidFn(_thisActor(), [=](AsyncState &_async_state_) mutable -> void { \ + return AsyncFnState::asyncVoidFn(_thisActor(), [=](AsyncFnState &_async_state_) mutable -> void { \ switch (_async_state_.currentLine()) { \ default: @@ -63,24 +64,45 @@ namespace litecore::actor { class AsyncBase; - class AsyncContext; + class AsyncProviderBase; template class Async; template class AsyncProvider; // The state data passed to the lambda of an async function. Internal use only. - class AsyncState { + class AsyncFnState { public: int currentLine() const {return _currentLine;} - void reset() {_awaiting = nullptr;} bool _await(const AsyncBase &a, int curLine); template Retained> awaited(); - static void asyncVoidFn(Actor *actor, std::function body); + static void asyncVoidFn(Actor *actor, std::function body); + protected: + friend class AsyncProviderBase; + template friend class AsyncProvider; + + AsyncFnState(AsyncProviderBase *owningProvider, Actor *owningActor) + :_owningProvider(owningProvider) + ,_owningActor(owningActor) + { } + + void updateFrom(AsyncFnState&); + + Retained _owningProvider; // Provider that I belong to + Retained _owningActor; // Actor (if any) that owns the async method + Retained _awaiting; // Provider my fn body is suspended awaiting + int _currentLine {0}; // label/line# to continue body function at + }; + + + // Interface for observing when an Async value becomes available. + class AsyncObserver { + public: + virtual ~AsyncObserver() = default; + void notifyAsyncResultAvailable(AsyncProviderBase*, Actor*); protected: - Retained _awaiting; // What my fn body is suspended awaiting - int _currentLine {0}; // label/line# to continue body function at + virtual void asyncResultAvailable(Retained) =0; }; @@ -88,9 +110,10 @@ namespace litecore::actor { // Maintains the context/state of an async operation and its observer. - // Abstract base class of AsyncProvider and Async::AsyncWaiter. - class AsyncContext : public RefCounted, protected AsyncState, - public fleece::InstanceCountedIn + // Abstract base class of AsyncProvider + class AsyncProviderBase : public RefCounted, + protected AsyncObserver, + public fleece::InstanceCountedIn { public: bool ready() const {return _ready;} @@ -98,89 +121,119 @@ namespace litecore::actor { template const T& result(); template T&& extractResult(); + void setObserver(AsyncObserver*); + protected: - friend class AsyncState; friend class AsyncBase; - friend class Actor; - explicit AsyncContext(Actor *actor = nullptr) :_actor(actor) { } - void _start(); + explicit AsyncProviderBase(bool ready = false) :_ready(ready) { } + explicit AsyncProviderBase(Actor *actorOwningFn); + ~AsyncProviderBase(); + void _start(); void _wait(); - void _waitOn(AsyncContext*); void _gotResult(); - virtual void _next(); // Overridden by AsyncProvider and Async::AsyncWaiter. - - Retained _observer; // Dependent context waiting on me - Actor* _actor; // Owning actor, if any - Retained _waitingActor; // Actor that's waiting, if any - Retained _waitingSelf; // Keeps `this` from being freed - std::atomic _ready {false}; // True when result is ready - + std::mutex mutable _mutex; + std::atomic _ready {false}; // True when result is ready + std::unique_ptr _fnState; // State of associated async fn private: - void setObserver(AsyncContext *p); - void wakeUp(AsyncContext *async); + struct Observer { + AsyncObserver* observer = nullptr; // AsyncObserver waiting on me + Retained observerActor; // Actor the observer was running on + }; + Observer _observer; }; /** An asynchronously-provided result, seen from the producer's side. */ template - class AsyncProvider : public AsyncContext { + class AsyncProvider final : public AsyncProviderBase { public: using ResultType = T; /// Creates a new empty AsyncProvider. static Retained create() {return new AsyncProvider;} + /// Creates a new AsyncProvider that already has a result. static Retained createReady(T&& r) {return new AsyncProvider(std::move(r));} + /// Constructs a new empty AsyncProvider. + AsyncProvider() = default; + /// Creates the client-side view of the result. Async asyncValue() {return Async(this);} - /// Resolves the value by storing the result and waking any waking clients. - void setResult(const T &result) {precondition(!_result); - _result = result; _gotResult();} - /// Resolves the value by move-storing the result and waking any waking clients. - void setResult(T &&result) {precondition(!_result); - _result = std::move(result); - _gotResult();} + /// Resolves the value by storing the result and waking any waiting clients. + void setResult(const T &result) { + { + std::unique_lock _lock(_mutex); + precondition(!_result); + _result = result; + } + _gotResult(); + } + + /// Resolves the value by move-storing the result and waking any waiting clients. + void setResult(T &&result) { + { + std::unique_lock _lock(_mutex); + precondition(!_result); + _result = std::move(result); + } + _gotResult(); + } /// Equivalent to `setResult` but constructs the T value directly inside the provider. template >> void emplaceResult(Args&&... args) { - precondition(!_result); - _result.emplace(args...); + { + std::unique_lock _lock(_mutex); + precondition(!_result); + _result.emplace(args...); + } _gotResult(); } /// Returns the result, which must be available. - const T& result() const & {precondition(_result);return *_result;} - T&& result() && {return extractResult();} + const T& result() const & { + std::unique_lock _lock(_mutex); + precondition(_result); + return *_result; + } + + T&& result() && { + return extractResult(); + } /// Moves the result to the caller. Result must be available. - T&& extractResult() {precondition(_result); - return *std::move(_result);} + T&& extractResult() { + std::unique_lock _lock(_mutex); + precondition(_result); + return *std::move(_result); + } + private: friend class Async; - using Body = std::function(AsyncState&)>; - - AsyncProvider() = default; + using Body = std::function(AsyncFnState&)>; explicit AsyncProvider(T&& result) - :_result(std::move(result)) - {_ready = true;} + :AsyncProviderBase(true) + ,_result(std::move(result)) + { } AsyncProvider(Actor *actor, Body &&body) - :AsyncContext(actor) + :AsyncProviderBase(actor) ,_body(std::move(body)) { } - void _next() override { - _result = _body(*this); - assert(_result || _awaiting); - AsyncContext::_next(); + void asyncResultAvailable(Retained async) override { + assert(async == _fnState->_awaiting); + if (std::optional r = _body(*_fnState)) + setResult(*std::move(r)); + else + _wait(); } Body _body; // The async function body @@ -188,41 +241,6 @@ namespace litecore::actor { }; - - // Specialization of AsyncProvider for use in functions with no return value (void). - template <> - class AsyncProvider : public AsyncContext { - public: - static Retained create() {return new AsyncProvider;} - - private: - friend class Async; - friend class AsyncState; - - using Body = std::function; - - AsyncProvider() :AsyncContext(nullptr) { } - - AsyncProvider(Actor *actor, Body &&body) - :AsyncContext(actor) - ,_body(std::move(body)) - { } - - AsyncProvider(Actor *actor, Body &&body, AsyncState &&state) - :AsyncProvider(actor, std::move(body)) - { - ((AsyncState&)*this) = std::move(state); - } - - void _next() override { - _body(*this); - AsyncContext::_next(); - } - - Body _body; // The async function body - }; - - #pragma mark - ASYNC: @@ -245,13 +263,18 @@ namespace litecore::actor { class AsyncBase { public: /// Returns true once the result is available. - bool ready() const {return _context->ready();} + bool ready() const {return _provider->ready();} + + /// Blocks the current thread (i.e. doesn't return) until the result is available. + /// Please don't use this unless absolutely necessary; use `then()` or `AWAIT()` instead. + void blockUntilReady(); + protected: - friend class AsyncState; - explicit AsyncBase(Retained &&context) :_context(std::move(context)) { } - explicit AsyncBase(AsyncContext *context, bool); // calls context->_start() + friend class AsyncFnState; + explicit AsyncBase(Retained &&context) :_provider(std::move(context)) { } + explicit AsyncBase(AsyncProviderBase *context, bool); // calls context->_start() - Retained _context; // The AsyncProvider that owns my value + Retained _provider; // The provider that owns my value }; @@ -259,26 +282,29 @@ namespace litecore::actor { template class Async : public AsyncBase { public: - using ResultType = T; - using AwaitReturnType = Async; - /// Returns a new AsyncProvider. static Retained> makeProvider() {return AsyncProvider::create();} - Async(T&& t) :AsyncBase(AsyncProvider::createReady(std::move(t))) { } + /// Creates an Async value from its provider. Async(AsyncProvider *provider) :AsyncBase(provider) { } - Async(const Retained> &provider) :AsyncBase(provider) { } + Async(Retained> &&provider) :AsyncBase(std::move(provider)) { } + + /// Creates an already-resolved Async with a value. + explicit Async(T&& t) + :AsyncBase(AsyncProvider::createReady(std::move(t))) + { } + // (used by `BEGIN_ASYNC_RETURNING(T)`. Don't call directly.) Async(Actor *actor, typename AsyncProvider::Body bodyFn) :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) { } /// Returns the result. (Will abort if the result is not yet available.) - const T& result() const & {return _context->result();} - T&& result() const && {return _context->result();} + const T& result() const & {return _provider->result();} + T&& result() const && {return _provider->result();} - /// Returns the result. (Will abort if the result is not yet available.) - T&& extractResult() const {return _context->extractResult();} + /// Move-returns the result. (Will abort if the result is not yet available.) + T&& extractResult() const {return _provider->extractResult();} /// Invokes the callback when the result becomes ready (immediately if it's already ready.) /// The callback should take a single parameter of type `T`, `T&` or `T&&`. @@ -300,21 +326,35 @@ namespace litecore::actor { return _then(callback); } + /// Blocks the current thread until the result is available, then returns it. + /// Please don't use this unless absolutely necessary; use `then()` or `AWAIT()` instead. + const T& blockingResult() { + blockUntilReady(); + return result(); + } + + using ResultType = T; + using AwaitReturnType = Async; + private: class AsyncWaiter; // defined below + AsyncProvider* provider() { + return (AsyncProvider*)_provider.get(); + } + // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. template typename Async::AwaitReturnType _then(std::function callback) { - auto provider = Async::provider(); + auto uProvider = Async::makeProvider(); if (ready()) { - provider->setResult(callback(extractResult())); + uProvider->setResult(callback(extractResult())); } else { - (void) new AsyncWaiter(_context, [provider,callback](T&& result) { - provider->setResult(callback(std::move(result))); + AsyncWaiter::start(this->provider(), [uProvider,callback](T&& result) { + uProvider->setResult(callback(std::move(result))); }); } - return provider->asyncValue(); + return uProvider->asyncValue(); } // Implements `then` where the lambda returns void. (Specialization of above method.) @@ -323,7 +363,7 @@ namespace litecore::actor { if (ready()) callback(extractResult()); else - (void) new AsyncWaiter(_context, std::move(callback)); + AsyncWaiter::start(provider(), std::move(callback)); } // Implements `then` where the lambda returns `Async`. @@ -334,39 +374,22 @@ namespace litecore::actor { return callback(extractResult()); } else { // Otherwise wait for my result... - auto provider = Async::makeProvider(); - (void) new AsyncWaiter(_context, [provider,callback](T&& result) { + auto uProvider = Async::makeProvider(); + AsyncWaiter::start(provider(), [uProvider,callback=std::move(callback)](T&& result) { // Invoke the callback, then wait to resolve the Async it returns: Async u = callback(std::move(result)); - u.then([=](U &&uresult) { + u.then([uProvider](U &&uresult) { // Then finally resolve the async I returned: - provider->setResult(std::move(uresult)); + uProvider->setResult(std::move(uresult)); }); }); - return provider->asyncValue(); + return uProvider->asyncValue(); } } }; - // Specialization of Async<> for `void` type; not used directly. - template <> - class Async : public AsyncBase { - public: - using AwaitReturnType = void; - - static Retained> makeProvider() {return AsyncProvider::create();} - - Async(AsyncProvider *provider) :AsyncBase(provider) { } - Async(const Retained> &provider) :AsyncBase(provider) { } - - Async(Actor *actor, typename AsyncProvider::Body bodyFn) - :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) - { } - }; - - - // Implementation gunk... + //---- Implementation gunk... // Used by `BEGIN_ASYNC` macros. Returns the lexically enclosing actor instance, else NULL. @@ -376,43 +399,103 @@ namespace litecore::actor { template - Retained> AsyncState::awaited() { - // Downcasts `_awaiting` to the specific type of AsyncProvider, and clears it. + Retained> AsyncFnState::awaited() { + // Move-returns `_awaiting`, downcast to the specific type of AsyncProvider<>. + // The dynamic_cast is a safety check: it will throw a `bad_cast` exception on mismatch. (void)dynamic_cast&>(*_awaiting); // runtime type-check return reinterpret_cast>&&>(_awaiting); } template - const T& AsyncContext::result() { + const T& AsyncProviderBase::result() { return dynamic_cast*>(this)->result(); } template - T&& AsyncContext::extractResult() { + T&& AsyncProviderBase::extractResult() { return dynamic_cast*>(this)->extractResult(); } + // Internal class used by `Async::then()`, above template - class Async::AsyncWaiter : public AsyncContext { + class Async::AsyncWaiter : public AsyncObserver { public: using Callback = std::function; - AsyncWaiter(AsyncContext *context, Callback &&callback) - :AsyncContext(nullptr) - ,_callback(std::move(callback)) - { - _waitOn(context); + static void start(AsyncProvider *provider, Callback &&callback) { + (void) new AsyncWaiter(provider, std::move(callback)); } + protected: - void _next() override { - _callback(awaited()->extractResult()); - _callback = nullptr; - _waitingSelf = nullptr; // release myself when done - // Does not call inherited method! + AsyncWaiter(AsyncProvider *provider, Callback &&callback) + :_callback(std::move(callback)) + { + provider->setObserver(this); + } + + void asyncResultAvailable(Retained ctx) override { + auto provider = dynamic_cast*>(ctx.get()); + _callback(provider->extractResult()); + delete this; // delete myself when done! } private: Callback _callback; }; + + // Specialization of AsyncProvider for use in functions with no return value (void). + // Not used directly, but it's used as part of the implementation of void-returning async fns. + template <> + class AsyncProvider : public AsyncProviderBase { + public: +// static Retained create() {return new AsyncProvider;} +// AsyncProvider() = default; + + private: + friend class Async; + friend class AsyncFnState; + + using Body = std::function; + + AsyncProvider(Actor *actor, Body &&body) + :AsyncProviderBase(actor) + ,_body(std::move(body)) + { } + + AsyncProvider(Actor *actor, Body &&body, AsyncFnState &&state) + :AsyncProvider(actor, std::move(body)) + { + _fnState->updateFrom(state); + } + + void asyncResultAvailable(Retained async) override { + assert(async == _fnState->_awaiting); + _body(*_fnState); + if (_fnState->_awaiting) + _wait(); + else + _gotResult(); + } + + Body _body; // The async function body + }; + + + // Specialization of Async<> for `void` type; not used directly. + template <> + class Async : public AsyncBase { + public: + using AwaitReturnType = void; + +// static Retained> makeProvider() {return AsyncProvider::create();} + + Async(AsyncProvider *provider) :AsyncBase(provider) { } + Async(const Retained> &provider) :AsyncBase(provider) { } + + Async(Actor *actor, typename AsyncProvider::Body bodyFn) + :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) + { } + }; + } diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 51894d302..b9b6fb830 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -19,21 +19,6 @@ using namespace std; using namespace litecore::actor; -template -static T waitFor(Async &async) { - C4Log("Waiting..."); - optional result; - async.then([&](T &&c) { - result = c; - }); - while (!result) { - this_thread::sleep_for(10ms); - } - C4Log("...done waiting"); - return *result; -} - - static Async downloader(string url) { auto provider = Async::makeProvider(); std::thread t([=] { @@ -47,15 +32,27 @@ static Async downloader(string url) { class AsyncTest { public: - Retained> aProvider = Async::makeProvider(); - Retained> bProvider = Async::makeProvider(); + Retained> _aProvider; + Retained> _bProvider; + + AsyncProvider* aProvider() { + if (!_aProvider) + _aProvider = Async::makeProvider(); + return _aProvider; + } + + AsyncProvider* bProvider() { + if (!_bProvider) + _bProvider = Async::makeProvider(); + return _bProvider; + } Async provideA() { - return aProvider; + return aProvider(); } Async provideB() { - return bProvider; + return bProvider(); } Async provideSum() { @@ -81,6 +78,22 @@ class AsyncTest { } + Async XXprovideSumPlus() { + string a; + return Async(_thisActor(), [=](AsyncFnState &_async_state_) mutable + -> std::optional { + switch (_async_state_.currentLine()) { + default: + if (_async_state_._await(provideSum(), 78)) return {}; + case 78: + a = _async_state_.awaited>() + ->extractResult(); + return a + "!"; + } + }); + } + + Async provideImmediately() { BEGIN_ASYNC_RETURNING(string) return "immediately"; @@ -120,9 +133,9 @@ class AsyncTest { TEST_CASE_METHOD(AsyncTest, "Async", "[Async]") { Async sum = provideSum(); REQUIRE(!sum.ready()); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); REQUIRE(!sum.ready()); - bProvider->setResult(" there"); + _bProvider->setResult(" there"); REQUIRE(sum.ready()); REQUIRE(sum.result() == "hi there"); } @@ -131,9 +144,9 @@ TEST_CASE_METHOD(AsyncTest, "Async", "[Async]") { TEST_CASE_METHOD(AsyncTest, "Async, other order", "[Async]") { Async sum = provideSum(); REQUIRE(!sum.ready()); - bProvider->setResult(" there"); // this time provideB() finishes first + bProvider()->setResult(" there"); // this time provideB() finishes first REQUIRE(!sum.ready()); - aProvider->setResult("hi"); + aProvider()->setResult("hi"); REQUIRE(sum.ready()); REQUIRE(sum.result() == "hi there"); } @@ -157,10 +170,10 @@ TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { }); REQUIRE(!sum.ready()); REQUIRE(result == ""); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); REQUIRE(!sum.ready()); REQUIRE(result == ""); - bProvider->setResult(" there"); + _bProvider->setResult(" there"); REQUIRE(sum.ready()); REQUIRE(result == "hi there"); } @@ -169,9 +182,9 @@ TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { TEST_CASE_METHOD(AsyncTest, "Async, 2 levels", "[Async]") { Async sum = provideSumPlus(); REQUIRE(!sum.ready()); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); REQUIRE(!sum.ready()); - bProvider->setResult(" there"); + _bProvider->setResult(" there"); REQUIRE(sum.ready()); REQUIRE(sum.result() == "hi there!"); } @@ -181,11 +194,11 @@ TEST_CASE_METHOD(AsyncTest, "Async, loop", "[Async]") { Async sum = provideLoop(); for (int i = 1; i <= 10; i++) { REQUIRE(!sum.ready()); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); + _aProvider = nullptr; REQUIRE(!sum.ready()); - aProvider = Async::makeProvider(); - bProvider->setResult(" there"); - bProvider = Async::makeProvider(); + _bProvider->setResult(" there"); + _bProvider = nullptr; } REQUIRE(sum.ready()); REQUIRE(sum.result() == 360); @@ -202,9 +215,9 @@ TEST_CASE_METHOD(AsyncTest, "Async, immediately", "[Async]") { TEST_CASE_METHOD(AsyncTest, "Async void fn", "[Async]") { provideNothing(); REQUIRE(provideNothingResult == ""); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); REQUIRE(provideNothingResult == ""); - bProvider->setResult(" there"); + _bProvider->setResult(" there"); REQUIRE(provideNothingResult == "hi there"); } @@ -217,9 +230,9 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning void", "[Async]") { }); Log("--Providing aProvider"); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); Log("--Providing bProvider"); - bProvider->setResult(" there"); + _bProvider->setResult(" there"); CHECK(result == "hi there"); } @@ -231,10 +244,10 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning T", "[Async]") { }); Log("--Providing aProvider"); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); Log("--Providing bProvider"); - bProvider->setResult(" there"); - CHECK(waitFor(size) == 8); + _bProvider->setResult(" there"); + CHECK(size.blockingResult() == 8); } @@ -245,10 +258,10 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning async T", "[Async]") { }); Log("--Providing aProvider"); - aProvider->setResult("hi"); + _aProvider->setResult("hi"); Log("--Providing bProvider"); - bProvider->setResult(" there"); - CHECK(waitFor(dl) == "Contents of hi there"); + _bProvider->setResult(" there"); + CHECK(dl.blockingResult() == "Contents of hi there"); } @@ -287,7 +300,7 @@ class AsyncTestActor : public Actor { BEGIN_ASYNC() downloader(url).then([=](string &&s) { // When `then` is used inside an Actor method, the lambda must be called on its queue: - CHECK(currentActor() == this); + assert(currentActor() == this); testThenResult = move(s); testThenReady = true; }); @@ -301,7 +314,7 @@ class AsyncTestActor : public Actor { TEST_CASE("Async on thread", "[Async]") { auto asyncContents = downloader("couchbase.com"); - string contents = waitFor(asyncContents); + string contents = asyncContents.blockingResult(); CHECK(contents == "Contents of couchbase.com"); } @@ -309,7 +322,7 @@ TEST_CASE("Async on thread", "[Async]") { TEST_CASE("Async Actor", "[Async]") { auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org"); - string contents = waitFor(asyncContents); + string contents = asyncContents.blockingResult(); CHECK(contents == "Contents of couchbase.org"); } @@ -317,7 +330,7 @@ TEST_CASE("Async Actor", "[Async]") { TEST_CASE("Async Actor Twice", "[Async]") { auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org", "couchbase.biz"); - string contents = waitFor(asyncContents); + string contents = asyncContents.blockingResult(); CHECK(contents == "Contents of couchbase.org and Contents of couchbase.biz"); } diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md index ad41c07e8..fbf5414bc 100644 --- a/Networking/BLIP/docs/Async.md +++ b/Networking/BLIP/docs/Async.md @@ -1,8 +1,14 @@ # The Async API -(Last updated Feb 4 2022 by Jens) +(Last updated Feb 7 2022 by Jens) -## Asynchronous Values (Futures) +**Async** is a major extension of LiteCore’s concurrency support, which should help us write clearer and safer multithreaded code in the future. It extends the functionality of Actors: so far, Actor methods have had to return `void` since they’re called asynchronously. Getting a value back from an Actor meant explicitly passing a callback function. + +The Async feature allows Actor methods to return values; it’s just that those values are themselves asynchronous, and can’t be accessed by the caller until the Actor method finishes and returns a value. This may seem constraining, but it’s actually very useful. And any class can take advantage of Async values, not just Actors. + +> If this sounds familiar: yes, this is an implementation of async/await as found in C#, JavaScript, Rust, etc. (C++ itself is getting this feature too, but not until C++20, and even then it’s only half-baked.) + +## 1. Asynchronous Values (Futures) `Async` represents a value of type `T` that may not be available yet. This concept is also referred to as a ["future"](https://en.wikipedia.org/wiki/Futures_and_promises). You can keep it around like a normal value type, but you can’t get the underlying value until it becomes available. @@ -46,7 +52,7 @@ cout << "Server says: " << i << "!\n"; Only … when is “later”, exactly? How do you know? -## Getting The Result With `then` +### Getting The Result With `then` You can’t call `Async::result()` before the result is available, or Bad Stuff happens, like a fatal exception. We don’t want anything to block; that’s the point of async! @@ -89,7 +95,7 @@ Async message = getIntFromServer().then([](int i) { }); ``` -## Asynchronous Functions +## 2. Asynchronous Functions An asynchronous function is a function that can resolve `Async` values in a way that *appears* synchronous, but without actually blocking. It lets you write code that looks more linear, without a bunch of “…then…”s in it. The bad news is that it’s reliant on some weird macros that uglify your code a bit. @@ -126,9 +132,22 @@ AWAIT(n, someOtherAsyncFunction()); This means “call `someOtherAsyncFunction()` [which returns an `Async`], *suspend this function* until that `Async`’s result becomes available, then assign the result to `n`.” -The weird part is that “suspend” doesn’t actually mean “block the current thread.” Instead it temporarily returns from the function (giving the caller an Async value as a placeholder), but when the value becomes available it resumes the function where it left off. This reduces the need for multiple threads, and in an Actor it lets you handle other messages while the current one is suspended. +The weird part is that “suspend” doesn’t actually mean “block the current thread.” Instead it *temporarily returns* from the function (giving the caller an Async value as a placeholder), but when the value becomes available it *resumes* the function where it left off. This reduces the need for multiple threads, and in an Actor it lets you handle other messages while the current one is suspended. + +> TMI: `AWAIT` is a macro that hides some very weird control flow. It first evaluates the second parameter to get its `Async` value. If that value isn't yet available, `AWAIT` _causes the enclosing function to return_. (Obviously it returns an unavailable `Async` value.) It also registers as an observer of the async value, so when its result does become available, the enclosing function _resumes_ right at the line where it left off (🤯), assigns the result to the variable, and continues. If you want even more gory details, see the appendix. + +### Parameters Of ASYNC functions + +You need to stay aware of the fact that an async function can be suspended, either in the `BEGIN_ASYNC()` or in an `AWAIT()`. One immediate consequence is that **the function shouldn’t take parameters that are pointers or references**, or things that behave like them (notably `slice`), because their values are likely to be garbage after the function’s been suspended and resumed. The same goes for any local variables in the function that are used across suspension points. -> TMI: `AWAIT` is a macro that hides some very weird control flow. It first evaluates the second parameter to get its `Async` value. If that value isn't yet available, `AWAIT` _causes the enclosing function to return_. (Obviously it returns an unavailable `Async` value.) It also registers as an observer of the async value, so when its result does become available, the enclosing function _resumes_ right at the line where it left off (🤯), assigns the result to the variable, and continues. +| Unsafe | Safe | +| -------------------------- | ------------------------------------ | +| `MyRefCountedClass*` | `Retained` | +| `MyStruct*` or `MyStruct&` | `MyStruct` or `unique_ptr` | +| `slice` | `alloc_slice` | +| `string_view` | `string` | + +(I feel compelled to admit that it is actually safe to have such parameters … as long as you use them *only before* the `BEGIN_ASYNC` call, not after. This seems like a rare case, though; it might happen in a method that can usually return immediately but only sometimes needs to go the async route.) ### Variable Scope Inside ASYNC functions @@ -163,7 +182,7 @@ BEGIN_ASYNC() AWAIT(int n, someOtherAsyncFunction()); // OK! ``` -## Threading +## 3. Threading and Actors By default, an async function starts immediately (as you’d expect) and runs until it either returns a value, or blocks in an AWAIT call on an Async value that isn’t ready. In the latter case, it returns to the caller, but its Async result isn’t ready yet. @@ -178,7 +197,7 @@ Fortunately, `BEGIN_ASYNC` and `AWAIT` are aware of Actors, and have special beh * `BEGIN_ASYNC` checks whether the current thread is already running as that Actor. If not, it doesn’t start yet, but schedules the function on the Actor’s queue. * When `AWAIT` resumes the function, it schedules it on the Actor's queue. -### Easier Actors Without Async +### Easier Actors With Async One benefit of this is that Actor methods using `BEGIN/END_ASYNC` don’t need the usual idiom where the public method enqueues a call to a matching private method: @@ -214,3 +233,62 @@ void Twiddler::twiddle(int n) { } ``` +## 4. Appendix: How Async Functions Work + +The weird part of async functions is the way the `AWAIT()` macro can *suspend* the function in the middle, and then *resume* it later when the async value is ready. This seems like black magic until you find out hot it works; it’s actually a clever technique for implementing coroutines in C invented by [Simon Tatham](https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html), which is based on an earlier hack called [Duff’s Device](https://en.wikipedia.org/wiki/Duff%27s_device), a weird (ab)use of the `switch` statement. + +Let’s look at a simple async function that calls another async function: + +```c++ +Async provideSum(); + +Async provideSumPlus() { + string a; + BEGIN_ASYNC_RETURNING(string) // (a) + Log("Entering provideSumPlus"); + AWAIT(a, provideSum()); // (b) + return a + "!"; + END_ASYNC() // (c) +} +``` + +If we run this through the preprocessor, we get: + +```c++ +Async provideSumPlus() { + string a; + return Async(_thisActor(), [=](AsyncState &_async_state_) mutable // (a) + -> std::optional { // + switch (_async_state_.currentLine()) { // + default: // + Log("Entering provideSumPlus"); + if (_async_state_._await(provideSum(), 78)) return {}; // (b) + case 78: // + a = _async_state_.awaited>()// + ->extractResult(); // + return a + "!"; + } // (c) + }); // +} +``` + +`BEGIN_ASYNC_RETURNING(string)` turns into `return Async(...)`, where the constructor parameters are `_thisActor()`, and a lambda that contains the rest of the function wrapped in a `switch` statement. + +`_thisActor()` is simple: within the scope of an Actor method, it’s an inline method that returns `this`. Otherwise, it’s a global function that returns `nullptr`. So this call evaluates to the Actor implementing this function/method, if any. + +The `Async` constructor either calls the lambda immediately or (if it’s in an Actor method) schedules it to be called on the Actor’s queue. The function ends up returning this Async instance, whether or not it’s been resolved. + +The really interesting stuff happens in that lambda. It’s passed an `AsyncState` value, which stores some state while the function is suspended; one piece of state is *which line of code it suspended at*, initially 0. + +1. The first thing the lambda does is enter a `switch` on the state’s `currentLine()` value. This will of course jump to the corresponding label. The first time the lambda is called, `currentLine()` is 0, so… +2. The PC jumps to the `default:` label, which is right at the start of the code we wrote. +3. The function writes to the log, then hits the magic AWAIT call, which has turned into: +4. `provideSum()` is called. This returns an `Async` value. +5. We call `_async_state_.await()` and pass it this Async value and the integer 78, which happens to be the current source line, as produced by the `__LINE__` macro. This function stores the value and line number into the AsyncState. The value returned is false if the Async has a result already, true if it doesn’t. Let’s assume the latter, since it’s more interesting. +6. Back in `provideSumPlus()`, the `false` result causes the lambda to return early. The lambda’s return type is `optional`, so the return value is `nullopt`. +7. The caller of the lambda (never mind who) sees that it’s unfinished since it returned `nullopt`. So it gets put into the suspended state. A listener is added to the Async value it’s waiting on, so that when that Async’s result arrives, the lambda will be called again. +8. After the result arrives, the lambda restarts. This time, `_async_state_.currentLine()` returns 78 (stored into it in step 5), so the `switch` statement jumps to `case 78`. +9. The next line is messy due to some template gunk. `_async_state_.awaited<...>()` returns the AsyncProvider of the Async that we were waiting for, and `extractResult()` gets the result from that provider. That value is assigned to our variable `a`. +10. Now we’re back in regular code. We simply append `“!”` to `a` and return that. +11. This time the caller of the lambda gets a real value and knows the function is done. It stuffs that value into the result of the Async it created way at the start (the one the top-level function returned) and notifies listeners that it’s available. +12. At this point the Async value returned by provideSumPlus() has a result, and whatever’s awaiting that result can run. From 7e27c8dde467caf3a6367484714b6aabddf987c6 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 10 Feb 2022 15:09:21 -0800 Subject: [PATCH 05/78] (WIP) --- LiteCore/Support/Async.cc | 28 +++++++--- LiteCore/Support/Async.hh | 106 ++++++++++++++++++++++++++------------ 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index daecccd20..49ca8c84f 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -111,14 +111,14 @@ namespace litecore::actor { } - void AsyncProviderBase::_gotResult() { + void AsyncProviderBase::_gotResult(std::unique_lock& lock) { + precondition(!_ready); + _ready = true; Observer obs = {}; - { - unique_lock _lock(_mutex); - precondition(!_ready); - _ready = true; - swap(obs, _observer); - } + swap(obs, _observer); + + lock.unlock(); + if (obs.observer) obs.observer->notifyAsyncResultAvailable(this, obs.observerActor); @@ -127,6 +127,20 @@ namespace litecore::actor { } + void AsyncProviderBase::setException(std::exception_ptr x) { + unique_lock lock(_mutex); + precondition(!_exception); + _exception = x; + _gotResult(lock); + } + + + void AsyncProviderBase::rethrowException() const { + if (_exception) + rethrow_exception(_exception); + } + + #pragma mark - ASYNC BASE: diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index b9e0c6a1f..33944582a 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -121,6 +121,15 @@ namespace litecore::actor { template const T& result(); template T&& extractResult(); + /// Returns the exception result, else nullptr. + std::exception_ptr exception() const {return _exception;} + + /// Sets an exception as the result. This will wake up observers. + void setException(std::exception_ptr); + + /// If the result is an exception, re-throws it. Else does nothing. + void rethrowException() const; + void setObserver(AsyncObserver*); protected: @@ -131,10 +140,11 @@ namespace litecore::actor { ~AsyncProviderBase(); void _start(); void _wait(); - void _gotResult(); + void _gotResult(std::unique_lock&); std::mutex mutable _mutex; std::atomic _ready {false}; // True when result is ready + std::exception_ptr _exception {nullptr}; // Exception if provider failed std::unique_ptr _fnState; // State of associated async fn private: struct Observer { @@ -165,39 +175,48 @@ namespace litecore::actor { /// Resolves the value by storing the result and waking any waiting clients. void setResult(const T &result) { - { - std::unique_lock _lock(_mutex); - precondition(!_result); - _result = result; - } - _gotResult(); + std::unique_lock lock(_mutex); + precondition(!_result); + _result = result; + _gotResult(lock); } /// Resolves the value by move-storing the result and waking any waiting clients. void setResult(T &&result) { - { - std::unique_lock _lock(_mutex); - precondition(!_result); - _result = std::move(result); - } - _gotResult(); + std::unique_lock lock(_mutex); + precondition(!_result); + _result = std::move(result); + _gotResult(lock); } /// Equivalent to `setResult` but constructs the T value directly inside the provider. template >> void emplaceResult(Args&&... args) { - { - std::unique_lock _lock(_mutex); - precondition(!_result); - _result.emplace(args...); + std::unique_lock lock(_mutex); + precondition(!_result); + _result.emplace(args...); + _gotResult(lock); + } + + template + void setResultFromCallback(LAMBDA callback) { + bool duringCallback = true; + try { + auto result = callback(); + duringCallback = false; + setResult(std::move(result)); + } catch (...) { + if (!duringCallback) + throw; + setException(std::current_exception()); } - _gotResult(); } /// Returns the result, which must be available. const T& result() const & { - std::unique_lock _lock(_mutex); + std::unique_lock _lock(_mutex); + rethrowException(); precondition(_result); return *_result; } @@ -208,7 +227,8 @@ namespace litecore::actor { /// Moves the result to the caller. Result must be available. T&& extractResult() { - std::unique_lock _lock(_mutex); + std::unique_lock _lock(_mutex); + rethrowException(); precondition(_result); return *std::move(_result); } @@ -225,18 +245,25 @@ namespace litecore::actor { AsyncProvider(Actor *actor, Body &&body) :AsyncProviderBase(actor) - ,_body(std::move(body)) + ,_fnBody(std::move(body)) { } void asyncResultAvailable(Retained async) override { assert(async == _fnState->_awaiting); - if (std::optional r = _body(*_fnState)) + std::optional r; + try { + r = _fnBody(*_fnState); + } catch(const std::exception &x) { + setException(std::current_exception()); + return; + } + if (r) setResult(*std::move(r)); else _wait(); } - Body _body; // The async function body + Body _fnBody; // The async function body, if any std::optional _result; // My result }; @@ -269,6 +296,9 @@ namespace litecore::actor { /// Please don't use this unless absolutely necessary; use `then()` or `AWAIT()` instead. void blockUntilReady(); + /// Returns the exception result, else nullptr. + std::exception_ptr exception() const {return _provider->exception();} + protected: friend class AsyncFnState; explicit AsyncBase(Retained &&context) :_provider(std::move(context)) { } @@ -337,7 +367,7 @@ namespace litecore::actor { using AwaitReturnType = Async; private: - class AsyncWaiter; // defined below + class Waiter; // defined below AsyncProvider* provider() { return (AsyncProvider*)_provider.get(); @@ -348,10 +378,15 @@ namespace litecore::actor { typename Async::AwaitReturnType _then(std::function callback) { auto uProvider = Async::makeProvider(); if (ready()) { - uProvider->setResult(callback(extractResult())); + // Result is available now, so call the callback: + if (auto x = exception()) + uProvider->setException(x); + else + uProvider->setResultFromCallback([&]{return callback(extractResult());}); } else { - AsyncWaiter::start(this->provider(), [uProvider,callback](T&& result) { - uProvider->setResult(callback(std::move(result))); + // Create an AsyncWaiter to wait on the provider: + Waiter::start(this->provider(), [uProvider,callback](T&& result) { + uProvider->setResultFromCallback([&]{return callback(std::move(result));}); }); } return uProvider->asyncValue(); @@ -363,7 +398,7 @@ namespace litecore::actor { if (ready()) callback(extractResult()); else - AsyncWaiter::start(provider(), std::move(callback)); + Waiter::start(provider(), std::move(callback)); } // Implements `then` where the lambda returns `Async`. @@ -375,7 +410,7 @@ namespace litecore::actor { } else { // Otherwise wait for my result... auto uProvider = Async::makeProvider(); - AsyncWaiter::start(provider(), [uProvider,callback=std::move(callback)](T&& result) { + Waiter::start(provider(), [uProvider,callback=std::move(callback)](T&& result) { // Invoke the callback, then wait to resolve the Async it returns: Async u = callback(std::move(result)); u.then([uProvider](U &&uresult) { @@ -419,16 +454,16 @@ namespace litecore::actor { // Internal class used by `Async::then()`, above template - class Async::AsyncWaiter : public AsyncObserver { + class Async::Waiter : public AsyncObserver { public: using Callback = std::function; static void start(AsyncProvider *provider, Callback &&callback) { - (void) new AsyncWaiter(provider, std::move(callback)); + (void) new Waiter(provider, std::move(callback)); } protected: - AsyncWaiter(AsyncProvider *provider, Callback &&callback) + Waiter(AsyncProvider *provider, Callback &&callback) :_callback(std::move(callback)) { provider->setObserver(this); @@ -469,13 +504,18 @@ namespace litecore::actor { _fnState->updateFrom(state); } + void setResult() { + std::unique_lock lock(_mutex); + _gotResult(lock); + } + void asyncResultAvailable(Retained async) override { assert(async == _fnState->_awaiting); _body(*_fnState); if (_fnState->_awaiting) _wait(); else - _gotResult(); + setResult(); } Body _body; // The async function body From 1e9e1ecdb50ea0998c73d864c03bcdb6600ed04c Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 17 Feb 2022 17:23:28 -0800 Subject: [PATCH 06/78] More Async work/fixes --- LiteCore/Support/Actor.hh | 41 ++++++++++++++++- LiteCore/Support/Async.cc | 29 ++++++++---- LiteCore/Support/Async.hh | 86 +++++++++++++++++++++-------------- LiteCore/tests/AsyncTest.cc | 22 ++++----- Networking/BLIP/docs/Async.md | 6 +-- 5 files changed, 126 insertions(+), 58 deletions(-) diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index 2f8bf9114..d98ae324e 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -100,6 +100,14 @@ namespace litecore { namespace actor { ,_mailbox(this, name, parentMailbox) { } + /** Within an Actor method, `thisActor` evaluates to `this`. + (Outside of one, it calls the static function `thisActor` that returns nullptr.) */ + Actor* thisActor() {return this;} + const Actor* thisActor() const {return this;} + + /** Returns true if `this` is the currently running Actor. */ + bool isCurrentActor() const {return currentActor() == this;} + /** Schedules a call to a method. */ template void enqueue(const char* methodName, void (Rcvr::*fn)(Args...), Args... args) { @@ -141,8 +149,6 @@ namespace litecore { namespace actor { } - Actor* _thisActor() {return this;} - private: friend class ThreadedMailbox; friend class GCDMailbox; @@ -163,7 +169,38 @@ namespace litecore { namespace actor { }; +#ifndef _THISACTOR_DEFINED +#define _THISACTOR_DEFINED + static inline Actor* thisActor() {return nullptr;} +#endif + #undef ACTOR_BIND_METHOD #undef ACTOR_BIND_FN + + template class actor_function; + + template + class actor_function { + public: + template + actor_function(Actor *actor, Callable &&callabl, + typename std::enable_if< + !std::is_same::type, + actor_function>::value>::type * = nullptr) + :_fn(std::forward(callabl)) + { } + + Ret operator()(Params ...params) const { + if (_actor == nullptr || _actor == Actor::currentActor()) + return _fn(std::forward(params)...); + else + _actor->enqueueOther("actor_function", + ACTOR_BIND_FN(_fn, std::forward(params)...)); + } + private: + std::function _fn; + Retained _actor; + }; + } } diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 49ca8c84f..10b2cf352 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -40,6 +40,12 @@ namespace litecore::actor { } + AsyncFnState::AsyncFnState(AsyncProviderBase *owningProvider, Actor *owningActor) + :_owningProvider(owningProvider) + ,_owningActor(owningActor) + { } + + // copy state from `other`, except `_owningProvider` void AsyncFnState::updateFrom(AsyncFnState &other) { _owningActor = other._owningActor; @@ -79,6 +85,11 @@ namespace litecore::actor { { } + AsyncProviderBase::AsyncProviderBase(bool ready) + :_ready(ready) + { } + + AsyncProviderBase::~AsyncProviderBase() { if (!_ready) WarnError("AsyncProvider %p deleted without ever getting a value!", (void*)this); @@ -95,32 +106,34 @@ namespace litecore::actor { } - void AsyncProviderBase::setObserver(AsyncObserver *o) { + void AsyncProviderBase::setObserver(AsyncObserver *o, Actor *actor) { { unique_lock _lock(_mutex); - precondition(!_observer.observer); + precondition(!_observer); // Presumably I wasn't ready when the caller decided to call `setObserver` on me; // but I might have become ready in between then and now, so check for that. if (!_ready) { - _observer = {o, Actor::currentActor()}; + _observer = o; + _observerActor = actor ? actor : Actor::currentActor(); return; } } // if I am ready, call the observer now: - o->notifyAsyncResultAvailable(this, Actor::currentActor()); + o->notifyAsyncResultAvailable(this, actor); } void AsyncProviderBase::_gotResult(std::unique_lock& lock) { precondition(!_ready); _ready = true; - Observer obs = {}; - swap(obs, _observer); + auto observer = _observer; + _observer = nullptr; + auto observerActor = std::move(_observerActor); lock.unlock(); - if (obs.observer) - obs.observer->notifyAsyncResultAvailable(this, obs.observerActor); + if (observer) + observer->notifyAsyncResultAvailable(this, observerActor); // If I am the result of an async fn, it must have finished, so forget its state: _fnState = nullptr; diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 33944582a..1392ba667 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -34,16 +34,16 @@ namespace litecore::actor { /// Put this at the top of an async function/method that returns `Async`, /// but below declarations of any variables that need to be in scope for the whole method. #define BEGIN_ASYNC_RETURNING(T) \ - return Async(_thisActor(), [=](AsyncFnState &_async_state_) mutable -> std::optional { \ + return litecore::actor::Async(thisActor(), [=](litecore::actor::AsyncFnState &_async_state_) mutable -> std::optional { \ switch (_async_state_.currentLine()) { \ - default: + default: { /// Put this at the top of an async method that returns `void`. /// See `BEGIN_ASYNC_RETURNING` for details. #define BEGIN_ASYNC() \ - return AsyncFnState::asyncVoidFn(_thisActor(), [=](AsyncFnState &_async_state_) mutable -> void { \ + return litecore::actor::AsyncFnState::asyncVoidFn(thisActor(), [=](litecore::actor::AsyncFnState &_async_state_) mutable -> void { \ switch (_async_state_.currentLine()) { \ - default: + default: { /// Use this in an async method to resolve an `Async<>` value, blocking until it's available. /// `VAR` is the name of the variable to which to assign the result. @@ -52,13 +52,21 @@ namespace litecore::actor { /// If the `Async` value's result is already available, it is immediately assigned to `VAR` and /// execution continues. /// Otherwise, this method is suspended until the result becomes available. -#define AWAIT(VAR, EXPR) \ - if (_async_state_._await(EXPR, __LINE__)) return {}; \ - case __LINE__: \ +#define AWAIT(T, VAR, EXPR) \ + if (_async_state_.await(EXPR, __LINE__)) return {}; \ + }\ + case __LINE__: {\ + T VAR = _async_state_.awaited()->extractResult(); + +#define XAWAIT(VAR, EXPR) \ + if (_async_state_.await(EXPR, __LINE__)) return {}; \ + }\ + case __LINE__: { \ VAR = _async_state_.awaited>()->extractResult(); /// Put this at the very end of an async function/method. #define END_ASYNC() \ + } \ } \ }); @@ -73,7 +81,7 @@ namespace litecore::actor { class AsyncFnState { public: int currentLine() const {return _currentLine;} - bool _await(const AsyncBase &a, int curLine); + template bool await(const Async &a, int curLine); template Retained> awaited(); static void asyncVoidFn(Actor *actor, std::function body); @@ -82,11 +90,9 @@ namespace litecore::actor { friend class AsyncProviderBase; template friend class AsyncProvider; - AsyncFnState(AsyncProviderBase *owningProvider, Actor *owningActor) - :_owningProvider(owningProvider) - ,_owningActor(owningActor) - { } + AsyncFnState(AsyncProviderBase *owningProvider, Actor *owningActor); + bool _await(const AsyncBase &a, int curLine); void updateFrom(AsyncFnState&); Retained _owningProvider; // Provider that I belong to @@ -130,12 +136,12 @@ namespace litecore::actor { /// If the result is an exception, re-throws it. Else does nothing. void rethrowException() const; - void setObserver(AsyncObserver*); + void setObserver(AsyncObserver*, Actor* =nullptr); protected: friend class AsyncBase; - explicit AsyncProviderBase(bool ready = false) :_ready(ready) { } + explicit AsyncProviderBase(bool ready = false); explicit AsyncProviderBase(Actor *actorOwningFn); ~AsyncProviderBase(); void _start(); @@ -147,11 +153,8 @@ namespace litecore::actor { std::exception_ptr _exception {nullptr}; // Exception if provider failed std::unique_ptr _fnState; // State of associated async fn private: - struct Observer { - AsyncObserver* observer = nullptr; // AsyncObserver waiting on me - Retained observerActor; // Actor the observer was running on - }; - Observer _observer; + AsyncObserver* _observer = nullptr; // AsyncObserver waiting on me + Retained _observerActor; // Actor the observer was running on }; @@ -350,10 +353,16 @@ namespace litecore::actor { /// - `a.then([](T) -> void { ... });` /// - `Async x = a.then([](T) -> X { ... });` /// - `Async x = a.then([](T) -> Async { ... });` + template + auto then(Actor *onActor, LAMBDA callback) { + using U = unwrap_async>; // return type w/o Async<> + return _then(onActor, callback); + } + template auto then(LAMBDA callback) { - using U = unwrap_async>; // return type w/o Async<> - return _then(callback); + using U = unwrap_async>; // return type w/o Async<> + return _then(nullptr, callback); } /// Blocks the current thread until the result is available, then returns it. @@ -375,7 +384,8 @@ namespace litecore::actor { // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. template - typename Async::AwaitReturnType _then(std::function callback) { + typename Async::AwaitReturnType + _then(Actor *onActor, std::function callback) { auto uProvider = Async::makeProvider(); if (ready()) { // Result is available now, so call the callback: @@ -385,7 +395,7 @@ namespace litecore::actor { uProvider->setResultFromCallback([&]{return callback(extractResult());}); } else { // Create an AsyncWaiter to wait on the provider: - Waiter::start(this->provider(), [uProvider,callback](T&& result) { + Waiter::start(this->provider(), onActor, [uProvider,callback](T&& result) { uProvider->setResultFromCallback([&]{return callback(std::move(result));}); }); } @@ -394,23 +404,23 @@ namespace litecore::actor { // Implements `then` where the lambda returns void. (Specialization of above method.) template<> - void _then(std::function callback) { + void _then(Actor *onActor, std::function callback) { if (ready()) callback(extractResult()); else - Waiter::start(provider(), std::move(callback)); + Waiter::start(provider(), onActor, std::move(callback)); } // Implements `then` where the lambda returns `Async`. template - Async _then(std::function(T&&)> callback) { + Async _then(Actor *onActor, std::function(T&&)> callback) { if (ready()) { // If I'm ready, just call the callback and pass on the Async it returns: return callback(extractResult()); } else { // Otherwise wait for my result... auto uProvider = Async::makeProvider(); - Waiter::start(provider(), [uProvider,callback=std::move(callback)](T&& result) { + Waiter::start(provider(), onActor, [uProvider,callback=std::move(callback)](T&& result) { // Invoke the callback, then wait to resolve the Async it returns: Async u = callback(std::move(result)); u.then([uProvider](U &&uresult) { @@ -427,10 +437,18 @@ namespace litecore::actor { //---- Implementation gunk... +#ifndef _THISACTOR_DEFINED +#define _THISACTOR_DEFINED // Used by `BEGIN_ASYNC` macros. Returns the lexically enclosing actor instance, else NULL. - // (How? Outside of an Actor method, `_thisActor()` refers to the function below. - // In an Actor method, it refers to `Actor::_thisActor()`, which returns `this`.) - static inline Actor* _thisActor() {return nullptr;} + // (How? Outside of an Actor method, `thisActor()` refers to the function below. + // In an Actor method, it refers to `Actor::thisActor()`, which returns `this`.) + static inline Actor* thisActor() {return nullptr;} +#endif + + template + bool AsyncFnState::await(const Async &a, int curLine) { + return _await(a, curLine); + } template @@ -458,15 +476,15 @@ namespace litecore::actor { public: using Callback = std::function; - static void start(AsyncProvider *provider, Callback &&callback) { - (void) new Waiter(provider, std::move(callback)); + static void start(AsyncProvider *provider, Actor *onActor, Callback &&callback) { + (void) new Waiter(provider, onActor, std::move(callback)); } protected: - Waiter(AsyncProvider *provider, Callback &&callback) + Waiter(AsyncProvider *provider, Actor *onActor, Callback &&callback) :_callback(std::move(callback)) { - provider->setObserver(this); + provider->setObserver(this, onActor); } void asyncResultAvailable(Retained ctx) override { diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index b9b6fb830..1d94b1450 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -60,9 +60,9 @@ class AsyncTest { string a, b; BEGIN_ASYNC_RETURNING(string) Log("provideSum: awaiting A"); - AWAIT(a, provideA()); + XAWAIT(a, provideA()); Log("provideSum: awaiting B"); - AWAIT(b, provideB()); + XAWAIT(b, provideB()); Log("provideSum: returning"); return a + b; END_ASYNC() @@ -72,7 +72,7 @@ class AsyncTest { Async provideSumPlus() { string a; BEGIN_ASYNC_RETURNING(string) - AWAIT(a, provideSum()); + XAWAIT(a, provideSum()); return a + "!"; END_ASYNC() } @@ -80,11 +80,11 @@ class AsyncTest { Async XXprovideSumPlus() { string a; - return Async(_thisActor(), [=](AsyncFnState &_async_state_) mutable + return Async(thisActor(), [=](AsyncFnState &_async_state_) mutable -> std::optional { switch (_async_state_.currentLine()) { default: - if (_async_state_._await(provideSum(), 78)) return {}; + if (_async_state_.await(provideSum(), 78)) return {}; case 78: a = _async_state_.awaited>() ->extractResult(); @@ -107,7 +107,7 @@ class AsyncTest { int i = 0; BEGIN_ASYNC_RETURNING(int) for (i = 0; i < 10; i++) { - AWAIT(n, provideSum()); + XAWAIT(n, provideSum()); //fprintf(stderr, "n=%f, i=%d, sum=%f\n", n, i, sum); sum += n.size() * i; } @@ -121,8 +121,8 @@ class AsyncTest { void provideNothing() { string a, b; BEGIN_ASYNC() - AWAIT(a, provideA()); - AWAIT(b, provideB()); + XAWAIT(a, provideA()); + XAWAIT(b, provideB()); provideNothingResult = a + b; END_ASYNC() } @@ -276,7 +276,7 @@ class AsyncTestActor : public Actor { string contents; BEGIN_ASYNC_RETURNING(string) CHECK(currentActor() == this); - AWAIT(contents, downloader(url)); + XAWAIT(contents, downloader(url)); CHECK(currentActor() == this); return contents; END_ASYNC() @@ -289,9 +289,9 @@ class AsyncTestActor : public Actor { CHECK(currentActor() == this); dl1 = download(url1); dl2 = download(url2); - AWAIT(contents, *dl1); + XAWAIT(contents, *dl1); CHECK(currentActor() == this); - AWAIT(string contents2, *dl2); + XAWAIT(string contents2, *dl2); return contents + " and " + contents2; END_ASYNC() } diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md index fbf5414bc..391862972 100644 --- a/Networking/BLIP/docs/Async.md +++ b/Networking/BLIP/docs/Async.md @@ -257,7 +257,7 @@ If we run this through the preprocessor, we get: ```c++ Async provideSumPlus() { string a; - return Async(_thisActor(), [=](AsyncState &_async_state_) mutable // (a) + return Async(thisActor(), [=](AsyncState &_async_state_) mutable // (a) -> std::optional { // switch (_async_state_.currentLine()) { // default: // @@ -272,9 +272,9 @@ Async provideSumPlus() { } ``` -`BEGIN_ASYNC_RETURNING(string)` turns into `return Async(...)`, where the constructor parameters are `_thisActor()`, and a lambda that contains the rest of the function wrapped in a `switch` statement. +`BEGIN_ASYNC_RETURNING(string)` turns into `return Async(...)`, where the constructor parameters are `thisActor()`, and a lambda that contains the rest of the function wrapped in a `switch` statement. -`_thisActor()` is simple: within the scope of an Actor method, it’s an inline method that returns `this`. Otherwise, it’s a global function that returns `nullptr`. So this call evaluates to the Actor implementing this function/method, if any. +`thisActor()` is simple: within the scope of an Actor method, it’s an inline method that returns `this`. Otherwise, it’s a global function that returns `nullptr`. So this call evaluates to the Actor implementing this function/method, if any. The `Async` constructor either calls the lambda immediately or (if it’s in an Actor method) schedules it to be called on the Actor’s queue. The function ends up returning this Async instance, whether or not it’s been resolved. From 04093a2a96acdd323a45d0d90a85b8ac9ab3b975 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 17 Feb 2022 17:23:07 -0800 Subject: [PATCH 07/78] Use Async in the replicator --- Networking/BLIP/BLIPConnection.cc | 16 ++++ Networking/BLIP/BLIPConnection.hh | 8 ++ Replicator/Puller.cc | 22 ++--- Replicator/Puller.hh | 3 +- Replicator/Pusher.cc | 75 +++++++-------- Replicator/Pusher.hh | 4 +- Replicator/Replicator.cc | 149 +++++++++++++++--------------- Replicator/Worker.cc | 16 ++++ Replicator/Worker.hh | 11 ++- 9 files changed, 176 insertions(+), 128 deletions(-) diff --git a/Networking/BLIP/BLIPConnection.cc b/Networking/BLIP/BLIPConnection.cc index 8bafec0c6..5e3fe9de3 100644 --- a/Networking/BLIP/BLIPConnection.cc +++ b/Networking/BLIP/BLIPConnection.cc @@ -664,6 +664,22 @@ namespace litecore { namespace blip { } + Connection::AsyncResponse Connection::sendAsyncRequest(MessageBuilder& builder) { + auto provider = AsyncResponse::makeProvider(); + builder.onProgress = [provider, oldOnProgress=std::move(builder.onProgress)] + (MessageProgress progress) { + if (progress.state >= MessageProgress::kComplete) + provider->setResult(progress.reply); + if (oldOnProgress) + oldOnProgress(progress); + }; + sendRequest(builder); + return provider; + } + + + + /** Internal API to send an outgoing message (a request, response, or ACK.) */ void Connection::send(MessageOut *msg) { if (_compressionLevel == 0) diff --git a/Networking/BLIP/BLIPConnection.hh b/Networking/BLIP/BLIPConnection.hh index a548ff014..adaf7627a 100644 --- a/Networking/BLIP/BLIPConnection.hh +++ b/Networking/BLIP/BLIPConnection.hh @@ -11,6 +11,7 @@ // #pragma once +#include "Async.hh" #include "WebSocketInterface.hh" #include "Message.hh" #include "Logging.hh" @@ -66,6 +67,13 @@ namespace litecore { namespace blip { /** Sends a built message as a new request. */ void sendRequest(MessageBuilder&); + using AsyncResponse = actor::Async>; + + /** Sends a built message as a new request and returns an async value that can be used + to get the response when it arrives. + @note The response will immediately resolve to `nullptr` if the connection closes. */ + AsyncResponse sendAsyncRequest(MessageBuilder&); + typedef std::function RequestHandler; /** Registers a callback that will be called when a message with a given profile arrives. */ diff --git a/Replicator/Puller.cc b/Replicator/Puller.cc index a0e3578a7..38ccd84c3 100644 --- a/Replicator/Puller.cc +++ b/Replicator/Puller.cc @@ -58,7 +58,8 @@ namespace litecore { namespace repl { // Starting an active pull. - void Puller::_start(RemoteSequence sinceSequence) { + void Puller::start(RemoteSequence sinceSequence) { + BEGIN_ASYNC(); _lastSequence = sinceSequence; _missingSequences.clear(sinceSequence); alloc_slice sinceStr = _lastSequence.toJSON(); @@ -111,16 +112,15 @@ namespace litecore { namespace repl { enc.writeValue(docIDs); enc.endDict(); } - - sendRequest(msg, [=](blip::MessageProgress progress) { - //... After request is sent: - if (progress.reply && progress.reply->isError()) { - gotError(progress.reply); - _fatalError = true; - } - if (progress.state == MessageProgress::kComplete) - Signpost::end(Signpost::blipSent); - }); + + AWAIT(Retained, reply, sendAsyncRequest(msg)); + + if (reply && reply->isError()) { + gotError(reply); + _fatalError = true; + } + Signpost::end(Signpost::blipSent); + END_ASYNC(); } diff --git a/Replicator/Puller.hh b/Replicator/Puller.hh index efb3bc3a1..82857aab2 100644 --- a/Replicator/Puller.hh +++ b/Replicator/Puller.hh @@ -32,7 +32,7 @@ namespace litecore { namespace repl { void setSkipDeleted() {_skipDeleted = true;} // Starts an active pull - void start(RemoteSequence sinceSequence) {enqueue(FUNCTION_TO_QUEUE(Puller::_start), sinceSequence);} + void start(RemoteSequence sinceSequence); // Called only by IncomingRev void revWasProvisionallyHandled() {_provisionallyHandledRevs.add(1);} @@ -54,7 +54,6 @@ namespace litecore { namespace repl { void activityLevelChanged(ActivityLevel level); private: - void _start(RemoteSequence sinceSequence); void _expectSequences(std::vector); void _documentsRevoked(std::vector>); void handleRev(Retained); diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index 3c017dade..f6b223756 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -176,7 +176,7 @@ namespace litecore { namespace repl { // Send the "changes" request: auto changeCount = changes.revs.size(); - sendChanges(changes.revs); + sendChanges(move(changes.revs)); if (!changes.askAgain) { // ChangesFeed says there are not currently any more changes, i.e. we've caught up. @@ -188,8 +188,7 @@ namespace litecore { namespace repl { if (changeCount > 0 && passive()) { // The protocol says catching up is signaled by an empty changes list, so send // one if we didn't already: - RevToSendList empty; - sendChanges(empty); + sendChanges(RevToSendList{}); } } } else if (_continuous) { @@ -222,23 +221,35 @@ namespace litecore { namespace repl { #pragma mark - SENDING A "CHANGES" MESSAGE & HANDLING RESPONSE: + void Pusher::encodeRevID(Encoder &enc, slice revID) { + if (_db->usingVersionVectors() && revID.findByte('*')) + enc << _db->convertVersionToAbsolute(revID); + else + enc << revID; + } + + // Sends a "changes" or "proposeChanges" message. - void Pusher::sendChanges(RevToSendList &changes) { - MessageBuilder req(_proposeChanges ? "proposeChanges"_sl : "changes"_sl); - if(_proposeChanges) { + void Pusher::sendChanges(RevToSendList &&in_changes) { + bool const proposedChanges = _proposeChanges; + auto changes = make_shared(move(in_changes)); + + BEGIN_ASYNC() + MessageBuilder req(proposedChanges ? "proposeChanges"_sl : "changes"_sl); + if(proposedChanges) { req[kConflictIncludesRevProperty] = "true"_sl; } req.urgent = tuning::kChangeMessagesAreUrgent; - req.compressed = !changes.empty(); + req.compressed = !changes->empty(); // Generate the JSON array of changes: auto &enc = req.jsonBody(); enc.beginArray(); - for (RevToSend *change : changes) { + for (RevToSend *change : *changes) { // Write the info array for this change: enc.beginArray(); - if (_proposeChanges) { + if (proposedChanges) { enc << change->docID; encodeRevID(enc, change->revID); slice remoteAncestorRevID = change->remoteAncestorRevID; @@ -263,40 +274,24 @@ namespace litecore { namespace repl { } enc.endArray(); - if (changes.empty()) { + if (changes->empty()) { // Empty == just announcing 'caught up', so no need to get a reply req.noreply = true; sendRequest(req); return; } - bool proposedChanges = _proposeChanges; - increment(_changeListsInFlight); - sendRequest(req, [this,changes=move(changes),proposedChanges](MessageProgress progress) mutable { - if (progress.state == MessageProgress::kComplete) - handleChangesResponse(changes, progress.reply, proposedChanges); - }); - } - - - void Pusher::encodeRevID(Encoder &enc, slice revID) { - if (_db->usingVersionVectors() && revID.findByte('*')) - enc << _db->convertVersionToAbsolute(revID); - else - enc << revID; - } + //---- SEND REQUEST AND WAIT FOR REPLY ---- + AWAIT(Retained, reply, sendAsyncRequest(req)); + if (!reply) + return; - // Handles the peer's response to a "changes" or "proposeChanges" message: - void Pusher::handleChangesResponse(RevToSendList &changes, - MessageIn *reply, - bool proposedChanges) - { // Got reply to the "changes" or "proposeChanges": - if (!changes.empty()) { + if (!changes->empty()) { logInfo("Got response for %zu local changes (sequences from %" PRIu64 ")", - changes.size(), (uint64_t)changes.front()->sequence); + changes->size(), (uint64_t)changes->front()->sequence); } decrement(_changeListsInFlight); _changesFeed.setFindForeignAncestors(getForeignAncestors()); @@ -308,11 +303,11 @@ namespace litecore { namespace repl { logInfo("Server requires 'proposeChanges'; retrying..."); _proposeChanges = true; _changesFeed.setFindForeignAncestors(getForeignAncestors()); - sendChanges(changes); + sendChanges(move(*changes)); } else { logError("Server does not allow '%s'; giving up", (_proposeChanges ? "proposeChanges" : "changes")); - for(RevToSend* change : changes) + for(RevToSend* change : *changes) doneWithRev(change, false, false); gotError(C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, "Incompatible with server replication protocol (changes)"_sl)); @@ -326,7 +321,7 @@ namespace litecore { namespace repl { maybeGetMoreChanges(); if (reply->isError()) { - for(RevToSend* change : changes) + for(RevToSend* change : *changes) doneWithRev(change, false, false); gotError(reply); return; @@ -342,7 +337,7 @@ namespace litecore { namespace repl { // The response body consists of an array that parallels the `changes` array I sent: Array::iterator iResponse(reply->JSONBody().asArray()); - for (RevToSend *change : changes) { + for (RevToSend *change : *changes) { change->maxHistory = maxHistory; change->legacyAttachments = legacyAttachments; change->deltaOK = _deltasOK; @@ -357,6 +352,8 @@ namespace litecore { namespace repl { ++iResponse; } maybeSendMoreRevs(); + + END_ASYNC() } @@ -414,8 +411,7 @@ namespace litecore { namespace repl { if (shouldRetryConflictWithNewerAncestor(change, serverRevID)) { // I have a newer revision to send in its place: - RevToSendList changes = {change}; - sendChanges(changes); + sendChanges(RevToSendList{change}); return true; } else if (_options->pull <= kC4Passive) { C4Error error = C4Error::make(WebSocketDomain, 409, @@ -548,8 +544,7 @@ namespace litecore { namespace repl { if (!passive()) _checkpointer.addPendingSequence(change->sequence); addProgress({0, change->bodySize}); - RevToSendList changes = {change}; - sendChanges(changes); + sendChanges(RevToSendList{change}); } diff --git a/Replicator/Pusher.hh b/Replicator/Pusher.hh index 855cd5792..3a544518f 100644 --- a/Replicator/Pusher.hh +++ b/Replicator/Pusher.hh @@ -59,8 +59,8 @@ namespace litecore { namespace repl { void handleSubChanges(Retained req); void gotOutOfOrderChange(RevToSend* NONNULL); void encodeRevID(Encoder &enc, slice revID); - void sendChanges(RevToSendList&); - void handleChangesResponse(RevToSendList&, blip::MessageIn*, bool proposedChanges); + void sendChanges(RevToSendList&&); + void handleChangesResponse(const RevToSendList&, blip::MessageIn*, bool proposedChanges); bool handleChangeResponse(RevToSend *change, Value response); bool handleProposedChangeResponse(RevToSend *change, Value response); bool handlePushConflict(RevToSend *change); diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index 352da0778..e8136040d 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -531,6 +531,8 @@ namespace litecore { namespace repl { // Get the remote checkpoint, after we've got the local one and the BLIP connection is up. void Replicator::getRemoteCheckpoint(bool refresh) { + BEGIN_ASYNC() + if (_remoteCheckpointRequested) return; // already in progress if (!_remoteCheckpointDocID) @@ -542,41 +544,6 @@ namespace litecore { namespace repl { MessageBuilder msg("getCheckpoint"_sl); msg["client"_sl] = _remoteCheckpointDocID; Signpost::begin(Signpost::blipSent); - sendRequest(msg, [this, refresh](MessageProgress progress) { - // ...after the checkpoint is received: - if (progress.state != MessageProgress::kComplete) - return; - Signpost::end(Signpost::blipSent); - MessageIn *response = progress.reply; - Checkpoint remoteCheckpoint; - - if (response->isError()) { - auto err = response->getError(); - if (!(err.domain == "HTTP"_sl && err.code == 404)) - return gotError(response); - logInfo("No remote checkpoint '%.*s'", SPLAT(_remoteCheckpointDocID)); - _remoteCheckpointRevID.reset(); - } else { - remoteCheckpoint.readJSON(response->body()); - _remoteCheckpointRevID = response->property("rev"_sl); - logInfo("Received remote checkpoint (rev='%.*s'): %.*s", - SPLAT(_remoteCheckpointRevID), SPLAT(response->body())); - } - _remoteCheckpointReceived = true; - - if (!refresh && _hadLocalCheckpoint) { - // Compare checkpoints, reset if mismatched: - bool valid = _checkpointer.validateWith(remoteCheckpoint); - if (!valid && _pusher) - _pusher->checkpointIsInvalid(); - - // Now we have the checkpoints! Time to start replicating: - startReplicating(); - } - - if (_checkpointJSONToSave) - saveCheckpointNow(); // _saveCheckpoint() was waiting for _remoteCheckpointRevID - }); _remoteCheckpointRequested = true; @@ -584,6 +551,42 @@ namespace litecore { namespace repl { // wait for the remote one before getting started: if (!refresh && !_hadLocalCheckpoint) startReplicating(); + + AWAIT(Retained, response, sendAsyncRequest(msg)); + + // ...after the checkpoint is received: + Signpost::end(Signpost::blipSent); + Checkpoint remoteCheckpoint; + + if (!response) + return; + if (response->isError()) { + auto err = response->getError(); + if (!(err.domain == "HTTP"_sl && err.code == 404)) + return gotError(response); + logInfo("No remote checkpoint '%.*s'", SPLAT(_remoteCheckpointDocID)); + _remoteCheckpointRevID.reset(); + } else { + remoteCheckpoint.readJSON(response->body()); + _remoteCheckpointRevID = response->property("rev"_sl); + logInfo("Received remote checkpoint (rev='%.*s'): %.*s", + SPLAT(_remoteCheckpointRevID), SPLAT(response->body())); + } + _remoteCheckpointReceived = true; + + if (!refresh && _hadLocalCheckpoint) { + // Compare checkpoints, reset if mismatched: + bool valid = _checkpointer.validateWith(remoteCheckpoint); + if (!valid && _pusher) + _pusher->checkpointIsInvalid(); + + // Now we have the checkpoints! Time to start replicating: + startReplicating(); + } + + if (_checkpointJSONToSave) + saveCheckpointNow(); // _saveCheckpoint() was waiting for _remoteCheckpointRevID + END_ASYNC() } @@ -598,6 +601,9 @@ namespace litecore { namespace repl { void Replicator::saveCheckpointNow() { + alloc_slice json = move(_checkpointJSONToSave); + + BEGIN_ASYNC() // Switch to the permanent checkpoint ID: alloc_slice checkpointID = _checkpointer.checkpointID(); if (checkpointID != _remoteCheckpointDocID) { @@ -605,8 +611,6 @@ namespace litecore { namespace repl { _remoteCheckpointRevID = nullslice; } - alloc_slice json = move(_checkpointJSONToSave); - logVerbose("Saving remote checkpoint '%.*s' over rev='%.*s': %.*s ...", SPLAT(_remoteCheckpointDocID), SPLAT(_remoteCheckpointRevID), SPLAT(json)); Assert(_remoteCheckpointReceived); @@ -617,44 +621,45 @@ namespace litecore { namespace repl { msg["rev"_sl] = _remoteCheckpointRevID; msg << json; Signpost::begin(Signpost::blipSent); - sendRequest(msg, [=](MessageProgress progress) { - if (progress.state != MessageProgress::kComplete) - return; - Signpost::end(Signpost::blipSent); - MessageIn *response = progress.reply; - if (response->isError()) { - Error responseErr = response->getError(); - if (responseErr.domain == "HTTP"_sl && responseErr.code == 409) { - // On conflict, read the remote checkpoint to get the real revID: - _checkpointJSONToSave = json; // move() has no effect here - _remoteCheckpointRequested = _remoteCheckpointReceived = false; - getRemoteCheckpoint(true); - } else { - gotError(response); - warn("Failed to save remote checkpoint!"); - // If the checkpoint didn't save, something's wrong; but if we don't mark it as - // saved, the replicator will stay busy (see computeActivityLevel, line 169). - _checkpointer.saveCompleted(); - } + + AWAIT(Retained, response, sendAsyncRequest(msg)); + + Signpost::end(Signpost::blipSent); + if (!response) + return; + else if (response->isError()) { + Error responseErr = response->getError(); + if (responseErr.domain == "HTTP"_sl && responseErr.code == 409) { + // On conflict, read the remote checkpoint to get the real revID: + _checkpointJSONToSave = json; // move() has no effect here + _remoteCheckpointRequested = _remoteCheckpointReceived = false; + getRemoteCheckpoint(true); } else { - // Remote checkpoint saved, so update local one: - _remoteCheckpointRevID = response->property("rev"_sl); - logInfo("Saved remote checkpoint '%.*s' as rev='%.*s'", - SPLAT(_remoteCheckpointDocID), SPLAT(_remoteCheckpointRevID)); - - try { - _db->useLocked([&](C4Database *db) { - _db->markRevsSyncedNow(); - _checkpointer.write(db, json); - }); - logInfo("Saved local checkpoint '%.*s': %.*s", - SPLAT(_remoteCheckpointDocID), SPLAT(json)); - } catch (...) { - gotError(C4Error::fromCurrentException()); - } + gotError(response); + warn("Failed to save remote checkpoint!"); + // If the checkpoint didn't save, something's wrong; but if we don't mark it as + // saved, the replicator will stay busy (see computeActivityLevel, line 169). _checkpointer.saveCompleted(); } - }); + } else { + // Remote checkpoint saved, so update local one: + _remoteCheckpointRevID = response->property("rev"_sl); + logInfo("Saved remote checkpoint '%.*s' as rev='%.*s'", + SPLAT(_remoteCheckpointDocID), SPLAT(_remoteCheckpointRevID)); + + try { + _db->useLocked([&](C4Database *db) { + _db->markRevsSyncedNow(); + _checkpointer.write(db, json); + }); + logInfo("Saved local checkpoint '%.*s': %.*s", + SPLAT(_remoteCheckpointDocID), SPLAT(json)); + } catch (...) { + gotError(C4Error::fromCurrentException()); + } + _checkpointer.saveCompleted(); + } + END_ASYNC() } diff --git a/Replicator/Worker.cc b/Replicator/Worker.cc index a0bded4c9..d1598910b 100644 --- a/Replicator/Worker.cc +++ b/Replicator/Worker.cc @@ -126,6 +126,22 @@ namespace litecore { namespace repl { } + Worker::AsyncResponse Worker::sendAsyncRequest(blip::MessageBuilder& builder) { + Assert(isCurrentActor()); + increment(_pendingResponseCount); + builder.onProgress = [=](MessageProgress progress) { + if (progress.state >= MessageProgress::kComplete) + enqueue(FUNCTION_TO_QUEUE(Worker::_endAsyncRequest)); + }; + return connection().sendAsyncRequest(builder); + } + + + void Worker::_endAsyncRequest() { + decrement(_pendingResponseCount); + } + + #pragma mark - ERRORS: diff --git a/Replicator/Worker.hh b/Replicator/Worker.hh index 7465fcf95..aecd523c6 100644 --- a/Replicator/Worker.hh +++ b/Replicator/Worker.hh @@ -12,6 +12,7 @@ #pragma once #include "Actor.hh" +#include "Async.hh" #include "ReplicatorOptions.hh" #include "BLIPConnection.hh" #include "Message.hh" @@ -98,7 +99,7 @@ namespace litecore { namespace repl { /// @param namePrefix Prepended to the Actor name. Worker(blip::Connection *connection NONNULL, Worker *parent, - const Options* options NONNULL, + const Options* options, std::shared_ptr db, const char *namePrefix NONNULL); @@ -151,6 +152,12 @@ namespace litecore { namespace repl { void sendRequest(blip::MessageBuilder& builder, blip::MessageProgressCallback onProgress = nullptr); + using AsyncResponse = actor::Async>; + + /// Sends a BLIP request, like `sendRequest` but returning the response asynchronously. + /// Note: The response object will be nullptr if the connection closed. + AsyncResponse sendAsyncRequest(blip::MessageBuilder& builder); + /// The number of BLIP responses I'm waiting for. int pendingResponseCount() const {return _pendingResponseCount;} @@ -200,6 +207,8 @@ namespace litecore { namespace repl { /// or this Actor has pending events in its queue, else `kC4Idle`. virtual ActivityLevel computeActivityLevel() const; + void _endAsyncRequest(); + #pragma mark - INSTANCE DATA: protected: RetainedConst _options; // The replicator options From 8762084b2b5c522678f954bbe05e61ca1533a2b0 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 15 Feb 2022 12:57:51 -0800 Subject: [PATCH 08/78] ConnectedClient prototype Only getDoc() is implemented so far. Also implemented the corresponding `getRev` BLIP message in the replicator, so it can be used in tests. --- Replicator/ConnectedClient/ConnectedClient.cc | 263 ++++++++++++++++++ Replicator/ConnectedClient/ConnectedClient.hh | 116 ++++++++ Replicator/Pusher+Revs.cc | 87 ++++-- Replicator/Pusher.cc | 2 + Replicator/Pusher.hh | 2 + Replicator/ReplicatorTypes.cc | 6 + Replicator/ReplicatorTypes.hh | 1 + Replicator/tests/ConnectedClientTest.cc | 138 +++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 18 ++ 9 files changed, 613 insertions(+), 20 deletions(-) create mode 100644 Replicator/ConnectedClient/ConnectedClient.cc create mode 100644 Replicator/ConnectedClient/ConnectedClient.hh create mode 100644 Replicator/tests/ConnectedClientTest.cc diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc new file mode 100644 index 000000000..69e8d8e25 --- /dev/null +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -0,0 +1,263 @@ +// +// ConnectedClient.cc +// +// Copyright © 2022 Couchbase. All rights reserved. +// +// 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. +// + +#include "ConnectedClient.hh" +#include "c4BlobStoreTypes.h" +#include "c4SocketTypes.h" +#include "Headers.hh" +#include "MessageBuilder.hh" +#include "WebSocketInterface.hh" + +namespace litecore::client { + using namespace std; + using namespace fleece; + using namespace litecore::actor; + using namespace blip; + + + ConnectedClient::ConnectedClient(websocket::WebSocket* webSocket, + Delegate& delegate, + fleece::AllocedDict options) + :Worker(new Connection(webSocket, options, *this), + nullptr, nullptr, nullptr, "Client") + ,_delegate(&delegate) + ,_status(kC4Stopped) + { + _importance = 2; + } + + + void ConnectedClient::setStatus(ActivityLevel status) { + if (status != _status) { + _status = status; + _delegate->clientStatusChanged(this, status); + } + } + + + void ConnectedClient::start() { + BEGIN_ASYNC() + logInfo("Connecting..."); + Assert(_status == kC4Stopped); + setStatus(kC4Connecting); + connection().start(); + _selfRetain = this; // retain myself while the connection is open + END_ASYNC(); + } + + void ConnectedClient::stop() { + BEGIN_ASYNC() + _disconnect(websocket::kCodeNormal, {}); + END_ASYNC(); + } + + + void ConnectedClient::_disconnect(websocket::CloseCode closeCode, slice message) { + if (connected()) { + logInfo("Disconnecting..."); + connection().close(closeCode, message); + setStatus(kC4Stopping); + } + } + + +#pragma mark - BLIP DELEGATE: + + + void ConnectedClient::onTLSCertificate(slice certData) { + if (_delegate) + _delegate->clientGotTLSCertificate(this, certData); + } + + + void ConnectedClient::onHTTPResponse(int status, const websocket::Headers &headers) { + BEGIN_ASYNC() + logVerbose("Got HTTP response from server, status %d", status); + if (_delegate) + _delegate->clientGotHTTPResponse(this, status, headers); + if (status == 101 && !headers["Sec-WebSocket-Protocol"_sl]) { + gotError(C4Error::make(WebSocketDomain, kWebSocketCloseProtocolError, + "Incompatible replication protocol " + "(missing 'Sec-WebSocket-Protocol' response header)"_sl)); + } + END_ASYNC() + } + + + void ConnectedClient::onConnect() { + BEGIN_ASYNC() + logInfo("Connected!"); + if (_status != kC4Stopping) // skip this if stop() already called + setStatus(kC4Idle); + END_ASYNC() + } + + + void ConnectedClient::onClose(Connection::CloseStatus status, Connection::State state) { + BEGIN_ASYNC() + logInfo("Connection closed with %-s %d: \"%.*s\" (state=%d)", + status.reasonName(), status.code, FMTSLICE(status.message), state); + + bool closedByPeer = (_status != kC4Stopping); + setStatus(kC4Stopped); + + _connectionClosed(); + + if (status.isNormal() && closedByPeer) { + logInfo("I didn't initiate the close; treating this as code 1001 (GoingAway)"); + status.code = websocket::kCodeGoingAway; + status.message = alloc_slice("WebSocket connection closed by peer"); + } + + static const C4ErrorDomain kDomainForReason[] = {WebSocketDomain, POSIXDomain, + NetworkDomain, LiteCoreDomain}; + + // If this was an unclean close, set my error property: + if (status.reason != websocket::kWebSocketClose || status.code != websocket::kCodeNormal) { + int code = status.code; + C4ErrorDomain domain; + if (status.reason < sizeof(kDomainForReason)/sizeof(C4ErrorDomain)) { + domain = kDomainForReason[status.reason]; + } else { + domain = LiteCoreDomain; + code = kC4ErrorRemoteError; + } + gotError(C4Error::make(domain, code, status.message)); + } + + if (_delegate) + _delegate->clientConnectionClosed(this, status); + + _selfRetain = nullptr; // balances the self-retain in start() + END_ASYNC() + } + + + // This only gets called if none of the registered handlers were triggered. + void ConnectedClient::onRequestReceived(MessageIn *msg) { + warn("Received unrecognized BLIP request #%" PRIu64 " with Profile '%.*s', %zu bytes", + msg->number(), FMTSLICE(msg->property("Profile"_sl)), msg->body().size); + msg->notHandled(); + } + + +#pragma mark - REQUESTS: + + + // Returns the error status of a response (including a NULL response, i.e. disconnection) + C4Error ConnectedClient::responseError(MessageIn *response) { + if (!response) { + // Disconnected! + return status().error ? status().error : C4Error::make(LiteCoreDomain, + kC4ErrorIOError, + "network connection lost"); + // TODO: Use a better default error than the one above + } else if (response->isError()) { + return blipToC4Error(response->getError()); + } else { + return {}; + } + } + + + Async ConnectedClient::getDoc(alloc_slice docID, + alloc_slice collectionID, + alloc_slice unlessRevID) + { + BEGIN_ASYNC_RETURNING(DocResponseOrError) + logInfo("getDoc(\"%.*s\")", FMTSLICE(docID)); + MessageBuilder req("getRev"); + req["id"] = docID; + req["ifNotRev"] = unlessRevID; + + AWAIT(Retained, response, sendAsyncRequest(req)); + logInfo("...getDoc got response"); + + if (C4Error err = responseError(response)) + return err; + + FLError flErr; + alloc_slice fleeceBody(FLData_ConvertJSON(response->body(), &flErr)); + if (!fleeceBody) + return C4Error::make(FleeceDomain, flErr); + + return DocResponse { + docID, + alloc_slice(response->property("rev")), + fleeceBody, + response->boolProperty("deleted") + }; + END_ASYNC() + } + + + Async ConnectedClient::getBlob(alloc_slice docID, + alloc_slice collectionID, + C4BlobKey blobKey, + bool compress) { + BEGIN_ASYNC_RETURNING(BlobOrError) + auto digest = blobKey.digestString(); + logInfo("getAttachment(\"%.*s\", <%s>)", FMTSLICE(docID), digest.c_str()); + MessageBuilder req("getAttachment"); + req["digest"] = digest; + req["docID"] = docID; + if (compress) + req["compress"] = "true"; + + AWAIT(Retained, response, sendAsyncRequest(req)); + logInfo("...getAttachment got response"); + + if (C4Error err = responseError(response)) + return err; + return response->body(); + END_ASYNC() + } + + + Async ConnectedClient::putDoc(alloc_slice docID, + alloc_slice collectionID, + alloc_slice revID, + alloc_slice parentRevID, + C4RevisionFlags revisionFlags, + alloc_slice fleeceData) + { + BEGIN_ASYNC_RETURNING(C4Error) + logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID), FMTSLICE(revID)); + MessageBuilder req("rev"); + req.compressed = true; + req["id"] = docID; + req["rev"] = revID; + req["history"] = parentRevID; + if (revisionFlags & kRevDeleted) + req["deleted"] = "1"; + + if (fleeceData.size > 0) { + // TODO: Convert blobs to legacy attachments + req.jsonBody().writeValue(Doc(fleeceData, kFLTrusted).asDict()); + } else { + req.write("{}"); + } + + AWAIT(Retained, response, sendAsyncRequest(req)); + logInfo("...putDoc got response"); + + return responseError(response); + END_ASYNC() + } + +} diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh new file mode 100644 index 000000000..91e7ea5a6 --- /dev/null +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -0,0 +1,116 @@ +// +// ConnectedClient.hh +// +// Copyright © 2022 Couchbase. All rights reserved. +// + +#pragma once +#include "Worker.hh" +#include "Async.hh" +#include "BLIPConnection.hh" +#include "c4ReplicatorTypes.h" +#include + +namespace litecore::client { + + struct DocResponse { + alloc_slice docID, revID, body; + bool deleted; + }; + + using DocResponseOrError = std::variant; + + using BlobOrError = std::variant; + + + class ConnectedClient : public repl::Worker, + private blip::ConnectionDelegate + { + public: + class Delegate; + using CloseStatus = blip::Connection::CloseStatus; + using ActivityLevel = C4ReplicatorActivityLevel; + + ConnectedClient(websocket::WebSocket* NONNULL, + Delegate&, + fleece::AllocedDict options); + + class Delegate { + public: + virtual void clientGotHTTPResponse(ConnectedClient* NONNULL, + int status, + const websocket::Headers &headers) { } + virtual void clientGotTLSCertificate(ConnectedClient* NONNULL, + slice certData) =0; + virtual void clientStatusChanged(ConnectedClient* NONNULL, + ActivityLevel) =0; + virtual void clientConnectionClosed(ConnectedClient* NONNULL, + const CloseStatus&) { } + virtual ~Delegate() =default; + }; + + void start(); + void stop(); + + /// Gets the current revision of a document from the server. + /// You can set the `unlessRevID` parameter to avoid getting a redundant copy of a + /// revision you already have. + /// @param docID The document ID. + /// @param collectionID The name of the document's collection, or `nullslice` for default. + /// @param unlessRevID If non-null, and equal to the current server-side revision ID, + /// the server will return error {WebSocketDomain, 304}. + /// @return An async value that, when resolved, contains either a `DocResponse` struct + /// or a C4Error. + actor::Async getDoc(alloc_slice docID, + alloc_slice collectionID, + alloc_slice unlessRevID); + + /// Gets the contents of a blob given its digest. + /// @param docID The ID of the document referencing this blob. + /// @param collectionID The name of the document's collection, or `nullslice` for default. + /// @param blobKey The binary digest of the blob. + /// @param compress True if the blob should be downloaded in compressed form. + /// @return An async value that, when resolved, contains either the blob body or a C4Error. + actor::Async getBlob(alloc_slice docID, + alloc_slice collectionID, + C4BlobKey blobKey, + bool compress); + + /// Pushes a document revision to the server. + /// @param docID The document ID. + /// @param collectionID The name of the document's collection, or `nullslice` for default. + /// @param revID The revision ID you're sending. + /// @param parentRevID The ID of the parent revision on the server, + /// or `nullslice` if this is a new document. + /// @param revisionFlags Flags of this revision. + /// @param fleeceData The document body encoded as Fleece (without shared keys!) + /// @return An async value that, when resolved, contains the status as a C4Error. + actor::Async putDoc(alloc_slice docID, + alloc_slice collectionID, + alloc_slice revID, + alloc_slice parentRevID, + C4RevisionFlags revisionFlags, + alloc_slice fleeceData); + + // exposed for unit tests: + websocket::WebSocket* webSocket() const {return connection().webSocket();} + + protected: + std::string loggingClassName() const override {return "Client";} + void onHTTPResponse(int status, const websocket::Headers &headers) override; + void onTLSCertificate(slice certData) override; + void onConnect() override; + void onClose(blip::Connection::CloseStatus status, blip::Connection::State state) override; + void onRequestReceived(blip::MessageIn* request) override; + + private: + void setStatus(ActivityLevel); + C4Error responseError(blip::MessageIn *response); + void _disconnect(websocket::CloseCode closeCode, slice message); + + Delegate* _delegate; // Delegate whom I report progress/errors to + ActivityLevel _status; + Retained _selfRetain; + }; + +} diff --git a/Replicator/Pusher+Revs.cc b/Replicator/Pusher+Revs.cc index ac177d8ad..8fae945d4 100644 --- a/Replicator/Pusher+Revs.cc +++ b/Replicator/Pusher+Revs.cc @@ -19,6 +19,7 @@ #include "Increment.hh" #include "StringUtil.hh" #include "c4Document.hh" +#include "c4DocEnumeratorTypes.h" #include "fleece/Mutable.hh" #include @@ -44,22 +45,27 @@ namespace litecore::repl { } - // Send a "rev" message containing a revision body. - void Pusher::sendRevision(Retained request) { - if (!connected()) - return; - - logVerbose("Sending rev '%.*s' #%.*s (seq #%" PRIu64 ") [%d/%d]", - SPLAT(request->docID), SPLAT(request->revID), (uint64_t)request->sequence, - _revisionsInFlight, tuning::kMaxRevsInFlight); - + // Creates a revision message from a RevToSend. Returns a BLIP error code. + int Pusher::buildRevisionMessage(RevToSend *request, + MessageBuilder &msg, + slice ifNotRevID) + { // Get the document & revision: C4Error c4err = {}; Dict root; Retained doc = _db->getDoc(request->docID, kDocGetAll); if (doc) { - if (doc->selectRevision(request->revID, true)) + if (request->revID.empty()) { + // When called from `handleGetRev`, all the request has is the docID + if (doc->revID() == ifNotRevID) + return 304; // NotChanged root = doc->getProperties(); + request->setRevID(doc->revID()); + request->sequence = doc->sequence(); + request->flags = doc->selectedRev().flags; + } else if (doc->selectRevision(request->revID, true)) { + root = doc->getProperties(); + } if (root) request->flags = doc->selectedRev().flags; else @@ -88,7 +94,7 @@ namespace litecore::repl { // Now send the BLIP message. Normally it's "rev", but if this is an error we make it // "norev" and include the error code: - MessageBuilder msg(root ? "rev"_sl : "norev"_sl); + //MessageBuilder msg(root ? "rev"_sl : "norev"_sl); msg.compressed = true; msg["id"_sl] = request->docID; msg["rev"_sl] = fullRevID; @@ -131,24 +137,44 @@ namespace litecore::repl { } logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", SPLAT(request->docID), SPLAT(request->revID)); - sendRequest(msg, [this, request](MessageProgress progress) { - onRevProgress(request, progress); - }); - increment(_revisionsInFlight); + return 0; } else { - // Send an error if we couldn't get the revision: - int blipError; if (c4err.domain == WebSocketDomain) - blipError = c4err.code; + return c4err.code; else if (c4err.domain == LiteCoreDomain && c4err.code == kC4ErrorNotFound) - blipError = 404; + return 404; else { warn("sendRevision: Couldn't get rev '%.*s' %.*s from db: %s", SPLAT(request->docID), SPLAT(request->revID), c4err.description().c_str()); - blipError = 500; + return 500; } + } + } + + + // Send a "rev" message containing a revision body. + void Pusher::sendRevision(Retained request) { + if (!connected()) + return; + + logVerbose("Sending rev '%.*s' #%.*s (seq #%" PRIu64 ") [%d/%d]", + SPLAT(request->docID), SPLAT(request->revID), (uint64_t)request->sequence, + _revisionsInFlight, tuning::kMaxRevsInFlight); + + MessageBuilder msg; + int blipError = buildRevisionMessage(request, msg); + if (blipError == 0) { + msg["Profile"] = "rev"; + logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", + SPLAT(request->docID), SPLAT(request->revID)); + sendRequest(msg, [this, request](MessageProgress progress) { + onRevProgress(request, progress); + }); + increment(_revisionsInFlight); + } else { + msg["Profile"] = "norev"; msg["error"_sl] = blipError; msg.noreply = true; sendRequest(msg); @@ -356,4 +382,25 @@ namespace litecore::repl { } } + + // Handles "getRev", which is sent not by the replicator but by ConnectedClient. + void Pusher::handleGetRev(Retained req) { + alloc_slice docID(req->property("id")); + slice ifNotRev = req->property("ifNotRev"); + C4DocumentInfo info = {}; + info.docID = docID; + auto rev = make_retained(info); + MessageBuilder response(req); + int blipError = buildRevisionMessage(rev, response, ifNotRev); + if (blipError == 0) { + logVerbose("Responding to getRev('%.*s') with rev #%.*s", + SPLAT(docID), SPLAT(rev->revID)); + req->respond(response); + } else { + logVerbose("Responding to getRev('%.*s') with BLIP err %d", + SPLAT(docID), blipError); + req->respondWithError({"BLIP", blipError}); + } + } + } diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index f6b223756..ad5520f94 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -51,6 +51,8 @@ namespace litecore { namespace repl { registerHandler("subChanges", &Pusher::handleSubChanges); registerHandler("getAttachment", &Pusher::handleGetAttachment); registerHandler("proveAttachment", &Pusher::handleProveAttachment); + if (_passive) + registerHandler("getRev", &Pusher::handleGetRev); } diff --git a/Replicator/Pusher.hh b/Replicator/Pusher.hh index 3a544518f..c95e6c483 100644 --- a/Replicator/Pusher.hh +++ b/Replicator/Pusher.hh @@ -83,6 +83,7 @@ namespace litecore { namespace repl { // Pusher+Revs.cc: void maybeSendMoreRevs(); void retryRevs(RevToSendList, bool immediate); + int buildRevisionMessage(RevToSend*, blip::MessageBuilder&, slice ifNotRevID = {}); void sendRevision(Retained); void onRevProgress(Retained rev, const blip::MessageProgress&); void couldntSendRevision(RevToSend* NONNULL); @@ -91,6 +92,7 @@ namespace litecore { namespace repl { fleece::Dict root, size_t revSize, bool sendLegacyAttachments); void revToSendIsObsolete(const RevToSend &request, C4Error *c4err =nullptr); + void handleGetRev(Retained req); using DocIDToRevMap = std::unordered_map>; diff --git a/Replicator/ReplicatorTypes.cc b/Replicator/ReplicatorTypes.cc index f8a997acc..a2c83555f 100644 --- a/Replicator/ReplicatorTypes.cc +++ b/Replicator/ReplicatorTypes.cc @@ -38,6 +38,12 @@ namespace litecore { namespace repl { } + void RevToSend::setRevID(slice id) { + Assert(!revID); + ((alloc_slice&)revID) = id; + } + + void RevToSend::addRemoteAncestor(slice revID) { if (!revID) return; diff --git a/Replicator/ReplicatorTypes.hh b/Replicator/ReplicatorTypes.hh index 5c553ac36..92867e107 100644 --- a/Replicator/ReplicatorTypes.hh +++ b/Replicator/ReplicatorTypes.hh @@ -68,6 +68,7 @@ namespace litecore { namespace repl { void addRemoteAncestor(slice revID); bool hasRemoteAncestor(slice revID) const; + void setRevID(slice id); Dir dir() const override {return Dir::kPushing;} void trim() override; diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc new file mode 100644 index 000000000..0e2376587 --- /dev/null +++ b/Replicator/tests/ConnectedClientTest.cc @@ -0,0 +1,138 @@ +// +// ConnectedClientTest.cc +// +// Copyright © 2022 Couchbase. All rights reserved. +// +// 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. +// + +#include "c4Test.hh" +#include "ConnectedClient.hh" +#include "Replicator.hh" +#include "LoopbackProvider.hh" +#include "fleece/Fleece.hh" + + +using namespace std; +using namespace fleece; +using namespace litecore; +using namespace litecore::websocket; + + +class ConnectedClientLoopbackTest : public C4Test, + public repl::Replicator::Delegate, + public client::ConnectedClient::Delegate +{ +public: + + void start() { + auto serverOpts = make_retained(kC4Passive,kC4Passive); + _server = new repl::Replicator(db, + new LoopbackWebSocket(alloc_slice("ws://srv/"), + Role::Server, {}), + *this, serverOpts); + AllocedDict clientOpts; + _client = new client::ConnectedClient(new LoopbackWebSocket(alloc_slice("ws://cli/"), + Role::Client, {}), + *this, + clientOpts); + Headers headers; + headers.add("Set-Cookie"_sl, "flavor=chocolate-chip"_sl); + LoopbackWebSocket::bind(_server->webSocket(), _client->webSocket(), headers); + + _server->start(); + _client->start(); + } + + + void stop() { + if (_server) { + _server->stop(); + _server = nullptr; + } + if (_client) { + _client->stop(); + _client = nullptr; + } + } + + + ~ConnectedClientLoopbackTest() { + stop(); + } + + + void clientGotHTTPResponse(client::ConnectedClient* NONNULL, + int status, + const websocket::Headers &headers) override + { + C4Log("Client got HTTP response"); + } + void clientGotTLSCertificate(client::ConnectedClient* NONNULL, + slice certData) override + { + C4Log("Client got TLS certificate"); + } + void clientStatusChanged(client::ConnectedClient* NONNULL, + C4ReplicatorActivityLevel level) override { + C4Log("Client status changed: %d", int(level)); + } + void clientConnectionClosed(client::ConnectedClient* NONNULL, + const CloseStatus &close) override { + C4Log("Client connection closed: reason=%d, code=%d, message=%.*s", + int(close.reason), close.code, FMTSLICE(close.message)); + } + + + void replicatorGotHTTPResponse(repl::Replicator* NONNULL, + int status, + const websocket::Headers &headers) override { } + void replicatorGotTLSCertificate(slice certData) override { } + void replicatorStatusChanged(repl::Replicator* NONNULL, + const repl::Replicator::Status&) override { } + void replicatorConnectionClosed(repl::Replicator* NONNULL, + const CloseStatus&) override { } + void replicatorDocumentsEnded(repl::Replicator* NONNULL, + const repl::Replicator::DocumentsEnded&) override { } + void replicatorBlobProgress(repl::Replicator* NONNULL, + const repl::Replicator::BlobProgress&) override { } + + + Retained _server; + Retained _client; +}; + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + start(); + + C4Log("++++ Calling ConnectedClient::getDoc()..."); + auto asyncResult = _client->getDoc(alloc_slice("0000001"), nullslice, nullslice); + asyncResult.blockUntilReady(); + + C4Log("++++ Async value available!"); + auto &result = asyncResult.result(); + auto rev = std::get_if(&result); + REQUIRE(rev); + CHECK(rev->docID == "0000001"); + CHECK(rev->revID == "1-4cbe54d79c405e368613186b0bc7ac9ee4a50fbb"); + CHECK(rev->deleted == false); + Doc doc(rev->body); + CHECK(doc.asDict()["birthday"].asString() == "1983-09-18"); + + C4Log("++++ Stopping..."); + stop(); + _server = nullptr; + _client = nullptr; +} diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index b985b4ce9..6f8a7c7e1 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -180,6 +180,8 @@ 275067DC230B6AD500FA23B2 /* c4Listener.cc in Sources */ = {isa = PBXBuildFile; fileRef = 275A74D51ED3AA11008CB57B /* c4Listener.cc */; }; 275067E2230B6C5400FA23B2 /* libLiteCoreREST-static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27FC81E81EAAB0D90028E38E /* libLiteCoreREST-static.a */; }; 27513A5D1A687EF80055DC40 /* sqlite3_unicodesn_tokenizer.c in Sources */ = {isa = PBXBuildFile; fileRef = 27513A591A687E770055DC40 /* sqlite3_unicodesn_tokenizer.c */; }; + 2752C6B727BC41DC001C1B76 /* ConnectedClient.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2752C6B627BC41DC001C1B76 /* ConnectedClient.cc */; }; + 2752C7A727BF18F2001C1B76 /* ConnectedClientTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2752C7A627BF18F2001C1B76 /* ConnectedClientTest.cc */; }; 275313392065844800463E74 /* RESTSyncListener_stub.cc in Sources */ = {isa = PBXBuildFile; fileRef = 270BEE1D20647E8A005E8BE8 /* RESTSyncListener_stub.cc */; }; 2753AF721EBD190600C12E98 /* LogDecoder.cc in Sources */ = {isa = PBXBuildFile; fileRef = 270C6B871EBA2CD600E73415 /* LogDecoder.cc */; }; 2753AF7D1EBD1BE300C12E98 /* Logging_Stub.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2753AF7C1EBD1BE300C12E98 /* Logging_Stub.cc */; }; @@ -1099,6 +1101,9 @@ 27513A591A687E770055DC40 /* sqlite3_unicodesn_tokenizer.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = sqlite3_unicodesn_tokenizer.c; sourceTree = ""; }; 27513A5C1A687EA70055DC40 /* sqlite3_unicodesn_tokenizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sqlite3_unicodesn_tokenizer.h; sourceTree = ""; }; 2752C6AC27BC3A2E001C1B76 /* replication-protocol.adoc */ = {isa = PBXFileReference; lastKnownFileType = text; name = "replication-protocol.adoc"; path = "../../modules/docs/pages/replication-protocol.adoc"; sourceTree = ""; }; + 2752C6B527BC41DC001C1B76 /* ConnectedClient.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ConnectedClient.hh; sourceTree = ""; }; + 2752C6B627BC41DC001C1B76 /* ConnectedClient.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ConnectedClient.cc; sourceTree = ""; }; + 2752C7A627BF18F2001C1B76 /* ConnectedClientTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ConnectedClientTest.cc; sourceTree = ""; }; 2753AF7C1EBD1BE300C12E98 /* Logging_Stub.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Logging_Stub.cc; sourceTree = ""; }; 2754B0C01E5F49AA00A05FD0 /* StringUtil.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StringUtil.cc; sourceTree = ""; }; 2754B0C11E5F49AA00A05FD0 /* StringUtil.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StringUtil.hh; sourceTree = ""; }; @@ -2297,6 +2302,15 @@ name = "Supporting Files"; sourceTree = ""; }; + 2752C6B127BC417E001C1B76 /* ConnectedClient */ = { + isa = PBXGroup; + children = ( + 2752C6B527BC41DC001C1B76 /* ConnectedClient.hh */, + 2752C6B627BC41DC001C1B76 /* ConnectedClient.cc */, + ); + path = ConnectedClient; + sourceTree = ""; + }; 2754616A22F21F3900DF87B0 /* Crypto */ = { isa = PBXGroup; children = ( @@ -2423,6 +2437,7 @@ 274D17C02615445B0018D39C /* DBAccessTestWrapper.hh */, 274D17C12615445B0018D39C /* DBAccessTestWrapper.cc */, 275CE1051E5B79A80084E014 /* ReplicatorLoopbackTest.cc */, + 2752C7A627BF18F2001C1B76 /* ConnectedClientTest.cc */, 273613F71F1696E700ECB9DF /* ReplicatorLoopbackTest.hh */, 2745DE4B1E735B9000F02CA0 /* ReplicatorAPITest.cc */, 273613FB1F16976300ECB9DF /* ReplicatorAPITest.hh */, @@ -2804,6 +2819,7 @@ 27CCC7D71E52613C00CE1989 /* Replicator.hh */, 275E98FF238360B200EA516B /* Checkpointer.cc */, 275E9904238360B200EA516B /* Checkpointer.hh */, + 2752C6B127BC417E001C1B76 /* ConnectedClient */, 275E4CD22241C701006C5B71 /* Pull */, 275E4CD32241C726006C5B71 /* Push */, 277B9567224597E1005B7E79 /* Support */, @@ -3929,6 +3945,7 @@ 2708FE5B1CF4D3370022F721 /* LiteCoreTest.cc in Sources */, 272850ED1E9D4C79009CA22F /* c4Test.cc in Sources */, 275FF6D31E494860005F90DD /* c4BaseTest.cc in Sources */, + 2752C7A727BF18F2001C1B76 /* ConnectedClientTest.cc in Sources */, 27431BC7258A8AB0009E3EC5 /* QuietReporter.hh in Sources */, 270C6B981EBA3AD200E73415 /* LogEncoderTest.cc in Sources */, 275067DC230B6AD500FA23B2 /* c4Listener.cc in Sources */, @@ -4236,6 +4253,7 @@ 275E9905238360B200EA516B /* Checkpointer.cc in Sources */, 275B35A5234E753800FE9CF0 /* Housekeeper.cc in Sources */, 271AB0162374AD09007B0319 /* IndexSpec.cc in Sources */, + 2752C6B727BC41DC001C1B76 /* ConnectedClient.cc in Sources */, 27FA568424AD0E9300B2F1F8 /* Pusher+Attachments.cc in Sources */, 93CD01101E933BE100AFB3FA /* Checkpoint.cc in Sources */, 27D74A6F1D4D3DF500D806E0 /* SQLiteDataFile.cc in Sources */, From 0162eb69be3f539efc99994e228a67ef4453f90d Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 18 Feb 2022 12:51:08 -0800 Subject: [PATCH 09/78] Thread-safety for QuietReporter Make it better behaved if a bg thread is dumping logs for an exception while the main thread finishes a test case. --- LiteCore/Support/QuietReporter.hh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/LiteCore/Support/QuietReporter.hh b/LiteCore/Support/QuietReporter.hh index d998f6555..2f1af77f1 100644 --- a/LiteCore/Support/QuietReporter.hh +++ b/LiteCore/Support/QuietReporter.hh @@ -27,6 +27,7 @@ #include "CaseListReporter.hh" #include #include +#include #ifdef _MSC_VER #include @@ -67,6 +68,7 @@ struct QuietReporter: public CaseListReporter { //---- Catch overrides virtual void testCaseStarting( Catch::TestCaseInfo const& testInfo ) override { + std::lock_guard lock(_mutex); c4log_setCallbackLevel(kC4LogWarning); c4log_warnOnErrors(true); _caseStartTime = litecore::LogIterator::now(); @@ -74,7 +76,16 @@ struct QuietReporter: public CaseListReporter { } + virtual void testCaseEnded( Catch::TestCaseStats const& testCaseStats ) override { + std::lock_guard lock(_mutex); + // Locking/unlocking the mutex merely so we block if a background thread is in + // dumpBinaryLogs due to an exception... + CaseListReporter::testCaseEnded(testCaseStats); + } + + virtual void sectionStarting( Catch::SectionInfo const& sectionInfo ) override { + std::lock_guard lock(_mutex); _caseStartTime = litecore::LogIterator::now(); CaseListReporter::sectionStarting(sectionInfo); } @@ -89,6 +100,7 @@ struct QuietReporter: public CaseListReporter { private: void dumpBinaryLogs() { + std::lock_guard lock(_mutex); fleece::alloc_slice logPath = c4log_binaryFilePath(); if (logPath && c4log_binaryFileLevel() < c4log_callbackLevel()) { c4log_flushLogFiles(); @@ -111,6 +123,7 @@ private: static inline QuietReporter* sInstance {nullptr}; + std::mutex _mutex; litecore::LogIterator::Timestamp _caseStartTime; }; From e2ac7656b3ceb466555a2ea46817f1df9e3a3aaf Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 18 Feb 2022 12:52:31 -0800 Subject: [PATCH 10/78] Xcode: Disable sanitizers for LogEncoder.cc and Logging.cc This greatly speeds up log calls in debug builds, making it more likely to catch Heisenbugs with logging on. --- Xcode/sanitizer-ignore-list.txt | 6 ++++++ Xcode/xcconfigs/Project_Debug.xcconfig | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 Xcode/sanitizer-ignore-list.txt diff --git a/Xcode/sanitizer-ignore-list.txt b/Xcode/sanitizer-ignore-list.txt new file mode 100644 index 000000000..84b7ec629 --- /dev/null +++ b/Xcode/sanitizer-ignore-list.txt @@ -0,0 +1,6 @@ +# Source files and/or functions to be ignored by the Clang sanitizers, to speed them up in debug +# builds or because they raise false alarms. +# See: + +src:*/LogEncoder.cc +src:*/Logging.cc diff --git a/Xcode/xcconfigs/Project_Debug.xcconfig b/Xcode/xcconfigs/Project_Debug.xcconfig index a85b5424e..2fe570a9b 100644 --- a/Xcode/xcconfigs/Project_Debug.xcconfig +++ b/Xcode/xcconfigs/Project_Debug.xcconfig @@ -13,3 +13,5 @@ GCC_OPTIMIZATION_LEVEL = 0 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) DEBUG=1 _LIBCPP_DEBUG=0 ENABLE_TESTABILITY = YES MTL_ENABLE_DEBUG_INFO = YES + +OTHER_CFLAGS = $(inherited) -fsanitize-ignorelist=$(SRCROOT)/sanitizer-ignore-list.txt From 1aef69cf0a339be92f53312ef7fcfa20697b7c75 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 18 Feb 2022 12:54:46 -0800 Subject: [PATCH 11/78] LoopbackProvider: Disabled a troublesome assertion If both sockets in a pair are closed at about the same time, it's possible one will handle the close() just after its peer socket has told it it's closed. That triggered an assertion failure. I don't think that's a valid failure. --- Networking/BLIP/LoopbackProvider.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Networking/BLIP/LoopbackProvider.hh b/Networking/BLIP/LoopbackProvider.hh index a132a9281..bfcd8b3ce 100644 --- a/Networking/BLIP/LoopbackProvider.hh +++ b/Networking/BLIP/LoopbackProvider.hh @@ -245,7 +245,7 @@ namespace litecore { namespace websocket { } virtual void _close(int status, fleece::alloc_slice message) { - if (_state != State::unconnected) { + if (_state != State::unconnected && _state != State::closed) { Assert(_state == State::connecting || _state == State::connected); logInfo("CLOSE; status=%d", status); std::string messageStr(message); From beaf25448a6fdc67c90cf3d0646367182f2987a3 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 18 Feb 2022 12:55:11 -0800 Subject: [PATCH 12/78] More ConnectedClient work --- Replicator/ConnectedClient/ConnectedClient.cc | 20 +++-- Replicator/ConnectedClient/ConnectedClient.hh | 16 +++- Replicator/Pusher+Revs.cc | 44 +++++----- Replicator/Pusher.hh | 2 +- Replicator/tests/ConnectedClientTest.cc | 86 ++++++++++++++++--- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 +- 6 files changed, 124 insertions(+), 46 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 69e8d8e25..72c75fcf5 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -177,7 +177,8 @@ namespace litecore::client { Async ConnectedClient::getDoc(alloc_slice docID, alloc_slice collectionID, - alloc_slice unlessRevID) + alloc_slice unlessRevID, + bool asFleece) { BEGIN_ASYNC_RETURNING(DocResponseOrError) logInfo("getDoc(\"%.*s\")", FMTSLICE(docID)); @@ -191,17 +192,20 @@ namespace litecore::client { if (C4Error err = responseError(response)) return err; - FLError flErr; - alloc_slice fleeceBody(FLData_ConvertJSON(response->body(), &flErr)); - if (!fleeceBody) - return C4Error::make(FleeceDomain, flErr); - - return DocResponse { + DocResponse docResponse { docID, alloc_slice(response->property("rev")), - fleeceBody, + response->body(), response->boolProperty("deleted") }; + + if (asFleece) { + FLError flErr; + docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); + if (!docResponse.body) + return C4Error::make(FleeceDomain, flErr); + } + return docResponse; END_ASYNC() } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 91e7ea5a6..2e23777aa 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -13,16 +13,23 @@ namespace litecore::client { + /** Result of a successful `ConnectedClient::getDoc()` call. */ struct DocResponse { alloc_slice docID, revID, body; bool deleted; }; + /** Result type of `ConnectedClient::getDoc()` -- either a response or an error. */ using DocResponseOrError = std::variant; + /** Result type of `ConnectedClient::getBlob()` -- either blob contents or an error. */ using BlobOrError = std::variant; + + /** A live connection to Sync Gateway (or a CBL peer) that can do interactive CRUD operations. + No C4Database necessary! + Its API is somewhat similar to `Replicator`. */ class ConnectedClient : public repl::Worker, private blip::ConnectionDelegate { @@ -35,6 +42,7 @@ namespace litecore::client { Delegate&, fleece::AllocedDict options); + /** ConnectedClient Delegate API. Almost identical to `Replicator::Delegate` */ class Delegate { public: virtual void clientGotHTTPResponse(ConnectedClient* NONNULL, @@ -52,6 +60,8 @@ namespace litecore::client { void start(); void stop(); + //---- CRUD! + /// Gets the current revision of a document from the server. /// You can set the `unlessRevID` parameter to avoid getting a redundant copy of a /// revision you already have. @@ -59,11 +69,13 @@ namespace litecore::client { /// @param collectionID The name of the document's collection, or `nullslice` for default. /// @param unlessRevID If non-null, and equal to the current server-side revision ID, /// the server will return error {WebSocketDomain, 304}. + /// @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. /// @return An async value that, when resolved, contains either a `DocResponse` struct /// or a C4Error. actor::Async getDoc(alloc_slice docID, alloc_slice collectionID, - alloc_slice unlessRevID); + alloc_slice unlessRevID, + bool asFleece = true); /// Gets the contents of a blob given its digest. /// @param docID The ID of the document referencing this blob. @@ -76,7 +88,7 @@ namespace litecore::client { C4BlobKey blobKey, bool compress); - /// Pushes a document revision to the server. + /// Pushes a new document revision to the server. /// @param docID The document ID. /// @param collectionID The name of the document's collection, or `nullslice` for default. /// @param revID The revision ID you're sending. diff --git a/Replicator/Pusher+Revs.cc b/Replicator/Pusher+Revs.cc index 8fae945d4..281894665 100644 --- a/Replicator/Pusher+Revs.cc +++ b/Replicator/Pusher+Revs.cc @@ -46,9 +46,10 @@ namespace litecore::repl { // Creates a revision message from a RevToSend. Returns a BLIP error code. - int Pusher::buildRevisionMessage(RevToSend *request, + bool Pusher::buildRevisionMessage(RevToSend *request, MessageBuilder &msg, - slice ifNotRevID) + slice ifNotRevID, + C4Error *outError) { // Get the document & revision: C4Error c4err = {}; @@ -137,19 +138,11 @@ namespace litecore::repl { } logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", SPLAT(request->docID), SPLAT(request->revID)); - return 0; + return true; } else { - if (c4err.domain == WebSocketDomain) - return c4err.code; - else if (c4err.domain == LiteCoreDomain && c4err.code == kC4ErrorNotFound) - return 404; - else { - warn("sendRevision: Couldn't get rev '%.*s' %.*s from db: %s", - SPLAT(request->docID), SPLAT(request->revID), - c4err.description().c_str()); - return 500; - } + if (outError) *outError = c4err; + return false; } } @@ -164,8 +157,8 @@ namespace litecore::repl { _revisionsInFlight, tuning::kMaxRevsInFlight); MessageBuilder msg; - int blipError = buildRevisionMessage(request, msg); - if (blipError == 0) { + C4Error c4err; + if (buildRevisionMessage(request, msg, {}, &c4err)) { msg["Profile"] = "rev"; logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", SPLAT(request->docID), SPLAT(request->revID)); @@ -174,6 +167,17 @@ namespace litecore::repl { }); increment(_revisionsInFlight); } else { + int blipError; + if (c4err.domain == WebSocketDomain) + blipError = c4err.code; + else if (c4err.domain == LiteCoreDomain && c4err.code == kC4ErrorNotFound) + blipError = 404; + else { + warn("sendRevision: Couldn't get rev '%.*s' %.*s from db: %s", + SPLAT(request->docID), SPLAT(request->revID), + c4err.description().c_str()); + blipError = 500; + } msg["Profile"] = "norev"; msg["error"_sl] = blipError; msg.noreply = true; @@ -391,15 +395,15 @@ namespace litecore::repl { info.docID = docID; auto rev = make_retained(info); MessageBuilder response(req); - int blipError = buildRevisionMessage(rev, response, ifNotRev); - if (blipError == 0) { + C4Error c4err; + if (buildRevisionMessage(rev, response, ifNotRev, &c4err)) { logVerbose("Responding to getRev('%.*s') with rev #%.*s", SPLAT(docID), SPLAT(rev->revID)); req->respond(response); } else { - logVerbose("Responding to getRev('%.*s') with BLIP err %d", - SPLAT(docID), blipError); - req->respondWithError({"BLIP", blipError}); + logInfo("Responding to getRev('%.*s') with error %s", + SPLAT(docID), c4err.description().c_str()); + req->respondWithError(c4ToBLIPError(c4err)); } } diff --git a/Replicator/Pusher.hh b/Replicator/Pusher.hh index c95e6c483..33cc25685 100644 --- a/Replicator/Pusher.hh +++ b/Replicator/Pusher.hh @@ -83,7 +83,7 @@ namespace litecore { namespace repl { // Pusher+Revs.cc: void maybeSendMoreRevs(); void retryRevs(RevToSendList, bool immediate); - int buildRevisionMessage(RevToSend*, blip::MessageBuilder&, slice ifNotRevID = {}); + bool buildRevisionMessage(RevToSend*, blip::MessageBuilder&, slice ifNotRevID, C4Error*); void sendRevision(Retained); void onRevProgress(Retained rev, const blip::MessageProgress&); void couldntSendRevision(RevToSend* NONNULL); diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 0e2376587..7bede8471 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -36,6 +36,9 @@ class ConnectedClientLoopbackTest : public C4Test, public: void start() { + std::unique_lock lock(_mutex); + Assert(!_serverRunning && !_clientRunning); + auto serverOpts = make_retained(kC4Passive,kC4Passive); _server = new repl::Replicator(db, new LoopbackWebSocket(alloc_slice("ws://srv/"), @@ -46,16 +49,19 @@ class ConnectedClientLoopbackTest : public C4Test, Role::Client, {}), *this, clientOpts); + Headers headers; headers.add("Set-Cookie"_sl, "flavor=chocolate-chip"_sl); LoopbackWebSocket::bind(_server->webSocket(), _client->webSocket(), headers); + _clientRunning = _serverRunning = true; _server->start(); _client->start(); } void stop() { + std::unique_lock lock(_mutex); if (_server) { _server->stop(); _server = nullptr; @@ -64,6 +70,34 @@ class ConnectedClientLoopbackTest : public C4Test, _client->stop(); _client = nullptr; } + + Log("Waiting for client & replicator to stop..."); + _cond.wait(lock, [&]{return !_clientRunning && !_serverRunning;}); + } + + + client::DocResponse waitForResponse(actor::Async &asyncResult) { + asyncResult.blockUntilReady(); + + C4Log("++++ Async response available!"); + auto &result = asyncResult.result(); + if (auto err = std::get_if(&result)) + FAIL("Response returned an error " << *err); + auto rev = std::get_if(&result); + REQUIRE(rev); + return *rev; + } + + + C4Error waitForErrorResponse(actor::Async &asyncResult) { + asyncResult.blockUntilReady(); + + C4Log("++++ Async response available!"); + auto &result = asyncResult.result(); + auto err = std::get_if(&result); + if (!err) + FAIL("Response unexpectedly didn't fail"); + return *err; } @@ -86,6 +120,12 @@ class ConnectedClientLoopbackTest : public C4Test, void clientStatusChanged(client::ConnectedClient* NONNULL, C4ReplicatorActivityLevel level) override { C4Log("Client status changed: %d", int(level)); + if (level == kC4Stopped) { + std::unique_lock lock(_mutex); + _clientRunning = false; + if (!_clientRunning && !_serverRunning) + _cond.notify_all(); + } } void clientConnectionClosed(client::ConnectedClient* NONNULL, const CloseStatus &close) override { @@ -99,7 +139,14 @@ class ConnectedClientLoopbackTest : public C4Test, const websocket::Headers &headers) override { } void replicatorGotTLSCertificate(slice certData) override { } void replicatorStatusChanged(repl::Replicator* NONNULL, - const repl::Replicator::Status&) override { } + const repl::Replicator::Status &status) override { + if (status.level == kC4Stopped) { + std::unique_lock lock(_mutex); + _serverRunning = false; + if (!_clientRunning && !_serverRunning) + _cond.notify_all(); + } + } void replicatorConnectionClosed(repl::Replicator* NONNULL, const CloseStatus&) override { } void replicatorDocumentsEnded(repl::Replicator* NONNULL, @@ -110,6 +157,9 @@ class ConnectedClientLoopbackTest : public C4Test, Retained _server; Retained _client; + bool _clientRunning = false, _serverRunning = false; + mutex _mutex; + condition_variable _cond; }; @@ -118,21 +168,29 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { start(); C4Log("++++ Calling ConnectedClient::getDoc()..."); - auto asyncResult = _client->getDoc(alloc_slice("0000001"), nullslice, nullslice); - asyncResult.blockUntilReady(); - - C4Log("++++ Async value available!"); - auto &result = asyncResult.result(); - auto rev = std::get_if(&result); - REQUIRE(rev); - CHECK(rev->docID == "0000001"); - CHECK(rev->revID == "1-4cbe54d79c405e368613186b0bc7ac9ee4a50fbb"); - CHECK(rev->deleted == false); - Doc doc(rev->body); + auto asyncResult1 = _client->getDoc(alloc_slice("0000001"), nullslice, nullslice); + auto asyncResult99 = _client->getDoc(alloc_slice("0000099"), nullslice, nullslice); + + auto rev = waitForResponse(asyncResult1); + CHECK(rev.docID == "0000001"); + CHECK(rev.revID == "1-4cbe54d79c405e368613186b0bc7ac9ee4a50fbb"); + CHECK(rev.deleted == false); + Doc doc(rev.body); CHECK(doc.asDict()["birthday"].asString() == "1983-09-18"); + rev = waitForResponse(asyncResult99); + CHECK(rev.docID == "0000099"); + CHECK(rev.revID == "1-94baf6e4e4a1442aa6d8e9aab87955b8b7f4817a"); + CHECK(rev.deleted == false); + doc = Doc(rev.body); + CHECK(doc.asDict()["birthday"].asString() == "1958-12-20"); +} + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev NotFound", "[ConnectedClient]") { + start(); + auto asyncResultX = _client->getDoc(alloc_slice("bogus"), nullslice, nullslice); + CHECK(waitForErrorResponse(asyncResultX) == C4Error{LiteCoreDomain, kC4ErrorNotFound}); C4Log("++++ Stopping..."); stop(); - _server = nullptr; - _client = nullptr; } diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 6f8a7c7e1..bda8d6dc2 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -2819,9 +2819,9 @@ 27CCC7D71E52613C00CE1989 /* Replicator.hh */, 275E98FF238360B200EA516B /* Checkpointer.cc */, 275E9904238360B200EA516B /* Checkpointer.hh */, - 2752C6B127BC417E001C1B76 /* ConnectedClient */, 275E4CD22241C701006C5B71 /* Pull */, 275E4CD32241C726006C5B71 /* Push */, + 2752C6B127BC417E001C1B76 /* ConnectedClient */, 277B9567224597E1005B7E79 /* Support */, 277B956122459796005B7E79 /* API implementation */, 275CE0FC1E5B78570084E014 /* tests */, From 660aea3311046d24d721e9d3aa8f60a930329547 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 21 Feb 2022 17:43:17 -0800 Subject: [PATCH 13/78] ConnectedClient: More tests and some fixes --- Replicator/ConnectedClient/ConnectedClient.cc | 15 +-- Replicator/ConnectedClient/ConnectedClient.hh | 6 +- Replicator/Puller.cc | 10 ++ Replicator/Puller.hh | 1 + Replicator/Pusher+Attachments.cc | 36 ++++-- Replicator/Pusher+Revs.cc | 14 +- Replicator/ReplicatorTypes.cc | 4 +- Replicator/tests/ConnectedClientTest.cc | 122 ++++++++++++++++-- 8 files changed, 165 insertions(+), 43 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 72c75fcf5..272b4551b 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -199,7 +199,7 @@ namespace litecore::client { response->boolProperty("deleted") }; - if (asFleece) { + if (asFleece && docResponse.body) { FLError flErr; docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); if (!docResponse.body) @@ -210,16 +210,14 @@ namespace litecore::client { } - Async ConnectedClient::getBlob(alloc_slice docID, - alloc_slice collectionID, - C4BlobKey blobKey, - bool compress) { + Async ConnectedClient::getBlob(C4BlobKey blobKey, + bool compress) + { BEGIN_ASYNC_RETURNING(BlobOrError) auto digest = blobKey.digestString(); - logInfo("getAttachment(\"%.*s\", <%s>)", FMTSLICE(docID), digest.c_str()); + logInfo("getAttachment(<%s>)", digest.c_str()); MessageBuilder req("getAttachment"); req["digest"] = digest; - req["docID"] = docID; if (compress) req["compress"] = "true"; @@ -242,7 +240,7 @@ namespace litecore::client { { BEGIN_ASYNC_RETURNING(C4Error) logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID), FMTSLICE(revID)); - MessageBuilder req("rev"); + MessageBuilder req("putDoc"); req.compressed = true; req["id"] = docID; req["rev"] = revID; @@ -251,6 +249,7 @@ namespace litecore::client { req["deleted"] = "1"; if (fleeceData.size > 0) { + // TODO: Encryption!! // TODO: Convert blobs to legacy attachments req.jsonBody().writeValue(Doc(fleeceData, kFLTrusted).asDict()); } else { diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 2e23777aa..332e5e9d2 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -78,14 +78,10 @@ namespace litecore::client { bool asFleece = true); /// Gets the contents of a blob given its digest. - /// @param docID The ID of the document referencing this blob. - /// @param collectionID The name of the document's collection, or `nullslice` for default. /// @param blobKey The binary digest of the blob. /// @param compress True if the blob should be downloaded in compressed form. /// @return An async value that, when resolved, contains either the blob body or a C4Error. - actor::Async getBlob(alloc_slice docID, - alloc_slice collectionID, - C4BlobKey blobKey, + actor::Async getBlob(C4BlobKey blobKey, bool compress); /// Pushes a new document revision to the server. diff --git a/Replicator/Puller.cc b/Replicator/Puller.cc index 38ccd84c3..27c936cae 100644 --- a/Replicator/Puller.cc +++ b/Replicator/Puller.cc @@ -50,6 +50,8 @@ namespace litecore { namespace repl { _passive = _options->pull <= kC4Passive; registerHandler("rev", &Puller::handleRev); registerHandler("norev", &Puller::handleNoRev); + if (passive()) + registerHandler("putDoc", &Puller::handlePutDoc); _spareIncomingRevs.reserve(tuning::kMaxActiveIncomingRevs); _skipDeleted = _options->skipDeleted(); if (!passive() && _options->noIncomingConflicts()) @@ -191,6 +193,14 @@ namespace litecore { namespace repl { } + // Received a "putDoc" message from a connected client (not part of replication) + void Puller::handlePutDoc(Retained msg) { + Retained inc = makeIncomingRev(); + if (inc) + inc->handleRev(msg, msg->body().size); // ... will call _revWasHandled when it's finished + } + + // Actually process an incoming "rev" now: void Puller::startIncomingRev(MessageIn *msg) { _revFinder->revReceived(); diff --git a/Replicator/Puller.hh b/Replicator/Puller.hh index 82857aab2..f67704ee8 100644 --- a/Replicator/Puller.hh +++ b/Replicator/Puller.hh @@ -58,6 +58,7 @@ namespace litecore { namespace repl { void _documentsRevoked(std::vector>); void handleRev(Retained); void handleNoRev(Retained); + void handlePutDoc(Retained); Retained makeIncomingRev(); void startIncomingRev(blip::MessageIn* NONNULL); void maybeStartIncomingRevs(); diff --git a/Replicator/Pusher+Attachments.cc b/Replicator/Pusher+Attachments.cc index 9510458e2..d39f445f8 100644 --- a/Replicator/Pusher+Attachments.cc +++ b/Replicator/Pusher+Attachments.cc @@ -78,23 +78,33 @@ namespace litecore::repl { slice &digestStr, Replicator::BlobProgress &progress) { + C4Error error = {}; try { - digestStr = req->property("digest"_sl); - progress = {Dir::kPushing}; - if (auto key = C4BlobKey::withDigestString(digestStr); key) - progress.key = *key; - else - C4Error::raise(LiteCoreDomain, kC4ErrorInvalidParameter, "Missing or invalid 'digest'"); auto blobStore = _db->blobStore(); - if (int64_t size = blobStore->getSize(progress.key); size >= 0) - progress.bytesTotal = size; - else - C4Error::raise(LiteCoreDomain, kC4ErrorNotFound, "No such blob"); - return make_unique(*blobStore, progress.key); + do { + digestStr = req->property("digest"_sl); + progress = {Dir::kPushing}; + if (auto key = C4BlobKey::withDigestString(digestStr); key) + progress.key = *key; + else { + error = C4Error::make(LiteCoreDomain, kC4ErrorInvalidParameter, + "Missing or invalid 'digest'"); + break; + } + if (int64_t size = blobStore->getSize(progress.key); size >= 0) + progress.bytesTotal = size; + else { + error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound, "No such blob"); + break; + } + } while (false); + if (!error) + return make_unique(*blobStore, progress.key); } catch (...) { - req->respondWithError(c4ToBLIPError(C4Error::fromCurrentException())); - return nullptr; + error = C4Error::fromCurrentException(); } + req->respondWithError(c4ToBLIPError(error)); + return nullptr; } diff --git a/Replicator/Pusher+Revs.cc b/Replicator/Pusher+Revs.cc index 281894665..a7b745554 100644 --- a/Replicator/Pusher+Revs.cc +++ b/Replicator/Pusher+Revs.cc @@ -57,9 +57,13 @@ namespace litecore::repl { Retained doc = _db->getDoc(request->docID, kDocGetAll); if (doc) { if (request->revID.empty()) { - // When called from `handleGetRev`, all the request has is the docID - if (doc->revID() == ifNotRevID) - return 304; // NotChanged + // When called from `handleGetRev`, all the request has is the docID. + // First check for a conditional get: + if (doc->revID() == ifNotRevID) { + c4error_return(WebSocketDomain, 304, "Not Changed"_sl, outError); + return false; + } + // Populate the request with the revision metadata: root = doc->getProperties(); request->setRevID(doc->revID()); request->sequence = doc->sequence(); @@ -93,9 +97,7 @@ namespace litecore::repl { auto fullRevID = alloc_slice(_db->convertVersionToAbsolute(request->revID)); - // Now send the BLIP message. Normally it's "rev", but if this is an error we make it - // "norev" and include the error code: - //MessageBuilder msg(root ? "rev"_sl : "norev"_sl); + // Now populate the BLIP message fields, whether or not this is an error msg.compressed = true; msg["id"_sl] = request->docID; msg["rev"_sl] = fullRevID; diff --git a/Replicator/ReplicatorTypes.cc b/Replicator/ReplicatorTypes.cc index a2c83555f..039fd597e 100644 --- a/Replicator/ReplicatorTypes.cc +++ b/Replicator/ReplicatorTypes.cc @@ -39,8 +39,8 @@ namespace litecore { namespace repl { void RevToSend::setRevID(slice id) { - Assert(!revID); - ((alloc_slice&)revID) = id; + Assert(revID.empty()); + const_cast(revID) = id; } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 7bede8471..7b4f317b7 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -40,6 +40,7 @@ class ConnectedClientLoopbackTest : public C4Test, Assert(!_serverRunning && !_clientRunning); auto serverOpts = make_retained(kC4Passive,kC4Passive); + serverOpts->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); _server = new repl::Replicator(db, new LoopbackWebSocket(alloc_slice("ws://srv/"), Role::Server, {}), @@ -76,26 +77,26 @@ class ConnectedClientLoopbackTest : public C4Test, } - client::DocResponse waitForResponse(actor::Async &asyncResult) { + template + T waitForResponse(actor::Async> &asyncResult) { asyncResult.blockUntilReady(); C4Log("++++ Async response available!"); auto &result = asyncResult.result(); if (auto err = std::get_if(&result)) FAIL("Response returned an error " << *err); - auto rev = std::get_if(&result); - REQUIRE(rev); - return *rev; + return * std::get_if(&result); } - C4Error waitForErrorResponse(actor::Async &asyncResult) { + template + C4Error waitForErrorResponse(actor::Async> &asyncResult) { asyncResult.blockUntilReady(); C4Log("++++ Async response available!"); auto &result = asyncResult.result(); - auto err = std::get_if(&result); - if (!err) + const C4Error *err = std::get_if(&result); + if (!*err) FAIL("Response unexpectedly didn't fail"); return *err; } @@ -163,6 +164,9 @@ class ConnectedClientLoopbackTest : public C4Test, }; +#pragma mark - TESTS: + + TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { importJSONLines(sFixturesDir + "names_100.json"); start(); @@ -187,10 +191,110 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { } +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev Conditional Match", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + start(); + + auto match = _client->getDoc(alloc_slice("0000002"), nullslice, + alloc_slice("1-1fdf9d4bdae09f6651938d9ec1d47177280f5a77")); + CHECK(waitForErrorResponse(match) == C4Error{WebSocketDomain, 304}); +} + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev Conditional No Match", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + start(); + + auto match = _client->getDoc(alloc_slice("0000002"), nullslice, + alloc_slice("1-beefbeefbeefbeefbeefbeefbeefbeefbeefbeef")); + auto rev = waitForResponse(match); + CHECK(rev.docID == "0000002"); + CHECK(rev.revID == "1-1fdf9d4bdae09f6651938d9ec1d47177280f5a77"); + CHECK(rev.deleted == false); + auto doc = Doc(rev.body); + CHECK(doc.asDict()["birthday"].asString() == "1989-04-29"); +} + + TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev NotFound", "[ConnectedClient]") { start(); auto asyncResultX = _client->getDoc(alloc_slice("bogus"), nullslice, nullslice); CHECK(waitForErrorResponse(asyncResultX) == C4Error{LiteCoreDomain, kC4ErrorNotFound}); - C4Log("++++ Stopping..."); - stop(); +} + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getBlob", "[ConnectedClient]") { + vector attachments = {"Hey, this is an attachment!", "So is this", ""}; + vector blobKeys; + { + TransactionHelper t(db); + blobKeys = addDocWithAttachments("att1"_sl, attachments, "text/plain"); + } + start(); + + auto asyncBlob1 = _client->getBlob(blobKeys[0], true); + auto asyncBlob2 = _client->getBlob(blobKeys[1], false); + auto asyncBadBlob = _client->getBlob(C4BlobKey{}, false); + + alloc_slice blob1 = waitForResponse(asyncBlob1); + CHECK(blob1 == slice(attachments[0])); + + alloc_slice blob2 = waitForResponse(asyncBlob2); + CHECK(blob2 == slice(attachments[1])); + + CHECK(waitForErrorResponse(asyncBadBlob) == C4Error{LiteCoreDomain, kC4ErrorNotFound}); +} + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + start(); + + Encoder enc; + enc.beginDict(); + enc["connected"] = "client"; + enc.endDict(); + auto docBody = enc.finish(); + + auto rq1 = _client->putDoc(alloc_slice("0000001"), nullslice, + alloc_slice("2-2222"), + alloc_slice("1-4cbe54d79c405e368613186b0bc7ac9ee4a50fbb"), + C4RevisionFlags{}, + docBody); + auto rq2 = _client->putDoc(alloc_slice("frob"), nullslice, + alloc_slice("1-1111"), + nullslice, + C4RevisionFlags{}, + docBody); + rq1.blockUntilReady(); + REQUIRE(rq1.result() == C4Error()); + c4::ref doc1 = c4db_getDoc(db, "0000001"_sl, true, kDocGetCurrentRev, ERROR_INFO()); + REQUIRE(doc1); + CHECK(doc1->revID == "2-2222"_sl); + + rq2.blockUntilReady(); + REQUIRE(rq2.result() == C4Error()); + c4::ref doc2 = c4db_getDoc(db, "frob"_sl, true, kDocGetCurrentRev, ERROR_INFO()); + REQUIRE(doc2); + CHECK(doc2->revID == "1-1111"_sl); +} + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Failure", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + start(); + + Encoder enc; + enc.beginDict(); + enc["connected"] = "client"; + enc.endDict(); + auto docBody = enc.finish(); + + auto rq1 = _client->putDoc(alloc_slice("0000001"), nullslice, + alloc_slice("2-2222"), + alloc_slice("1-d00d"), + C4RevisionFlags{}, + docBody); + rq1.blockUntilReady(); + REQUIRE(rq1.result() == C4Error{LiteCoreDomain, kC4ErrorConflict}); } From 75eba0316db3c76797ded5509de2700b86c19e36 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 10:03:55 -0800 Subject: [PATCH 14/78] Added kC4ReplicatorOptionAllowConnectedClient The replicator now only responds to connected-client messages "getRev", "putRev" if this option is set. (Also renamed "putDoc" to "putRev" for consistency.) --- C/include/c4ReplicatorTypes.h | 1 + Replicator/ConnectedClient/ConnectedClient.cc | 2 +- Replicator/Puller.cc | 9 +++++---- Replicator/Puller.hh | 2 +- Replicator/Pusher.cc | 3 ++- Replicator/tests/ConnectedClientTest.cc | 3 ++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/C/include/c4ReplicatorTypes.h b/C/include/c4ReplicatorTypes.h index ccfd64925..4311241fe 100644 --- a/C/include/c4ReplicatorTypes.h +++ b/C/include/c4ReplicatorTypes.h @@ -218,6 +218,7 @@ C4API_BEGIN_DECLS #define kC4ReplicatorOptionMaxRetries "maxRetries" ///< Max number of retry attempts (int) #define kC4ReplicatorOptionMaxRetryInterval "maxRetryInterval" ///< Max delay betw retries (secs) #define kC4ReplicatorOptionAutoPurge "autoPurge" ///< Enables auto purge; default is true (bool) + #define kC4ReplicatorOptionAllowConnectedClient "allowConnectedClient" ///< Allow peer to use connected-client (CRUD) API // TLS options: #define kC4ReplicatorOptionRootCerts "rootCerts" ///< Trusted root certs (data) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 272b4551b..0b9d90fbc 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -240,7 +240,7 @@ namespace litecore::client { { BEGIN_ASYNC_RETURNING(C4Error) logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID), FMTSLICE(revID)); - MessageBuilder req("putDoc"); + MessageBuilder req("putRev"); req.compressed = true; req["id"] = docID; req["rev"] = revID; diff --git a/Replicator/Puller.cc b/Replicator/Puller.cc index 27c936cae..07b352633 100644 --- a/Replicator/Puller.cc +++ b/Replicator/Puller.cc @@ -50,12 +50,13 @@ namespace litecore { namespace repl { _passive = _options->pull <= kC4Passive; registerHandler("rev", &Puller::handleRev); registerHandler("norev", &Puller::handleNoRev); - if (passive()) - registerHandler("putDoc", &Puller::handlePutDoc); _spareIncomingRevs.reserve(tuning::kMaxActiveIncomingRevs); _skipDeleted = _options->skipDeleted(); if (!passive() && _options->noIncomingConflicts()) warn("noIncomingConflicts mode is not compatible with active pull replications!"); + + if (_options->properties[kC4ReplicatorOptionAllowConnectedClient]) + registerHandler("putRev", &Puller::handlePutRev); } @@ -193,8 +194,8 @@ namespace litecore { namespace repl { } - // Received a "putDoc" message from a connected client (not part of replication) - void Puller::handlePutDoc(Retained msg) { + // Received a "putRev" message from a connected client (not part of replication) + void Puller::handlePutRev(Retained msg) { Retained inc = makeIncomingRev(); if (inc) inc->handleRev(msg, msg->body().size); // ... will call _revWasHandled when it's finished diff --git a/Replicator/Puller.hh b/Replicator/Puller.hh index f67704ee8..134fc368e 100644 --- a/Replicator/Puller.hh +++ b/Replicator/Puller.hh @@ -58,7 +58,7 @@ namespace litecore { namespace repl { void _documentsRevoked(std::vector>); void handleRev(Retained); void handleNoRev(Retained); - void handlePutDoc(Retained); + void handlePutRev(Retained); Retained makeIncomingRev(); void startIncomingRev(blip::MessageIn* NONNULL); void maybeStartIncomingRevs(); diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index ad5520f94..e3c0acbbe 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -51,7 +51,8 @@ namespace litecore { namespace repl { registerHandler("subChanges", &Pusher::handleSubChanges); registerHandler("getAttachment", &Pusher::handleGetAttachment); registerHandler("proveAttachment", &Pusher::handleProveAttachment); - if (_passive) + + if (_options->properties[kC4ReplicatorOptionAllowConnectedClient]) registerHandler("getRev", &Pusher::handleGetRev); } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 7b4f317b7..67f25e18a 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -40,6 +40,7 @@ class ConnectedClientLoopbackTest : public C4Test, Assert(!_serverRunning && !_clientRunning); auto serverOpts = make_retained(kC4Passive,kC4Passive); + serverOpts->setProperty(kC4ReplicatorOptionAllowConnectedClient, true); serverOpts->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); _server = new repl::Replicator(db, new LoopbackWebSocket(alloc_slice("ws://srv/"), @@ -246,7 +247,7 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getBlob", "[ConnectedClient]") { } -TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc", "[ConnectedClient]") { +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putRev", "[ConnectedClient]") { importJSONLines(sFixturesDir + "names_100.json"); start(); From 5138867c523085015c68a0c2f31005982e0a380e Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 11:56:27 -0800 Subject: [PATCH 15/78] WIP: Implementing ConnectedClient observer --- Replicator/ConnectedClient/ConnectedClient.cc | 106 ++++++++++++++++++ Replicator/ConnectedClient/ConnectedClient.hh | 28 ++++- Replicator/Pusher.cc | 6 +- 3 files changed, 135 insertions(+), 5 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 0b9d90fbc..729c96ae3 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -18,9 +18,11 @@ #include "ConnectedClient.hh" #include "c4BlobStoreTypes.h" +#include "c4Document.hh" #include "c4SocketTypes.h" #include "Headers.hh" #include "MessageBuilder.hh" +#include "NumConversion.hh" #include "WebSocketInterface.hh" namespace litecore::client { @@ -263,4 +265,108 @@ namespace litecore::client { END_ASYNC() } + + Async ConnectedClient::observeCollection(alloc_slice collectionID, + CollectionObserver callback) + { + bool observe = !!callback; + BEGIN_ASYNC_RETURNING(C4Error) + logInfo("observeCollection(%.*s)", FMTSLICE(collectionID)); + + bool sameSubState = (observe == !!_observer); + _observer = move(callback); + if (sameSubState) + return {}; + + MessageBuilder req; + if (observe) { + if (!_registeredChangesHandler) { + registerHandler("changes", &ConnectedClient::handleChanges); + _registeredChangesHandler = true; + } + req.setProfile("subChanges"); + req["since"] = "NOW"; + req["continuous"] = true; + } else { + req.setProfile("unsubChanges"); + } + AWAIT(Retained, response, sendAsyncRequest(req)); + + logInfo("...observeCollection got response"); + C4Error error = responseError(response); + if (!error) + _observing = observe; + return error; + END_ASYNC() + } + + + void ConnectedClient::handleChanges(Retained req) { + // The code below is adapted from RevFinder::handleChangesNow and RevFinder::findRevs. + auto inChanges = req->JSONBody().asArray(); + if (!inChanges && req->body() != "null"_sl) { + warn("Invalid body of 'changes' message"); + req->respondWithError(400, "Invalid JSON body"_sl); + return; + } + + // "changes" expects a response with an array of which items we want "rev" messages for. + // We don't actually want any. An empty array will indicate that. + MessageBuilder response(req); + auto &enc = response.jsonBody(); + enc.beginArray(); + enc.endArray(); + req->respond(response); + + if (_observer) { + // Convert the JSON change list into a vector: + vector outChanges; + outChanges.reserve(inChanges.count()); + for (auto item : inChanges) { + // "changes" entry: [sequence, docID, revID, deleted?, bodySize?] + auto &outChange = outChanges.emplace_back(); + auto inChange = item.asArray(); + outChange.sequence = C4SequenceNumber{inChange[0].asUnsigned()}; + outChange.docID = inChange[1].asString(); + outChange.revID = inChange[2].asString(); + outChange.flags = 0; + int64_t deletion = inChange[3].asInt(); + outChange.bodySize = fleece::narrow_cast(inChange[4].asUnsigned()); + + checkDocAndRevID(outChange.docID, outChange.revID); + + // In SG 2.x "deletion" is a boolean flag, 0=normal, 1=deleted. + // SG 3.x adds 2=revoked, 3=revoked+deleted, 4=removal (from channel) + if (deletion & 0b001) + outChange.flags |= kRevDeleted; + if (deletion & 0b110) + outChange.flags |= kRevPurged; + } + + // Finally call the observer callback: + try { + _observer(outChanges); + } catch (...) { + logError("ConnectedClient observer threw exception: %s", + C4Error::fromCurrentException().description().c_str()); + } + } + } + + + void ConnectedClient::checkDocAndRevID(slice docID, slice revID) { + bool valid; + if (!C4Document::isValidDocID(docID)) + valid = false; + else if (_remoteUsesVersionVectors) + valid = revID.findByte('@') && !revID.findByte('*'); // require absolute form + else + valid = revID.findByte('-'); + if (!valid) { + C4Error::raise(LiteCoreDomain, kC4ErrorRemoteError, + "Invalid docID/revID '%.*s' #%.*s in incoming change list", + FMTSLICE(docID), FMTSLICE(revID)); + } + } + } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 332e5e9d2..ee5aa4ed6 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -8,8 +8,11 @@ #include "Worker.hh" #include "Async.hh" #include "BLIPConnection.hh" +#include "c4Observer.hh" #include "c4ReplicatorTypes.h" +#include #include +#include namespace litecore::client { @@ -26,6 +29,8 @@ namespace litecore::client { using BlobOrError = std::variant; + using CollectionObserver = std::function const&)>; + /** A live connection to Sync Gateway (or a CBL peer) that can do interactive CRUD operations. No C4Database necessary! @@ -100,6 +105,14 @@ namespace litecore::client { C4RevisionFlags revisionFlags, alloc_slice fleeceData); + /// Registers a listener function that will be called when any document is changed. + /// @note To cancel, pass a null callback. + /// @param collectionID The ID of the collection to observe. + /// @param callback The function to call (on an arbitrary background thread!) + /// @return An async value that, when resolved, contains the status as a C4Error. + actor::Async observeCollection(alloc_slice collectionID, + CollectionObserver callback); + // exposed for unit tests: websocket::WebSocket* webSocket() const {return connection().webSocket();} @@ -111,14 +124,21 @@ namespace litecore::client { void onClose(blip::Connection::CloseStatus status, blip::Connection::State state) override; void onRequestReceived(blip::MessageIn* request) override; + void handleChanges(Retained); + private: void setStatus(ActivityLevel); C4Error responseError(blip::MessageIn *response); void _disconnect(websocket::CloseCode closeCode, slice message); - - Delegate* _delegate; // Delegate whom I report progress/errors to - ActivityLevel _status; - Retained _selfRetain; + void checkDocAndRevID(slice docID, slice revID); + + Delegate* _delegate; // Delegate whom I report progress/errors to + ActivityLevel _status; + Retained _selfRetain; + CollectionObserver _observer; + bool _observing = false; + bool _registeredChangesHandler = false; + bool _remoteUsesVersionVectors = false; }; } diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index e3c0acbbe..ae35a02c1 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -83,7 +83,11 @@ namespace litecore { namespace repl { return; } - auto since = C4SequenceNumber(max(req->intProperty("since"_sl), 0l)); + C4SequenceNumber since = {}; + if (req->property("since") == "NOW") + since = _db->useLocked()->getLastSequence(); + else + since = C4SequenceNumber(max(req->intProperty("since"_sl), 0l)); _continuous = req->boolProperty("continuous"_sl); _changesFeed.setContinuous(_continuous); _changesFeed.setSkipDeletedDocs(req->boolProperty("activeOnly"_sl)); From 88ae6ddf9a064b587dab4c5035b91fa7cd0dc105 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 11:57:23 -0800 Subject: [PATCH 16/78] BLIP: Define constants for hardcoded strings like "Profile" --- Networking/BLIP/BLIPConnection.cc | 4 +- Networking/BLIP/BLIPProtocol.hh | 43 ++++++++++++++----- Networking/BLIP/Message.cc | 8 ++-- Networking/BLIP/Message.hh | 10 ++++- Networking/BLIP/MessageBuilder.cc | 11 +++-- Networking/BLIP/MessageBuilder.hh | 2 + Replicator/ConnectedClient/ConnectedClient.cc | 2 +- Replicator/Pusher+Attachments.cc | 2 +- Replicator/Pusher+Revs.cc | 4 +- Replicator/Pusher.cc | 2 +- Replicator/Replicator.cc | 4 +- Replicator/RevFinder.cc | 10 ++--- 12 files changed, 70 insertions(+), 32 deletions(-) diff --git a/Networking/BLIP/BLIPConnection.cc b/Networking/BLIP/BLIPConnection.cc index 5e3fe9de3..d9fed486e 100644 --- a/Networking/BLIP/BLIPConnection.cc +++ b/Networking/BLIP/BLIPConnection.cc @@ -595,7 +595,7 @@ namespace litecore { namespace blip { if (state == MessageIn::kOther) return; bool beginning = (state == MessageIn::kBeginning); - auto profile = request->property("Profile"_sl); + auto profile = request->profile(); if (profile) { auto i = _requestHandlers.find({profile.asString(), beginning}); if (i != _requestHandlers.end()) { @@ -610,7 +610,7 @@ namespace litecore { namespace blip { _connection->delegate().onRequestReceived(request); } catch (...) { logError("Caught exception thrown from BLIP request handler"); - request->respondWithError({"BLIP"_sl, 501, "unexpected exception"_sl}); + request->respondWithError({kBLIPErrorDomain, 501, "unexpected exception"_sl}); } } diff --git a/Networking/BLIP/BLIPProtocol.hh b/Networking/BLIP/BLIPProtocol.hh index e16bc1941..bdb6524bf 100644 --- a/Networking/BLIP/BLIPProtocol.hh +++ b/Networking/BLIP/BLIPProtocol.hh @@ -11,36 +11,59 @@ // #pragma once +#include "fleece/slice.hh" #include namespace litecore { namespace blip { // See "docs/BLIP Protocol.md" + /// The types of messages in BLIP. enum MessageType: uint8_t { - kRequestType = 0, // A message initiated by a peer - kResponseType = 1, // A response to a Request - kErrorType = 2, // A response indicating failure + kRequestType = 0, ///< A message initiated by a peer + kResponseType = 1, ///< A response to a Request + kErrorType = 2, ///< A response indicating failure kAckRequestType = 4, // Acknowledgement of data received from a Request (internal) kAckResponseType = 5, // Acknowledgement of data received from a Response (internal) }; - // Array mapping MessageType to a short mnemonic like "REQ". + /// Array mapping MessageType to a short mnemonic like "REQ", for logging purposes. extern const char* const kMessageTypeNames[8]; - + /// The flags at the start of a message frame, including the 3 bits containing the type. enum FrameFlags: uint8_t { - kTypeMask = 0x07, // These 3 bits hold a MessageType - kCompressed = 0x08, // Message payload is gzip-deflated - kUrgent = 0x10, // Message is given priority delivery - kNoReply = 0x20, // Request only: no response desired + kTypeMask = 0x07, ///< These 3 bits hold a MessageType + kCompressed = 0x08, ///< Message payload is gzip-deflated + kUrgent = 0x10, ///< Message is given priority delivery + kNoReply = 0x20, ///< Request only: no response desired kMoreComing = 0x40, // Used only in frames, not in messages }; + /// A message number. Each peer numbers messages it sends sequentially starting at 1. + /// Each peer's message numbers are independent. typedef uint64_t MessageNo; + + /// The size of a message. typedef uint64_t MessageSize; - // Implementation-imposed max encoded size of message properties (not part of protocol) + /// Implementation-imposed max encoded size of message properties (not part of protocol) constexpr uint64_t kMaxPropertiesSize = 100 * 1024; + + + static constexpr const char* kProfilePropertyStr = "Profile"; + /// The "Profile" property contains the message's type + static constexpr fleece::slice kProfileProperty(kProfilePropertyStr); + + /// Property in an error response giving a namespace for the error code. + /// If omitted the default value is `kBLIPErrorDomain`. + static constexpr fleece::slice kErrorDomainProperty = "Error-Domain"; + + /// Property in an error response giving a numeric error code. + static constexpr fleece::slice kErrorCodeProperty = "Error-Code"; + + /// The default error domain, for errors that are not app-specific. + /// By convention its error codes are based on HTTP's, i.e. 404 for "not found". + static constexpr fleece::slice kBLIPErrorDomain = "BLIP"; + } } diff --git a/Networking/BLIP/Message.cc b/Networking/BLIP/Message.cc index a2c1bf7a2..cafa5b9fa 100644 --- a/Networking/BLIP/Message.cc +++ b/Networking/BLIP/Message.cc @@ -73,7 +73,7 @@ namespace litecore { namespace blip { void Message::writeDescription(slice payload, std::ostream& out) { if (type() == kRequestType) { - const char *profile = findProperty(payload, "Profile"); + const char *profile = findProperty(payload, kProfilePropertyStr); if (profile) out << "'" << profile << "' "; } @@ -379,7 +379,7 @@ namespace litecore { namespace blip { void MessageIn::notHandled() { - respondWithError({"BLIP"_sl, 404, "no handler for message"_sl}); + respondWithError({kBLIPErrorDomain, 404, "no handler for message"_sl}); } @@ -433,8 +433,8 @@ namespace litecore { namespace blip { Error MessageIn::getError() const { if (!isError()) return Error(); - return Error(property("Error-Domain"_sl), - (int) intProperty("Error-Code"_sl), + return Error(property(kErrorDomainProperty), + (int) intProperty(kErrorCodeProperty), body()); } diff --git a/Networking/BLIP/Message.hh b/Networking/BLIP/Message.hh index d8733ee2f..7a3ee8f4d 100644 --- a/Networking/BLIP/Message.hh +++ b/Networking/BLIP/Message.hh @@ -137,6 +137,9 @@ namespace litecore { namespace blip { long intProperty(slice property, long defaultValue =0) const; bool boolProperty(slice property, bool defaultValue =false) const; + /** The "Profile" property gives the message type. */ + slice profile() const {return property(kProfileProperty);} + /** Returns information about an error (if this message is an error.) */ Error getError() const; @@ -162,9 +165,14 @@ namespace litecore { namespace blip { (The message must be complete.) */ void respond(); - /** Sends an error as a response. (The message must be complete.) */ + /** Sends an error as a response. (The MessageIn must be complete.) */ void respondWithError(Error); + /** Sends a BLIP-domain error as a response. (The MessageIn must be complete.) */ + void respondWithError(int blipErrorCode, slice message) { + respondWithError(Error{kBLIPErrorDomain, blipErrorCode, message}); + } + /** Responds with an error saying that the message went unhandled. Call this if you don't know what to do with a request. (The message must be complete.) */ diff --git a/Networking/BLIP/MessageBuilder.cc b/Networking/BLIP/MessageBuilder.cc index 2a3d6b4b9..b4d74dffd 100644 --- a/Networking/BLIP/MessageBuilder.cc +++ b/Networking/BLIP/MessageBuilder.cc @@ -30,7 +30,7 @@ namespace litecore { namespace blip { MessageBuilder::MessageBuilder(slice profile) { if (profile) - addProperty("Profile"_sl, profile); + setProfile(profile); } @@ -50,6 +50,11 @@ namespace litecore { namespace blip { } + void MessageBuilder::setProfile(slice profile) { + addProperty(kProfileProperty, profile); + } + + MessageBuilder& MessageBuilder::addProperties(initializer_list properties) { for (const property &p : properties) addProperty(p.first, p.second); @@ -60,8 +65,8 @@ namespace litecore { namespace blip { void MessageBuilder::makeError(Error err) { DebugAssert(err.domain && err.code); type = kErrorType; - addProperty("Error-Domain"_sl, err.domain); - addProperty("Error-Code"_sl, err.code); + addProperty(kErrorDomainProperty, err.domain); + addProperty(kErrorDomainProperty, err.code); write(err.message); } diff --git a/Networking/BLIP/MessageBuilder.hh b/Networking/BLIP/MessageBuilder.hh index e64348048..2c10f3030 100644 --- a/Networking/BLIP/MessageBuilder.hh +++ b/Networking/BLIP/MessageBuilder.hh @@ -51,6 +51,8 @@ namespace litecore { namespace blip { /** Constructs a MessageBuilder for a response. */ MessageBuilder(MessageIn *inReplyTo); + void setProfile(slice profile); + /** Adds a property. */ MessageBuilder& addProperty(slice name, slice value); diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 729c96ae3..eb3324fde 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -153,7 +153,7 @@ namespace litecore::client { // This only gets called if none of the registered handlers were triggered. void ConnectedClient::onRequestReceived(MessageIn *msg) { warn("Received unrecognized BLIP request #%" PRIu64 " with Profile '%.*s', %zu bytes", - msg->number(), FMTSLICE(msg->property("Profile"_sl)), msg->body().size); + msg->number(), FMTSLICE(msg->profile()), msg->body().size); msg->notHandled(); } diff --git a/Replicator/Pusher+Attachments.cc b/Replicator/Pusher+Attachments.cc index d39f445f8..e49015ade 100644 --- a/Replicator/Pusher+Attachments.cc +++ b/Replicator/Pusher+Attachments.cc @@ -149,7 +149,7 @@ namespace litecore::repl { // First digest the length-prefixed nonce: slice nonce = request->body(); if (nonce.size == 0 || nonce.size > 255) { - request->respondWithError({"BLIP"_sl, 400, "Missing nonce"_sl}); + request->respondWithError(400, "Missing nonce"_sl); return; } sha << (nonce.size & 0xFF) << nonce; diff --git a/Replicator/Pusher+Revs.cc b/Replicator/Pusher+Revs.cc index a7b745554..2fed9df70 100644 --- a/Replicator/Pusher+Revs.cc +++ b/Replicator/Pusher+Revs.cc @@ -161,7 +161,7 @@ namespace litecore::repl { MessageBuilder msg; C4Error c4err; if (buildRevisionMessage(request, msg, {}, &c4err)) { - msg["Profile"] = "rev"; + msg.setProfile("rev"); logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", SPLAT(request->docID), SPLAT(request->revID)); sendRequest(msg, [this, request](MessageProgress progress) { @@ -180,7 +180,7 @@ namespace litecore::repl { c4err.description().c_str()); blipError = 500; } - msg["Profile"] = "norev"; + msg.setProfile("norev"); msg["error"_sl] = blipError; msg.noreply = true; sendRequest(msg); diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index ae35a02c1..721df7a29 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -304,7 +304,7 @@ namespace litecore { namespace repl { _changesFeed.setFindForeignAncestors(getForeignAncestors()); if (!proposedChanges && reply->isError()) { auto err = reply->getError(); - if (err.code == 409 && (err.domain == "BLIP"_sl || err.domain == "HTTP"_sl)) { + if (err.code == 409 && (err.domain == kBLIPErrorDomain || err.domain == "HTTP"_sl)) { if (!_proposeChanges && !_proposeChangesKnown) { // Caller is in no-conflict mode, wants 'proposeChanges' instead; retry logInfo("Server requires 'proposeChanges'; retrying..."); diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index e8136040d..5bf570535 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -492,7 +492,7 @@ namespace litecore { namespace repl { // This only gets called if none of the registered handlers were triggered. void Replicator::_onRequestReceived(Retained msg) { warn("Received unrecognized BLIP request #%" PRIu64 " with Profile '%.*s', %zu bytes", - msg->number(), SPLAT(msg->property("Profile"_sl)), msg->body().size); + msg->number(), SPLAT(msg->profile()), msg->body().size); msg->notHandled(); } @@ -715,7 +715,7 @@ namespace litecore { namespace repl { if (checkpointID) logInfo("Request to %s peer checkpoint '%.*s'", whatFor, SPLAT(checkpointID)); else - request->respondWithError({"BLIP"_sl, 400, "missing checkpoint ID"_sl}); + request->respondWithError(400, "missing checkpoint ID"); return checkpointID; } diff --git a/Replicator/RevFinder.cc b/Replicator/RevFinder.cc index 3064a1165..b18943fe9 100644 --- a/Replicator/RevFinder.cc +++ b/Replicator/RevFinder.cc @@ -57,7 +57,7 @@ namespace litecore::repl { handleChangesNow(req); } else { logVerbose("Queued '%.*s' REQ#%" PRIu64 " (now %zu)", - SPLAT(req->property("Profile"_sl)), req->number(), + SPLAT(req->profile()), req->number(), _waitingChangesMessages.size() + 1); Signpost::begin(Signpost::handlingChanges, (uintptr_t)req->number()); _waitingChangesMessages.push_back(move(req)); @@ -85,7 +85,7 @@ namespace litecore::repl { // Actually handle a "changes" (or "proposeChanges") message: void RevFinder::handleChangesNow(MessageIn *req) { try { - slice reqType = req->property("Profile"_sl); + slice reqType = req->profile(); bool proposed = (reqType == "proposeChanges"_sl); logVerbose("Handling '%.*s' REQ#%" PRIu64, SPLAT(reqType), req->number()); @@ -93,11 +93,11 @@ namespace litecore::repl { auto nChanges = changes.count(); if (!changes && req->body() != "null"_sl) { warn("Invalid body of 'changes' message"); - req->respondWithError({"BLIP"_sl, 400, "Invalid JSON body"_sl}); + req->respondWithError(400, "Invalid JSON body"_sl); } else if ((!proposed && _mustBeProposed) || (proposed && _db->usingVersionVectors())) { // In conflict-free mode plus rev-trees the protocol requires the pusher send // "proposeChanges" instead. But with version vectors, always use "changes". - req->respondWithError({"BLIP"_sl, 409}); + req->respondWithError({kBLIPErrorDomain, 409}); } else if (nChanges == 0) { // Empty array indicates we've caught up. (This may have been sent no-reply) logInfo("Caught up with remote changes"); @@ -157,7 +157,7 @@ namespace litecore::repl { req->respond(response); logInfo("Responded to '%.*s' REQ#%" PRIu64 " w/request for %u revs in %.6f sec", - SPLAT(req->property("Profile"_sl)), req->number(), requested, st.elapsed()); + SPLAT(req->profile()), req->number(), requested, st.elapsed()); } } catch (...) { auto error = C4Error::fromCurrentException(); From dbccbb9c79c43344b55d795b9a95edf0eaf28847 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 12:22:00 -0800 Subject: [PATCH 17/78] Made BLIP::MessageNo type-safe --- C/Cpp_include/c4EnumUtil.hh | 1 + Networking/BLIP/BLIPConnection.cc | 22 ++++++++++++++-------- Networking/BLIP/BLIPProtocol.hh | 4 +++- Networking/BLIP/Message.cc | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/C/Cpp_include/c4EnumUtil.hh b/C/Cpp_include/c4EnumUtil.hh index 8952a8ba2..9032500f0 100644 --- a/C/Cpp_include/c4EnumUtil.hh +++ b/C/Cpp_include/c4EnumUtil.hh @@ -11,6 +11,7 @@ // #pragma once +#include "c4Compat.h" #include /// Declares `++` and `--` functions for an `enum class`. diff --git a/Networking/BLIP/BLIPConnection.cc b/Networking/BLIP/BLIPConnection.cc index d9fed486e..7ec28a772 100644 --- a/Networking/BLIP/BLIPConnection.cc +++ b/Networking/BLIP/BLIPConnection.cc @@ -16,6 +16,7 @@ #include "WebSocketInterface.hh" #include "Actor.hh" #include "Batcher.hh" +#include "c4EnumUtil.hh" #include "Codec.hh" #include "Error.hh" #include "Logging.hh" @@ -37,6 +38,10 @@ using namespace litecore::websocket; namespace litecore { namespace blip { + // Allow some arithmetic on MessageNo: + DEFINE_ENUM_INC_DEC(MessageNo) + DEFINE_ENUM_ADD_SUB_INT(MessageNo) + static const size_t kDefaultFrameSize = 4096; // Default size of frame static const size_t kBigFrameSize = 16384; // Max size of frame @@ -102,7 +107,7 @@ namespace litecore { namespace blip { MessageQueue _icebox; bool _writeable {true}; MessageMap _pendingRequests, _pendingResponses; - atomic _lastMessageNo {0}; + MessageNo _lastMessageNo {0}; MessageNo _numRequestsReceived {0}; Deflater _outputCodec; Inflater _inputCodec; @@ -260,7 +265,7 @@ namespace litecore { namespace blip { msg->disconnected(); return; } - if (msg->_number == 0) + if (msg->_number == MessageNo{0}) msg->_number = ++_lastMessageNo; if (BLIPLog.willLog(LogLevel::Verbose)) { if (!msg->isAck() || BLIPLog.willLog(LogLevel::Debug)) @@ -349,7 +354,7 @@ namespace litecore { namespace blip { if (!_frameBuf) _frameBuf.reset(new uint8_t[kMaxVarintLen64 + 1 + 4 + kBigFrameSize]); slice_ostream out(_frameBuf.get(), maxSize); - out.writeUVarInt(msg->_number); + out.writeUVarInt(messageno_t(msg->_number)); auto flagsPos = (FrameFlags*)out.next(); out.advance(1); @@ -409,17 +414,17 @@ namespace litecore { namespace blip { // Read the frame header: slice_istream payload = wsMessage->data; _totalBytesRead += payload.size; - uint64_t msgNo; + MessageNo msgNo; FrameFlags flags; { auto pMsgNo = payload.readUVarInt(), pFlags = payload.readUVarInt(); if (!pMsgNo || !pFlags) throw runtime_error("Illegal BLIP frame header"); - msgNo = *pMsgNo; + msgNo = MessageNo{*pMsgNo}; flags = (FrameFlags)*pFlags; } logVerbose("Received frame: %s #%" PRIu64 " %c%c%c%c, length %5ld", - kMessageTypeNames[flags & kTypeMask], msgNo, + kMessageTypeNames[flags & kTypeMask], messageno_t(msgNo), (flags & kMoreComing ? 'M' : '-'), (flags & kUrgent ? 'U' : '-'), (flags & kNoReply ? 'N' : '-'), @@ -552,7 +557,8 @@ namespace litecore { namespace blip { _pendingResponses.erase(i); } else { throw runtime_error(format("BLIP protocol error: Bad incoming RES #%" PRIu64 " (%s)", - msgNo, (msgNo <= _lastMessageNo ? "no request waiting" : "too high"))); + messageno_t(msgNo), + (msgNo <= _lastMessageNo ? "no request waiting" : "too high"))); } return msg; } @@ -658,7 +664,7 @@ namespace litecore { namespace blip { /** Public API to send a new request. */ void Connection::sendRequest(MessageBuilder &mb) { - Retained message = new MessageOut(this, mb, 0); + Retained message = new MessageOut(this, mb, MessageNo{0}); DebugAssert(message->type() == kRequestType); send(message); } diff --git a/Networking/BLIP/BLIPProtocol.hh b/Networking/BLIP/BLIPProtocol.hh index bdb6524bf..a4d904126 100644 --- a/Networking/BLIP/BLIPProtocol.hh +++ b/Networking/BLIP/BLIPProtocol.hh @@ -40,9 +40,11 @@ namespace litecore { namespace blip { }; + using messageno_t = uint64_t; + /// A message number. Each peer numbers messages it sends sequentially starting at 1. /// Each peer's message numbers are independent. - typedef uint64_t MessageNo; + enum class MessageNo : messageno_t { }; /// The size of a message. typedef uint64_t MessageSize; diff --git a/Networking/BLIP/Message.cc b/Networking/BLIP/Message.cc index cafa5b9fa..86360ff44 100644 --- a/Networking/BLIP/Message.cc +++ b/Networking/BLIP/Message.cc @@ -65,7 +65,7 @@ namespace litecore { namespace blip { void Message::dumpHeader(std::ostream& out) { out << kMessageTypeNames[type()]; - out << " #" << _number << ' '; + out << " #" << messageno_t(_number) << ' '; if (_flags & kUrgent) out << 'U'; if (_flags & kNoReply) out << 'N'; if (_flags & kCompressed) out << 'Z'; @@ -177,7 +177,7 @@ namespace litecore { namespace blip { if (!_in) { // First frame! // Update my flags and allocate the Writer: - DebugAssert(_number > 0); + DebugAssert(messageno_t(_number) > 0); _flags = (FrameFlags)(frameFlags & ~kMoreComing); _in.reset(new fleece::JSONEncoder); From 55665b1e1426d447b5a91190fdec6da4af6afd0a Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 12:22:11 -0800 Subject: [PATCH 18/78] (more blip cleanup) --- Networking/BLIP/BLIPConnection.hh | 2 +- Networking/BLIP/Message.hh | 2 +- Networking/BLIP/MessageBuilder.hh | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Networking/BLIP/BLIPConnection.hh b/Networking/BLIP/BLIPConnection.hh index adaf7627a..1c0012c92 100644 --- a/Networking/BLIP/BLIPConnection.hh +++ b/Networking/BLIP/BLIPConnection.hh @@ -40,7 +40,7 @@ namespace litecore { namespace blip { using CloseStatus = websocket::CloseStatus; - /** WebSocket 'protocol' name for BLIP; use as value of kProtocolsOption option. */ + /** WebSocket 'protocol' name for BLIP; use as value of kC4SocketOptionWSProtocols. */ static constexpr const char *kWSProtocolName = "BLIP_3"; /** Option to set the 'deflate' compression level. Value must be an integer in the range diff --git a/Networking/BLIP/Message.hh b/Networking/BLIP/Message.hh index 7a3ee8f4d..c42f29ceb 100644 --- a/Networking/BLIP/Message.hh +++ b/Networking/BLIP/Message.hh @@ -137,7 +137,7 @@ namespace litecore { namespace blip { long intProperty(slice property, long defaultValue =0) const; bool boolProperty(slice property, bool defaultValue =false) const; - /** The "Profile" property gives the message type. */ + /** The "Profile" property gives the message's application-level type name. */ slice profile() const {return property(kProfileProperty);} /** Returns information about an error (if this message is an error.) */ diff --git a/Networking/BLIP/MessageBuilder.hh b/Networking/BLIP/MessageBuilder.hh index 2c10f3030..3fa825a1f 100644 --- a/Networking/BLIP/MessageBuilder.hh +++ b/Networking/BLIP/MessageBuilder.hh @@ -23,7 +23,6 @@ namespace litecore { namespace blip { /** A callback to provide data for an outgoing message. When called, it should copy data to the location in the `buf` parameter, with a maximum length of `capacity`. It should return the number of bytes written, or 0 on EOF, or a negative number on error. */ - //using MessageDataSource = std::function; class IMessageDataSource { public: virtual int operator() (void *buf, size_t capacity) =0; @@ -43,13 +42,13 @@ namespace litecore { namespace blip { typedef std::pair property; /** Constructs a MessageBuilder for a request, optionally setting its Profile property. */ - MessageBuilder(slice profile = fleece::nullslice); + explicit MessageBuilder(slice profile = fleece::nullslice); /** Constructs a MessageBuilder for a request, with a list of properties. */ - MessageBuilder(std::initializer_list); + explicit MessageBuilder(std::initializer_list); /** Constructs a MessageBuilder for a response. */ - MessageBuilder(MessageIn *inReplyTo); + explicit MessageBuilder(MessageIn *inReplyTo); void setProfile(slice profile); From 625e945ea442155991267b0c280207d382380f94 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 13:27:32 -0800 Subject: [PATCH 19/78] c4Test: Made importJSONLines work in non-empty db --- C/tests/c4Test.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/C/tests/c4Test.cc b/C/tests/c4Test.cc index 07843e092..ecd13418f 100644 --- a/C/tests/c4Test.cc +++ b/C/tests/c4Test.cc @@ -604,7 +604,8 @@ unsigned C4Test::importJSONLines(string path, double timeout, bool verbose, C4Da if(database == nullptr) { database = db; } - + + uint64_t docCount = c4db_getDocumentCount(database); unsigned numDocs = 0; bool completed; { @@ -615,7 +616,7 @@ unsigned C4Test::importJSONLines(string path, double timeout, bool verbose, C4Da REQUIRE(body.buf); char docID[20]; - sprintf(docID, "%07u", numDocs+1); + sprintf(docID, "%07u", unsigned(docCount+1)); // Save document: C4DocPutRequest rq = {}; @@ -626,6 +627,7 @@ unsigned C4Test::importJSONLines(string path, double timeout, bool verbose, C4Da REQUIRE(doc != nullptr); c4doc_release(doc); ++numDocs; + ++docCount; if (numDocs % 1000 == 0 && timeout > 0.0 && st.elapsed() >= timeout) { C4Warn("Stopping JSON import after %.3f sec ", st.elapsed()); return false; @@ -638,7 +640,7 @@ unsigned C4Test::importJSONLines(string path, double timeout, bool verbose, C4Da } if (verbose) st.printReport("Importing", numDocs, "doc"); if (completed) - CHECK(c4db_getDocumentCount(database) == numDocs); + CHECK(c4db_getDocumentCount(database) == docCount); return numDocs; } From 8565cb6d9cefb7dbf63c8c9b2242a6229ff1d3b1 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 13:27:49 -0800 Subject: [PATCH 20/78] (oops, fixed MessageBuilder) --- Networking/BLIP/MessageBuilder.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Networking/BLIP/MessageBuilder.cc b/Networking/BLIP/MessageBuilder.cc index b4d74dffd..cdbf816c9 100644 --- a/Networking/BLIP/MessageBuilder.cc +++ b/Networking/BLIP/MessageBuilder.cc @@ -66,7 +66,7 @@ namespace litecore { namespace blip { DebugAssert(err.domain && err.code); type = kErrorType; addProperty(kErrorDomainProperty, err.domain); - addProperty(kErrorDomainProperty, err.code); + addProperty(kErrorCodeProperty, err.code); write(err.message); } From d62c9bce88ffeff0e0f1eda192e3c11e2898f3d9 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 13:28:08 -0800 Subject: [PATCH 21/78] ConnectedClient::observe works --- Replicator/ConnectedClient/ConnectedClient.cc | 30 +++++---- Replicator/tests/ConnectedClientTest.cc | 62 ++++++++++++++++--- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index eb3324fde..6ab1e3a5f 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -163,17 +163,21 @@ namespace litecore::client { // Returns the error status of a response (including a NULL response, i.e. disconnection) C4Error ConnectedClient::responseError(MessageIn *response) { + C4Error error; if (!response) { // Disconnected! - return status().error ? status().error : C4Error::make(LiteCoreDomain, - kC4ErrorIOError, - "network connection lost"); + error = status().error; + if (!error) + error = C4Error::make(LiteCoreDomain, kC4ErrorIOError, "network connection lost"); // TODO: Use a better default error than the one above } else if (response->isError()) { - return blipToC4Error(response->getError()); + error = blipToC4Error(response->getError()); } else { - return {}; + error = {}; } + if (error) + logError("Connected Client got error response %s", error.description().c_str()); + return error; } @@ -205,7 +209,7 @@ namespace litecore::client { FLError flErr; docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); if (!docResponse.body) - return C4Error::make(FleeceDomain, flErr); + return C4Error::make(FleeceDomain, flErr, "Unparseable JSON response from server"); } return docResponse; END_ASYNC() @@ -312,13 +316,15 @@ namespace litecore::client { // "changes" expects a response with an array of which items we want "rev" messages for. // We don't actually want any. An empty array will indicate that. - MessageBuilder response(req); - auto &enc = response.jsonBody(); - enc.beginArray(); - enc.endArray(); - req->respond(response); + if (!req->noReply()) { + MessageBuilder response(req); + auto &enc = response.jsonBody(); + enc.beginArray(); + enc.endArray(); + req->respond(response); + } - if (_observer) { + if (_observer && !inChanges.empty()) { // Convert the JSON change list into a vector: vector outChanges; outChanges.reserve(inChanges.count()); diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 67f25e18a..2ba865b72 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -42,7 +42,10 @@ class ConnectedClientLoopbackTest : public C4Test, auto serverOpts = make_retained(kC4Passive,kC4Passive); serverOpts->setProperty(kC4ReplicatorOptionAllowConnectedClient, true); serverOpts->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); - _server = new repl::Replicator(db, + + c4::ref serverDB = c4db_openAgain(db, ERROR_INFO()); + REQUIRE(serverDB); + _server = new repl::Replicator(serverDB, new LoopbackWebSocket(alloc_slice("ws://srv/"), Role::Server, {}), *this, serverOpts); @@ -73,7 +76,7 @@ class ConnectedClientLoopbackTest : public C4Test, _client = nullptr; } - Log("Waiting for client & replicator to stop..."); + Log("+++ Waiting for client & replicator to stop..."); _cond.wait(lock, [&]{return !_clientRunning && !_serverRunning;}); } @@ -82,7 +85,7 @@ class ConnectedClientLoopbackTest : public C4Test, T waitForResponse(actor::Async> &asyncResult) { asyncResult.blockUntilReady(); - C4Log("++++ Async response available!"); + Log("++++ Async response available!"); auto &result = asyncResult.result(); if (auto err = std::get_if(&result)) FAIL("Response returned an error " << *err); @@ -94,7 +97,7 @@ class ConnectedClientLoopbackTest : public C4Test, C4Error waitForErrorResponse(actor::Async> &asyncResult) { asyncResult.blockUntilReady(); - C4Log("++++ Async response available!"); + Log("++++ Async response available!"); auto &result = asyncResult.result(); const C4Error *err = std::get_if(&result); if (!*err) @@ -112,16 +115,16 @@ class ConnectedClientLoopbackTest : public C4Test, int status, const websocket::Headers &headers) override { - C4Log("Client got HTTP response"); + Log("+++ Client got HTTP response"); } void clientGotTLSCertificate(client::ConnectedClient* NONNULL, slice certData) override { - C4Log("Client got TLS certificate"); + Log("+++ Client got TLS certificate"); } void clientStatusChanged(client::ConnectedClient* NONNULL, C4ReplicatorActivityLevel level) override { - C4Log("Client status changed: %d", int(level)); + Log("+++ Client status changed: %d", int(level)); if (level == kC4Stopped) { std::unique_lock lock(_mutex); _clientRunning = false; @@ -131,7 +134,7 @@ class ConnectedClientLoopbackTest : public C4Test, } void clientConnectionClosed(client::ConnectedClient* NONNULL, const CloseStatus &close) override { - C4Log("Client connection closed: reason=%d, code=%d, message=%.*s", + Log("+++ Client connection closed: reason=%d, code=%d, message=%.*s", int(close.reason), close.code, FMTSLICE(close.message)); } @@ -172,7 +175,7 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { importJSONLines(sFixturesDir + "names_100.json"); start(); - C4Log("++++ Calling ConnectedClient::getDoc()..."); + Log("++++ Calling ConnectedClient::getDoc()..."); auto asyncResult1 = _client->getDoc(alloc_slice("0000001"), nullslice, nullslice); auto asyncResult99 = _client->getDoc(alloc_slice("0000099"), nullslice, nullslice); @@ -299,3 +302,44 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Failure", "[ConnectedClien rq1.blockUntilReady(); REQUIRE(rq1.result() == C4Error{LiteCoreDomain, kC4ErrorConflict}); } + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "observeCollection", "[ConnectedClient]") { + { + // Start with a single doc that should not be sent to the observer + TransactionHelper t(db); + createFleeceRev(db, "doc1"_sl, "1-1111"_sl, R"({"name":"Puddin' Tane"})"_sl); + } + start(); + + mutex m; + condition_variable cond; + vector allChanges; + + _client->observeCollection(nullslice, [&](vector const& changes) { + // Observer callback: + unique_lock lock(m); + Log("+++ Observer got %zu changes!", changes.size()); + allChanges.insert(allChanges.end(), changes.begin(), changes.end()); + cond.notify_one(); + }).then([&](C4Error error) { + // Async callback when the observer has started: + unique_lock lock(m); + REQUIRE(error == C4Error{}); + Log("+++ Importing docs..."); + importJSONLines(sFixturesDir + "names_100.json"); + }); + + Log("+++ Waiting for 100 changes to arrive..."); + unique_lock lock(m); + cond.wait(lock, [&]{return allChanges.size() >= 100;}); + + Log("+++ Checking the changes"); + REQUIRE(allChanges.size() == 100); + C4SequenceNumber expectedSeq = 2; + for (auto &change : allChanges) { + CHECK(change.docID.size == 7); + CHECK(change.flags == 0); + CHECK(change.sequence == expectedSeq++); + } +} From 91c8960e45a75ef6d5382e3cfd546b382528cfa1 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 16:17:28 -0800 Subject: [PATCH 22/78] Async tweaks (avoid copying) --- LiteCore/Support/Async.cc | 2 +- LiteCore/Support/Async.hh | 66 +++++++++++++++++++++---------------- LiteCore/tests/AsyncTest.cc | 2 +- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 10b2cf352..d9a5578ac 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -23,7 +23,7 @@ namespace litecore::actor { // Called from `BEGIN_ASYNC()`. This is an optimization for a void-returning function that // avoids allocating an AsyncProvider if the function body never has to block. - void AsyncFnState::asyncVoidFn(Actor *thisActor, function fnBody) { + void AsyncFnState::asyncVoidFn(Actor *thisActor, function &&fnBody) { if (thisActor && thisActor != Actor::currentActor()) { // Need to run this on the Actor's queue, so schedule it: (new AsyncProvider(thisActor, move(fnBody)))->_start(); diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 1392ba667..203c23a40 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -33,18 +33,25 @@ namespace litecore::actor { /// Put this at the top of an async function/method that returns `Async`, /// but below declarations of any variables that need to be in scope for the whole method. -#define BEGIN_ASYNC_RETURNING(T) \ - return litecore::actor::Async(thisActor(), [=](litecore::actor::AsyncFnState &_async_state_) mutable -> std::optional { \ +#define BEGIN_ASYNC_RETURNING_CAPTURING(T, ...) \ + return litecore::actor::Async(thisActor(), \ + [__VA_ARGS__](litecore::actor::AsyncFnState &_async_state_) mutable \ + -> std::optional { \ switch (_async_state_.currentLine()) { \ default: { +#define BEGIN_ASYNC_RETURNING(T) BEGIN_ASYNC_RETURNING_CAPTURING(T, =) + /// Put this at the top of an async method that returns `void`. /// See `BEGIN_ASYNC_RETURNING` for details. -#define BEGIN_ASYNC() \ - return litecore::actor::AsyncFnState::asyncVoidFn(thisActor(), [=](litecore::actor::AsyncFnState &_async_state_) mutable -> void { \ +#define BEGIN_ASYNC_CAPTURING(...) \ + return litecore::actor::AsyncFnState::asyncVoidFn(thisActor(), \ + [__VA_ARGS__](litecore::actor::AsyncFnState &_async_state_) mutable -> void { \ switch (_async_state_.currentLine()) { \ default: { +#define BEGIN_ASYNC() BEGIN_ASYNC_CAPTURING(=) + /// Use this in an async method to resolve an `Async<>` value, blocking until it's available. /// `VAR` is the name of the variable to which to assign the result. /// `EXPR` is the expression (usually a call to an async method) returning an `Async` value, @@ -84,7 +91,7 @@ namespace litecore::actor { template bool await(const Async &a, int curLine); template Retained> awaited(); - static void asyncVoidFn(Actor *actor, std::function body); + static void asyncVoidFn(Actor *actor, std::function &&body); protected: friend class AsyncProviderBase; @@ -124,7 +131,7 @@ namespace litecore::actor { public: bool ready() const {return _ready;} - template const T& result(); + template T& result(); template T&& extractResult(); /// Returns the exception result, else nullptr. @@ -168,7 +175,9 @@ namespace litecore::actor { static Retained create() {return new AsyncProvider;} /// Creates a new AsyncProvider that already has a result. - static Retained createReady(T&& r) {return new AsyncProvider(std::move(r));} + static Retained createReady(T&& r) { + return new AsyncProvider(std::forward(r)); + } /// Constructs a new empty AsyncProvider. AsyncProvider() = default; @@ -188,7 +197,7 @@ namespace litecore::actor { void setResult(T &&result) { std::unique_lock lock(_mutex); precondition(!_result); - _result = std::move(result); + _result = std::forward(result); _gotResult(lock); } @@ -203,7 +212,7 @@ namespace litecore::actor { } template - void setResultFromCallback(LAMBDA callback) { + void setResultFromCallback(LAMBDA &&callback) { bool duringCallback = true; try { auto result = callback(); @@ -217,7 +226,7 @@ namespace litecore::actor { } /// Returns the result, which must be available. - const T& result() const & { + T& result() & { std::unique_lock _lock(_mutex); rethrowException(); precondition(_result); @@ -324,20 +333,20 @@ namespace litecore::actor { /// Creates an already-resolved Async with a value. explicit Async(T&& t) - :AsyncBase(AsyncProvider::createReady(std::move(t))) + :AsyncBase(AsyncProvider::createReady(std::forward(t))) { } // (used by `BEGIN_ASYNC_RETURNING(T)`. Don't call directly.) - Async(Actor *actor, typename AsyncProvider::Body bodyFn) + Async(Actor *actor, typename AsyncProvider::Body &&bodyFn) :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) { } /// Returns the result. (Will abort if the result is not yet available.) - const T& result() const & {return _provider->result();} - T&& result() const && {return _provider->result();} + T& result() & {return _provider->result();} + T&& result() && {return _provider->result();} /// Move-returns the result. (Will abort if the result is not yet available.) - T&& extractResult() const {return _provider->extractResult();} + T&& extractResult() {return _provider->extractResult();} /// Invokes the callback when the result becomes ready (immediately if it's already ready.) /// The callback should take a single parameter of type `T`, `T&` or `T&&`. @@ -354,20 +363,20 @@ namespace litecore::actor { /// - `Async x = a.then([](T) -> X { ... });` /// - `Async x = a.then([](T) -> Async { ... });` template - auto then(Actor *onActor, LAMBDA callback) { + auto then(Actor *onActor, LAMBDA &&callback) && { using U = unwrap_async>; // return type w/o Async<> - return _then(onActor, callback); + return _then(onActor, std::forward(callback)); } template - auto then(LAMBDA callback) { + auto then(LAMBDA &&callback) && { using U = unwrap_async>; // return type w/o Async<> - return _then(nullptr, callback); + return _then(nullptr, std::forward(callback)); } /// Blocks the current thread until the result is available, then returns it. /// Please don't use this unless absolutely necessary; use `then()` or `AWAIT()` instead. - const T& blockingResult() { + T& blockingResult() { blockUntilReady(); return result(); } @@ -385,7 +394,7 @@ namespace litecore::actor { // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. template typename Async::AwaitReturnType - _then(Actor *onActor, std::function callback) { + _then(Actor *onActor, std::function &&callback) { auto uProvider = Async::makeProvider(); if (ready()) { // Result is available now, so call the callback: @@ -396,7 +405,7 @@ namespace litecore::actor { } else { // Create an AsyncWaiter to wait on the provider: Waiter::start(this->provider(), onActor, [uProvider,callback](T&& result) { - uProvider->setResultFromCallback([&]{return callback(std::move(result));}); + uProvider->setResultFromCallback([&]{return callback(std::forward(result));}); }); } return uProvider->asyncValue(); @@ -404,7 +413,7 @@ namespace litecore::actor { // Implements `then` where the lambda returns void. (Specialization of above method.) template<> - void _then(Actor *onActor, std::function callback) { + void _then(Actor *onActor, std::function &&callback) { if (ready()) callback(extractResult()); else @@ -413,7 +422,7 @@ namespace litecore::actor { // Implements `then` where the lambda returns `Async`. template - Async _then(Actor *onActor, std::function(T&&)> callback) { + Async _then(Actor *onActor, std::function(T&&)> &&callback) { if (ready()) { // If I'm ready, just call the callback and pass on the Async it returns: return callback(extractResult()); @@ -422,10 +431,9 @@ namespace litecore::actor { auto uProvider = Async::makeProvider(); Waiter::start(provider(), onActor, [uProvider,callback=std::move(callback)](T&& result) { // Invoke the callback, then wait to resolve the Async it returns: - Async u = callback(std::move(result)); - u.then([uProvider](U &&uresult) { + callback(std::move(result)) .then([uProvider](U &&uresult) { // Then finally resolve the async I returned: - uProvider->setResult(std::move(uresult)); + uProvider->setResult(std::forward(uresult)); }); }); return uProvider->asyncValue(); @@ -460,7 +468,7 @@ namespace litecore::actor { } template - const T& AsyncProviderBase::result() { + T& AsyncProviderBase::result() { return dynamic_cast*>(this)->result(); } @@ -477,7 +485,7 @@ namespace litecore::actor { using Callback = std::function; static void start(AsyncProvider *provider, Actor *onActor, Callback &&callback) { - (void) new Waiter(provider, onActor, std::move(callback)); + (void) new Waiter(provider, onActor, std::forward(callback)); } protected: diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 1d94b1450..e770490eb 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -165,7 +165,7 @@ TEST_CASE_METHOD(AsyncTest, "Async, emplaceResult") { TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { Async sum = provideSum(); string result; - sum.then([&](string &&s) { + move(sum).then([&](string &&s) { result = s; }); REQUIRE(!sum.ready()); From d39ba90b11c7249282d7b10bef95fd528c813af9 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 22 Feb 2022 16:17:58 -0800 Subject: [PATCH 23/78] ConnectedClient: Don't use alloc_slice in API --- Replicator/ConnectedClient/ConnectedClient.cc | 88 +++++++++++-------- Replicator/ConnectedClient/ConnectedClient.hh | 20 ++--- Replicator/tests/ConnectedClientTest.cc | 30 +++---- 3 files changed, 78 insertions(+), 60 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 6ab1e3a5f..66ff6ed87 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -181,12 +181,17 @@ namespace litecore::client { } - Async ConnectedClient::getDoc(alloc_slice docID, - alloc_slice collectionID, - alloc_slice unlessRevID, + Async ConnectedClient::getDoc(slice docID_, + slice collectionID_, + slice unlessRevID_, bool asFleece) { - BEGIN_ASYNC_RETURNING(DocResponseOrError) + BEGIN_ASYNC_RETURNING_CAPTURING(DocResponseOrError, + this, + docID = alloc_slice(docID_), + collectionID = alloc_slice(collectionID_), + unlessRevID = alloc_slice(unlessRevID_), + asFleece) logInfo("getDoc(\"%.*s\")", FMTSLICE(docID)); MessageBuilder req("getRev"); req["id"] = docID; @@ -237,14 +242,21 @@ namespace litecore::client { } - Async ConnectedClient::putDoc(alloc_slice docID, - alloc_slice collectionID, - alloc_slice revID, - alloc_slice parentRevID, + Async ConnectedClient::putDoc(slice docID_, + slice collectionID_, + slice revID_, + slice parentRevID_, C4RevisionFlags revisionFlags, - alloc_slice fleeceData) + slice fleeceData_) { - BEGIN_ASYNC_RETURNING(C4Error) + BEGIN_ASYNC_RETURNING_CAPTURING(C4Error, + this, + docID = alloc_slice(docID_), + collectionID = alloc_slice(collectionID_), + revID = alloc_slice(revID_), + parentRevID = alloc_slice(parentRevID_), + revisionFlags, + fleeceData = alloc_slice(fleeceData_)) logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID), FMTSLICE(revID)); MessageBuilder req("putRev"); req.compressed = true; @@ -257,7 +269,7 @@ namespace litecore::client { if (fleeceData.size > 0) { // TODO: Encryption!! // TODO: Convert blobs to legacy attachments - req.jsonBody().writeValue(Doc(fleeceData, kFLTrusted).asDict()); + req.jsonBody().writeValue(FLValue_FromData(fleeceData, kFLTrusted)); } else { req.write("{}"); } @@ -270,11 +282,14 @@ namespace litecore::client { } - Async ConnectedClient::observeCollection(alloc_slice collectionID, - CollectionObserver callback) + Async ConnectedClient::observeCollection(slice collectionID_, + CollectionObserver callback_) { - bool observe = !!callback; - BEGIN_ASYNC_RETURNING(C4Error) + BEGIN_ASYNC_RETURNING_CAPTURING(C4Error, + this, + collectionID = alloc_slice(collectionID_), + observe = !!callback_, + callback = move(callback_)) logInfo("observeCollection(%.*s)", FMTSLICE(collectionID)); bool sameSubState = (observe == !!_observer); @@ -325,28 +340,31 @@ namespace litecore::client { } if (_observer && !inChanges.empty()) { + logInfo("Received %u doc changes from server", inChanges.count()); // Convert the JSON change list into a vector: vector outChanges; outChanges.reserve(inChanges.count()); for (auto item : inChanges) { // "changes" entry: [sequence, docID, revID, deleted?, bodySize?] - auto &outChange = outChanges.emplace_back(); auto inChange = item.asArray(); - outChange.sequence = C4SequenceNumber{inChange[0].asUnsigned()}; - outChange.docID = inChange[1].asString(); - outChange.revID = inChange[2].asString(); - outChange.flags = 0; - int64_t deletion = inChange[3].asInt(); - outChange.bodySize = fleece::narrow_cast(inChange[4].asUnsigned()); - - checkDocAndRevID(outChange.docID, outChange.revID); - - // In SG 2.x "deletion" is a boolean flag, 0=normal, 1=deleted. - // SG 3.x adds 2=revoked, 3=revoked+deleted, 4=removal (from channel) - if (deletion & 0b001) - outChange.flags |= kRevDeleted; - if (deletion & 0b110) - outChange.flags |= kRevPurged; + slice docID = inChange[1].asString(); + slice revID = inChange[2].asString(); + if (validateDocAndRevID(docID, revID)) { + auto &outChange = outChanges.emplace_back(); + outChange.sequence = C4SequenceNumber{inChange[0].asUnsigned()}; + outChange.docID = docID; + outChange.revID = revID; + outChange.flags = 0; + int64_t deletion = inChange[3].asInt(); + outChange.bodySize = fleece::narrow_cast(inChange[4].asUnsigned()); + + // In SG 2.x "deletion" is a boolean flag, 0=normal, 1=deleted. + // SG 3.x adds 2=revoked, 3=revoked+deleted, 4=removal (from channel) + if (deletion & 0b001) + outChange.flags |= kRevDeleted; + if (deletion & 0b110) + outChange.flags |= kRevPurged; + } } // Finally call the observer callback: @@ -360,7 +378,7 @@ namespace litecore::client { } - void ConnectedClient::checkDocAndRevID(slice docID, slice revID) { + bool ConnectedClient::validateDocAndRevID(slice docID, slice revID) { bool valid; if (!C4Document::isValidDocID(docID)) valid = false; @@ -369,10 +387,10 @@ namespace litecore::client { else valid = revID.findByte('-'); if (!valid) { - C4Error::raise(LiteCoreDomain, kC4ErrorRemoteError, - "Invalid docID/revID '%.*s' #%.*s in incoming change list", - FMTSLICE(docID), FMTSLICE(revID)); + warn("Invalid docID/revID '%.*s' #%.*s in incoming change list", + FMTSLICE(docID), FMTSLICE(revID)); } + return valid; } } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index ee5aa4ed6..e2523f5b2 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -77,9 +77,9 @@ namespace litecore::client { /// @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. /// @return An async value that, when resolved, contains either a `DocResponse` struct /// or a C4Error. - actor::Async getDoc(alloc_slice docID, - alloc_slice collectionID, - alloc_slice unlessRevID, + actor::Async getDoc(slice docID, + slice collectionID, + slice unlessRevID, bool asFleece = true); /// Gets the contents of a blob given its digest. @@ -98,19 +98,19 @@ namespace litecore::client { /// @param revisionFlags Flags of this revision. /// @param fleeceData The document body encoded as Fleece (without shared keys!) /// @return An async value that, when resolved, contains the status as a C4Error. - actor::Async putDoc(alloc_slice docID, - alloc_slice collectionID, - alloc_slice revID, - alloc_slice parentRevID, + actor::Async putDoc(slice docID, + slice collectionID, + slice revID, + slice parentRevID, C4RevisionFlags revisionFlags, - alloc_slice fleeceData); + slice fleeceData); /// Registers a listener function that will be called when any document is changed. /// @note To cancel, pass a null callback. /// @param collectionID The ID of the collection to observe. /// @param callback The function to call (on an arbitrary background thread!) /// @return An async value that, when resolved, contains the status as a C4Error. - actor::Async observeCollection(alloc_slice collectionID, + actor::Async observeCollection(slice collectionID, CollectionObserver callback); // exposed for unit tests: @@ -130,7 +130,7 @@ namespace litecore::client { void setStatus(ActivityLevel); C4Error responseError(blip::MessageIn *response); void _disconnect(websocket::CloseCode closeCode, slice message); - void checkDocAndRevID(slice docID, slice revID); + bool validateDocAndRevID(slice docID, slice revID); Delegate* _delegate; // Delegate whom I report progress/errors to ActivityLevel _status; diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 2ba865b72..6a8a5267f 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -176,8 +176,8 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { start(); Log("++++ Calling ConnectedClient::getDoc()..."); - auto asyncResult1 = _client->getDoc(alloc_slice("0000001"), nullslice, nullslice); - auto asyncResult99 = _client->getDoc(alloc_slice("0000099"), nullslice, nullslice); + auto asyncResult1 = _client->getDoc("0000001", nullslice, nullslice); + auto asyncResult99 = _client->getDoc("0000099", nullslice, nullslice); auto rev = waitForResponse(asyncResult1); CHECK(rev.docID == "0000001"); @@ -199,8 +199,8 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev Conditional Match", "[Conn importJSONLines(sFixturesDir + "names_100.json"); start(); - auto match = _client->getDoc(alloc_slice("0000002"), nullslice, - alloc_slice("1-1fdf9d4bdae09f6651938d9ec1d47177280f5a77")); + auto match = _client->getDoc("0000002", nullslice, + "1-1fdf9d4bdae09f6651938d9ec1d47177280f5a77"); CHECK(waitForErrorResponse(match) == C4Error{WebSocketDomain, 304}); } @@ -209,8 +209,8 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev Conditional No Match", "[C importJSONLines(sFixturesDir + "names_100.json"); start(); - auto match = _client->getDoc(alloc_slice("0000002"), nullslice, - alloc_slice("1-beefbeefbeefbeefbeefbeefbeefbeefbeefbeef")); + auto match = _client->getDoc("0000002", nullslice, + "1-beefbeefbeefbeefbeefbeefbeefbeefbeefbeef"); auto rev = waitForResponse(match); CHECK(rev.docID == "0000002"); CHECK(rev.revID == "1-1fdf9d4bdae09f6651938d9ec1d47177280f5a77"); @@ -222,7 +222,7 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev Conditional No Match", "[C TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev NotFound", "[ConnectedClient]") { start(); - auto asyncResultX = _client->getDoc(alloc_slice("bogus"), nullslice, nullslice); + auto asyncResultX = _client->getDoc("bogus", nullslice, nullslice); CHECK(waitForErrorResponse(asyncResultX) == C4Error{LiteCoreDomain, kC4ErrorNotFound}); } @@ -260,13 +260,13 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putRev", "[ConnectedClient]") { enc.endDict(); auto docBody = enc.finish(); - auto rq1 = _client->putDoc(alloc_slice("0000001"), nullslice, - alloc_slice("2-2222"), - alloc_slice("1-4cbe54d79c405e368613186b0bc7ac9ee4a50fbb"), + auto rq1 = _client->putDoc("0000001", nullslice, + "2-2222", + "1-4cbe54d79c405e368613186b0bc7ac9ee4a50fbb", C4RevisionFlags{}, docBody); - auto rq2 = _client->putDoc(alloc_slice("frob"), nullslice, - alloc_slice("1-1111"), + auto rq2 = _client->putDoc("frob", nullslice, + "1-1111", nullslice, C4RevisionFlags{}, docBody); @@ -294,9 +294,9 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Failure", "[ConnectedClien enc.endDict(); auto docBody = enc.finish(); - auto rq1 = _client->putDoc(alloc_slice("0000001"), nullslice, - alloc_slice("2-2222"), - alloc_slice("1-d00d"), + auto rq1 = _client->putDoc("0000001", nullslice, + "2-2222", + "1-d00d", C4RevisionFlags{}, docBody); rq1.blockUntilReady(); From b1ea96bc751b00eeec5900cfb425bcadbc9a52b8 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 24 Feb 2022 17:30:25 -0800 Subject: [PATCH 24/78] Async: Got rid of BEGIN/AWAIT/END, and other work --- LiteCore/Support/Actor.hh | 81 +++-- LiteCore/Support/ActorProperty.hh | 73 ----- LiteCore/Support/Async.cc | 69 +--- LiteCore/Support/Async.hh | 302 ++++++------------ LiteCore/Support/AsyncActorCommon.hh | 14 + LiteCore/tests/AsyncTest.cc | 139 +++----- Networking/BLIP/BLIPConnection.cc | 18 +- Networking/BLIP/BLIPConnection.hh | 5 +- Networking/BLIP/MessageBuilder.cc | 9 + Networking/BLIP/MessageBuilder.hh | 26 +- Networking/BLIP/MessageOut.cc | 13 + Networking/BLIP/MessageOut.hh | 9 +- Networking/BLIP/docs/Async.md | 238 ++++---------- Replicator/ConnectedClient/ConnectedClient.cc | 266 ++++++++------- Replicator/Puller.cc | 114 +++---- Replicator/Pusher+Attachments.cc | 2 +- Replicator/Pusher+Revs.cc | 9 +- Replicator/Pusher.cc | 126 ++++---- Replicator/Replicator.cc | 133 ++++---- Replicator/Worker.cc | 21 +- Replicator/Worker.hh | 3 +- .../xcschemes/LiteCore C++ Tests.xcscheme | 19 ++ build_cmake/scripts/build_unix_all.sh | 20 ++ 23 files changed, 698 insertions(+), 1011 deletions(-) delete mode 100644 LiteCore/Support/ActorProperty.hh create mode 100644 LiteCore/Support/AsyncActorCommon.hh create mode 100755 build_cmake/scripts/build_unix_all.sh diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index d98ae324e..442c9e015 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -31,7 +31,7 @@ namespace litecore { namespace actor { class Actor; - + template class Async; //// Some support code for asynchronize(), from http://stackoverflow.com/questions/42124866 template @@ -50,16 +50,27 @@ namespace litecore { namespace actor { #define ACTOR_BIND_METHOD0(RCVR, METHOD) ^{ ((RCVR)->*METHOD)(); } #define ACTOR_BIND_METHOD(RCVR, METHOD, ARGS) ^{ ((RCVR)->*METHOD)(ARGS...); } #define ACTOR_BIND_FN(FN, ARGS) ^{ FN(ARGS...); } + #define ACTOR_BIND_FN0(FN) ^{ FN(); } #else using Mailbox = ThreadedMailbox; #define ACTOR_BIND_METHOD0(RCVR, METHOD) std::bind(METHOD, RCVR) #define ACTOR_BIND_METHOD(RCVR, METHOD, ARGS) std::bind(METHOD, RCVR, ARGS...) #define ACTOR_BIND_FN(FN, ARGS) std::bind(FN, ARGS...) + #define ACTOR_BIND_FN0(FN) (FN) #endif #define FUNCTION_TO_QUEUE(METHOD) #METHOD, &METHOD + namespace { + // Magic template gunk. `unwrap_async` removes a layer of `Async<...>` from a type: + // - `unwrap_async` is `string`. + // - `unwrap_async> is `string`. + template T _unwrap_async(T*); + template T _unwrap_async(Async*); + template using unwrap_async = decltype(_unwrap_async((T*)nullptr)); + } + /** Abstract base actor class. Subclasses should implement their public methods as calls to `enqueue` that pass the parameter values through, and name a matching private implementation method; for example: @@ -138,12 +149,27 @@ namespace litecore { namespace actor { return _asynchronize(methodName, fn); } + /** Schedules a call to `fn` on the actor's thread. + The return type depends on `fn`s return type: + - `void` -- `asCurrentActor` will return `void`. + - `X` -- `asCurrentActor` will return `Async`, which will resolve after `fn` runs. + - `Async` -- `asCurrentActor` will return `Async`, which will resolve after `fn` + runs _and_ its returned async value resolves. */ + template + auto asCurrentActor(LAMBDA &&fn) { + using U = unwrap_async>; // return type w/o Async<> + return _asCurrentActor(std::forward(fn)); + } + + /** The scheduler calls this after every call to the Actor. */ virtual void afterEvent() { } + /** Called if an Actor method throws an exception. */ virtual void caughtException(const std::exception &x); virtual std::string loggingIdentifier() const { return actorName(); } + /** Writes statistics to the log. */ void logStats() { _mailbox.logStats(); } @@ -165,6 +191,33 @@ namespace litecore { namespace actor { _mailbox.enqueue(methodName, ACTOR_BIND_METHOD(other, fn, args)); } + // Implementation of `asCurrentActor` where `fn` returns a non-async type `T`. + template + auto _asCurrentActor(std::function fn) { + auto provider = Async::makeProvider(); + asCurrentActor([fn,provider] { provider->setResultFromFunction(fn); }); + return provider->asyncValue(); + } + + // Specialization of `asCurrentActor` where `fn` returns void. + template <> + auto _asCurrentActor(std::function fn) { + if (currentActor() == this) + fn(); + else + _mailbox.enqueue("asCurrentActor", ACTOR_BIND_FN0(fn)); + } + + // Implementation of `asCurrentActor` where `fn` itself returns an `Async`. + template + Async _asCurrentActor(std::function()> fn) { + auto provider = Async::makeProvider(); + asCurrentActor([fn,provider] { + fn().then([=](U result) { provider->setResult(std::move(result)); }); + }); + return provider; + } + Mailbox _mailbox; }; @@ -177,30 +230,4 @@ namespace litecore { namespace actor { #undef ACTOR_BIND_METHOD #undef ACTOR_BIND_FN - - template class actor_function; - - template - class actor_function { - public: - template - actor_function(Actor *actor, Callable &&callabl, - typename std::enable_if< - !std::is_same::type, - actor_function>::value>::type * = nullptr) - :_fn(std::forward(callabl)) - { } - - Ret operator()(Params ...params) const { - if (_actor == nullptr || _actor == Actor::currentActor()) - return _fn(std::forward(params)...); - else - _actor->enqueueOther("actor_function", - ACTOR_BIND_FN(_fn, std::forward(params)...)); - } - private: - std::function _fn; - Retained _actor; - }; - } } diff --git a/LiteCore/Support/ActorProperty.hh b/LiteCore/Support/ActorProperty.hh deleted file mode 100644 index c6f6779dd..000000000 --- a/LiteCore/Support/ActorProperty.hh +++ /dev/null @@ -1,73 +0,0 @@ -// -// ActorProperty.hh -// -// Copyright 2017-Present Couchbase, Inc. -// -// Use of this software is governed by the Business Source License included -// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified -// in that file, in accordance with the Business Source License, use of this -// software will be governed by the Apache License, Version 2.0, included in -// the file licenses/APL2.txt. -// - -#pragma once -#include "Actor.hh" -#include -#include - -namespace litecore { namespace actor { - - template - class Observer; - - /** Implementation of an Actor property. This would be a private member variable of an Actor. */ - template - class PropertyImpl { - public: - explicit PropertyImpl(Actor *owner) :_owner(*owner) { } - explicit PropertyImpl(Actor *owner, T t):_owner(*owner), _value(t) { } - - T get() const {return _value;} - operator T() const {return _value;} - PropertyImpl& operator= (const T &t); - - void addObserver(Observer &observer); - - private: - Actor &_owner; - T _value {}; - std::vector> _observers; - }; - - - template - class ObservedProperty { - public: - ~ObservedProperty(); - - T get() const {return _value;} - operator T() const {return _value;} - private: - void receiveValue(T t) {_value = t;} - - Retained &_provider; - T _value; - }; - - - /** Public Actor property. This would be a public member variable of an Actor. */ - template - class Property { - public: - explicit Property(PropertyImpl &prop) :_impl(prop) { } - - using Observer = std::function; - - void addObserver(Observer &observer) {_impl.addObserver(observer);} - void removeObserver(Actor &a); - - private: - PropertyImpl &_impl; - }; - -} } diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index d9a5578ac..8dad20e6e 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -18,50 +18,6 @@ namespace litecore::actor { using namespace std; -#pragma mark - ASYNC FN STATE: - - - // Called from `BEGIN_ASYNC()`. This is an optimization for a void-returning function that - // avoids allocating an AsyncProvider if the function body never has to block. - void AsyncFnState::asyncVoidFn(Actor *thisActor, function &&fnBody) { - if (thisActor && thisActor != Actor::currentActor()) { - // Need to run this on the Actor's queue, so schedule it: - (new AsyncProvider(thisActor, move(fnBody)))->_start(); - } else { - // It's OK to call the body synchronously. As an optimization, call it directly with a - // stack-based AsyncFnState, instead of from a heap-allocated AsyncProvider: - AsyncFnState state(nullptr, thisActor); - fnBody(state); - if (state._awaiting) { - // Body didn't finish (is "blocked" in an `AWAIT()`), so set up a proper provider: - (new AsyncProvider(thisActor, move(fnBody), move(state)))->_wait(); - } - } - } - - - AsyncFnState::AsyncFnState(AsyncProviderBase *owningProvider, Actor *owningActor) - :_owningProvider(owningProvider) - ,_owningActor(owningActor) - { } - - - // copy state from `other`, except `_owningProvider` - void AsyncFnState::updateFrom(AsyncFnState &other) { - _owningActor = other._owningActor; - _awaiting = move(other._awaiting); - _currentLine = other._currentLine; - } - - - // called by `AWAIT()` macro - bool AsyncFnState::_await(const AsyncBase &a, int curLine) { - _awaiting = a._provider; - _currentLine = curLine; - return !a.ready(); - } - - #pragma mark - ASYNC OBSERVER: @@ -80,11 +36,6 @@ namespace litecore::actor { #pragma mark - ASYNC PROVIDER BASE: - AsyncProviderBase::AsyncProviderBase(Actor *actorOwningFn) - :_fnState(new AsyncFnState(this, actorOwningFn)) - { } - - AsyncProviderBase::AsyncProviderBase(bool ready) :_ready(ready) { } @@ -96,16 +47,6 @@ namespace litecore::actor { } - void AsyncProviderBase::_start() { - notifyAsyncResultAvailable(nullptr, _fnState->_owningActor); - } - - - void AsyncProviderBase::_wait() { - _fnState->_awaiting->setObserver(this); - } - - void AsyncProviderBase::setObserver(AsyncObserver *o, Actor *actor) { { unique_lock _lock(_mutex); @@ -124,6 +65,7 @@ namespace litecore::actor { void AsyncProviderBase::_gotResult(std::unique_lock& lock) { + // on entry, `_mutex` is locked by `lock` precondition(!_ready); _ready = true; auto observer = _observer; @@ -134,9 +76,6 @@ namespace litecore::actor { if (observer) observer->notifyAsyncResultAvailable(this, observerActor); - - // If I am the result of an async fn, it must have finished, so forget its state: - _fnState = nullptr; } @@ -157,10 +96,8 @@ namespace litecore::actor { #pragma mark - ASYNC BASE: - AsyncBase::AsyncBase(AsyncProviderBase *context, bool) - :AsyncBase(context) - { - _provider->_start(); + bool AsyncBase::canCallNow() const { + return ready() && (_onActor == nullptr || _onActor == Actor::currentActor()); } diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 203c23a40..72a3f8a38 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -24,89 +24,13 @@ namespace litecore::actor { using fleece::RefCounted; using fleece::Retained; class Actor; - -// *** For full documentation, read Networking/BLIP/docs/Async.md *** - - -#pragma mark - ASYNC/AWAIT MACROS: - - -/// Put this at the top of an async function/method that returns `Async`, -/// but below declarations of any variables that need to be in scope for the whole method. -#define BEGIN_ASYNC_RETURNING_CAPTURING(T, ...) \ - return litecore::actor::Async(thisActor(), \ - [__VA_ARGS__](litecore::actor::AsyncFnState &_async_state_) mutable \ - -> std::optional { \ - switch (_async_state_.currentLine()) { \ - default: { - -#define BEGIN_ASYNC_RETURNING(T) BEGIN_ASYNC_RETURNING_CAPTURING(T, =) - -/// Put this at the top of an async method that returns `void`. -/// See `BEGIN_ASYNC_RETURNING` for details. -#define BEGIN_ASYNC_CAPTURING(...) \ - return litecore::actor::AsyncFnState::asyncVoidFn(thisActor(), \ - [__VA_ARGS__](litecore::actor::AsyncFnState &_async_state_) mutable -> void { \ - switch (_async_state_.currentLine()) { \ - default: { - -#define BEGIN_ASYNC() BEGIN_ASYNC_CAPTURING(=) - -/// Use this in an async method to resolve an `Async<>` value, blocking until it's available. -/// `VAR` is the name of the variable to which to assign the result. -/// `EXPR` is the expression (usually a call to an async method) returning an `Async` value, -/// where `T` can be assigned to `VAR`. -/// If the `Async` value's result is already available, it is immediately assigned to `VAR` and -/// execution continues. -/// Otherwise, this method is suspended until the result becomes available. -#define AWAIT(T, VAR, EXPR) \ - if (_async_state_.await(EXPR, __LINE__)) return {}; \ - }\ - case __LINE__: {\ - T VAR = _async_state_.awaited()->extractResult(); - -#define XAWAIT(VAR, EXPR) \ - if (_async_state_.await(EXPR, __LINE__)) return {}; \ - }\ - case __LINE__: { \ - VAR = _async_state_.awaited>()->extractResult(); - -/// Put this at the very end of an async function/method. -#define END_ASYNC() \ - } \ - } \ - }); - - class AsyncBase; class AsyncProviderBase; template class Async; template class AsyncProvider; - // The state data passed to the lambda of an async function. Internal use only. - class AsyncFnState { - public: - int currentLine() const {return _currentLine;} - template bool await(const Async &a, int curLine); - template Retained> awaited(); - - static void asyncVoidFn(Actor *actor, std::function &&body); - - protected: - friend class AsyncProviderBase; - template friend class AsyncProvider; - - AsyncFnState(AsyncProviderBase *owningProvider, Actor *owningActor); - - bool _await(const AsyncBase &a, int curLine); - void updateFrom(AsyncFnState&); - - Retained _owningProvider; // Provider that I belong to - Retained _owningActor; // Actor (if any) that owns the async method - Retained _awaiting; // Provider my fn body is suspended awaiting - int _currentLine {0}; // label/line# to continue body function at - }; + // *** For full documentation, read Networking/BLIP/docs/Async.md *** // Interface for observing when an Async value becomes available. @@ -125,14 +49,13 @@ namespace litecore::actor { // Maintains the context/state of an async operation and its observer. // Abstract base class of AsyncProvider class AsyncProviderBase : public RefCounted, - protected AsyncObserver, public fleece::InstanceCountedIn { public: bool ready() const {return _ready;} template T& result(); - template T&& extractResult(); + template T extractResult(); /// Returns the exception result, else nullptr. std::exception_ptr exception() const {return _exception;} @@ -149,19 +72,16 @@ namespace litecore::actor { friend class AsyncBase; explicit AsyncProviderBase(bool ready = false); - explicit AsyncProviderBase(Actor *actorOwningFn); + explicit AsyncProviderBase(Actor *actorOwningFn, const char *functionName); ~AsyncProviderBase(); - void _start(); - void _wait(); void _gotResult(std::unique_lock&); std::mutex mutable _mutex; - std::atomic _ready {false}; // True when result is ready - std::exception_ptr _exception {nullptr}; // Exception if provider failed - std::unique_ptr _fnState; // State of associated async fn private: - AsyncObserver* _observer = nullptr; // AsyncObserver waiting on me - Retained _observerActor; // Actor the observer was running on + AsyncObserver* _observer {nullptr}; // AsyncObserver waiting on me + Retained _observerActor; // Actor the observer was running on + std::exception_ptr _exception {nullptr}; // Exception if provider failed + std::atomic _ready {false}; // True when result is ready }; @@ -225,57 +145,44 @@ namespace litecore::actor { } } - /// Returns the result, which must be available. + private: + friend class AsyncProviderBase; + friend class Async; + + explicit AsyncProvider(T&& result) + :AsyncProviderBase(true) + ,_result(std::move(result)) + { } + T& result() & { std::unique_lock _lock(_mutex); rethrowException(); precondition(_result); return *_result; } - - T&& result() && { + + T result() && { return extractResult(); } - /// Moves the result to the caller. Result must be available. - T&& extractResult() { + T extractResult() { std::unique_lock _lock(_mutex); rethrowException(); precondition(_result); return *std::move(_result); } - private: - friend class Async; - - using Body = std::function(AsyncFnState&)>; - - explicit AsyncProvider(T&& result) - :AsyncProviderBase(true) - ,_result(std::move(result)) - { } - - AsyncProvider(Actor *actor, Body &&body) - :AsyncProviderBase(actor) - ,_fnBody(std::move(body)) - { } - - void asyncResultAvailable(Retained async) override { - assert(async == _fnState->_awaiting); - std::optional r; + template + Async _now(std::function(T&&)> &callback) { + if (auto x = exception()) + return Async(x); try { - r = _fnBody(*_fnState); - } catch(const std::exception &x) { - setException(std::current_exception()); - return; + return callback(extractResult()); + } catch (...) { + return Async(std::current_exception()); } - if (r) - setResult(*std::move(r)); - else - _wait(); } - Body _fnBody; // The async function body, if any std::optional _result; // My result }; @@ -301,22 +208,26 @@ namespace litecore::actor { // base class of Async class AsyncBase { public: + /// Sets which Actor the callback of a `then` call should run on. + AsyncBase& on(Actor *actor) {_onActor = actor; return *this;} + /// Returns true once the result is available. bool ready() const {return _provider->ready();} - /// Blocks the current thread (i.e. doesn't return) until the result is available. - /// Please don't use this unless absolutely necessary; use `then()` or `AWAIT()` instead. - void blockUntilReady(); - /// Returns the exception result, else nullptr. std::exception_ptr exception() const {return _provider->exception();} + /// Blocks the current thread (i.e. doesn't return) until the result is available. + /// \warning This is intended for use in unit tests. Please don't use it otherwise unless + /// absolutely necessary; use `then()` or `AWAIT()` instead. + void blockUntilReady(); + protected: - friend class AsyncFnState; - explicit AsyncBase(Retained &&context) :_provider(std::move(context)) { } - explicit AsyncBase(AsyncProviderBase *context, bool); // calls context->_start() + explicit AsyncBase(Retained &&provider) :_provider(std::move(provider)) { } + bool canCallNow() const; - Retained _provider; // The provider that owns my value + Retained _provider; // The provider that owns my value + Actor* _onActor {nullptr}; // Actor that `then` should call on }; @@ -332,23 +243,18 @@ namespace litecore::actor { Async(Retained> &&provider) :AsyncBase(std::move(provider)) { } /// Creates an already-resolved Async with a value. - explicit Async(T&& t) + Async(T&& t) :AsyncBase(AsyncProvider::createReady(std::forward(t))) { } - // (used by `BEGIN_ASYNC_RETURNING(T)`. Don't call directly.) - Async(Actor *actor, typename AsyncProvider::Body &&bodyFn) - :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) - { } - - /// Returns the result. (Will abort if the result is not yet available.) - T& result() & {return _provider->result();} - T&& result() && {return _provider->result();} - - /// Move-returns the result. (Will abort if the result is not yet available.) - T&& extractResult() {return _provider->extractResult();} + /// Creates an already-resolved Async with an exception. + explicit Async(std::exception_ptr x) + :AsyncBase(makeProvider()) + { + _provider->setException(x); + } - /// Invokes the callback when the result becomes ready (immediately if it's already ready.) + /// Invokes the callback when the result is ready. /// The callback should take a single parameter of type `T`, `T&` or `T&&`. /// The callback's return type may be: /// - `void` -- the `then` method will return `void`. @@ -358,31 +264,43 @@ namespace litecore::actor { /// returns, _and_ its returned async value becomes ready, the returned /// async value will resolve to that value. /// + /// By default the callback will be invoked either on the thread that set the result, + /// or if the result is already available, synchronously on the current thread + /// (before `then` returns.) + /// + /// If an Actor method is calling `then`, it should first call `on(this)` to specify that + /// it wants the callback to run on its event queue; e.g. `a.on(this).then(...)`. + /// /// Examples: /// - `a.then([](T) -> void { ... });` /// - `Async x = a.then([](T) -> X { ... });` /// - `Async x = a.then([](T) -> Async { ... });` - template - auto then(Actor *onActor, LAMBDA &&callback) && { - using U = unwrap_async>; // return type w/o Async<> - return _then(onActor, std::forward(callback)); - } - template auto then(LAMBDA &&callback) && { using U = unwrap_async>; // return type w/o Async<> - return _then(nullptr, std::forward(callback)); + return _then(std::forward(callback)); } + /// Returns the result. (Throws an exception if the result is not yet available.) + /// If the result contains an exception, throws that exception. + T& result() & {return _provider->result();} + T result() && {return _provider->result();} + + /// Move-returns the result. (Throws an exception if the result is not yet available.) + /// If the result contains an exception, throws that exception. + T extractResult() {return _provider->extractResult();} + /// Blocks the current thread until the result is available, then returns it. - /// Please don't use this unless absolutely necessary; use `then()` or `AWAIT()` instead. + /// If the result contains an exception, throws that exception. + /// \warning This is intended for use in unit tests. Please don't use it otherwise unless + /// absolutely necessary; use `then()` or `AWAIT()` instead. T& blockingResult() { blockUntilReady(); return result(); } using ResultType = T; - using AwaitReturnType = Async; + using ThenReturnType = Async; private: class Waiter; // defined below @@ -393,10 +311,10 @@ namespace litecore::actor { // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. template - typename Async::AwaitReturnType - _then(Actor *onActor, std::function &&callback) { + typename Async::ThenReturnType + _then(std::function &&callback) { auto uProvider = Async::makeProvider(); - if (ready()) { + if (canCallNow()) { // Result is available now, so call the callback: if (auto x = exception()) uProvider->setException(x); @@ -404,8 +322,12 @@ namespace litecore::actor { uProvider->setResultFromCallback([&]{return callback(extractResult());}); } else { // Create an AsyncWaiter to wait on the provider: - Waiter::start(this->provider(), onActor, [uProvider,callback](T&& result) { - uProvider->setResultFromCallback([&]{return callback(std::forward(result));}); + Waiter::start(this->provider(), _onActor, [uProvider,callback](auto &provider) { + if (auto x = provider.exception()) + uProvider->setException(x); + else { + uProvider->setResultFromCallback([&]{return callback(provider.extractResult());}); + } }); } return uProvider->asyncValue(); @@ -413,25 +335,28 @@ namespace litecore::actor { // Implements `then` where the lambda returns void. (Specialization of above method.) template<> - void _then(Actor *onActor, std::function &&callback) { - if (ready()) + void _then(std::function &&callback) { + if (canCallNow()) callback(extractResult()); else - Waiter::start(provider(), onActor, std::move(callback)); + Waiter::start(provider(), _onActor, [=](auto &provider) { + callback(provider.extractResult()); + }); } // Implements `then` where the lambda returns `Async`. template - Async _then(Actor *onActor, std::function(T&&)> &&callback) { - if (ready()) { + Async _then(std::function(T&&)> &&callback) { + if (canCallNow()) { // If I'm ready, just call the callback and pass on the Async it returns: - return callback(extractResult()); + return provider()->_now(callback); } else { // Otherwise wait for my result... auto uProvider = Async::makeProvider(); - Waiter::start(provider(), onActor, [uProvider,callback=std::move(callback)](T&& result) { + Waiter::start(provider(), _onActor, [=] (auto &provider) mutable { // Invoke the callback, then wait to resolve the Async it returns: - callback(std::move(result)) .then([uProvider](U &&uresult) { + auto asyncU = provider._now(callback); + std::move(asyncU).then([uProvider](U &&uresult) { // Then finally resolve the async I returned: uProvider->setResult(std::forward(uresult)); }); @@ -439,6 +364,7 @@ namespace litecore::actor { return uProvider->asyncValue(); } } + }; @@ -453,19 +379,6 @@ namespace litecore::actor { static inline Actor* thisActor() {return nullptr;} #endif - template - bool AsyncFnState::await(const Async &a, int curLine) { - return _await(a, curLine); - } - - - template - Retained> AsyncFnState::awaited() { - // Move-returns `_awaiting`, downcast to the specific type of AsyncProvider<>. - // The dynamic_cast is a safety check: it will throw a `bad_cast` exception on mismatch. - (void)dynamic_cast&>(*_awaiting); // runtime type-check - return reinterpret_cast>&&>(_awaiting); - } template T& AsyncProviderBase::result() { @@ -473,7 +386,7 @@ namespace litecore::actor { } template - T&& AsyncProviderBase::extractResult() { + T AsyncProviderBase::extractResult() { return dynamic_cast*>(this)->extractResult(); } @@ -482,7 +395,7 @@ namespace litecore::actor { template class Async::Waiter : public AsyncObserver { public: - using Callback = std::function; + using Callback = std::function&)>; static void start(AsyncProvider *provider, Actor *onActor, Callback &&callback) { (void) new Waiter(provider, onActor, std::forward(callback)); @@ -497,7 +410,7 @@ namespace litecore::actor { void asyncResultAvailable(Retained ctx) override { auto provider = dynamic_cast*>(ctx.get()); - _callback(provider->extractResult()); + _callback(*provider); delete this; // delete myself when done! } private: @@ -515,36 +428,11 @@ namespace litecore::actor { private: friend class Async; - friend class AsyncFnState; - - using Body = std::function; - - AsyncProvider(Actor *actor, Body &&body) - :AsyncProviderBase(actor) - ,_body(std::move(body)) - { } - - AsyncProvider(Actor *actor, Body &&body, AsyncFnState &&state) - :AsyncProvider(actor, std::move(body)) - { - _fnState->updateFrom(state); - } void setResult() { std::unique_lock lock(_mutex); _gotResult(lock); } - - void asyncResultAvailable(Retained async) override { - assert(async == _fnState->_awaiting); - _body(*_fnState); - if (_fnState->_awaiting) - _wait(); - else - setResult(); - } - - Body _body; // The async function body }; @@ -552,16 +440,10 @@ namespace litecore::actor { template <> class Async : public AsyncBase { public: - using AwaitReturnType = void; - -// static Retained> makeProvider() {return AsyncProvider::create();} + using ThenReturnType = void; Async(AsyncProvider *provider) :AsyncBase(provider) { } Async(const Retained> &provider) :AsyncBase(provider) { } - - Async(Actor *actor, typename AsyncProvider::Body bodyFn) - :AsyncBase(new AsyncProvider(actor, std::move(bodyFn)), true) - { } }; } diff --git a/LiteCore/Support/AsyncActorCommon.hh b/LiteCore/Support/AsyncActorCommon.hh new file mode 100644 index 000000000..4d85c5586 --- /dev/null +++ b/LiteCore/Support/AsyncActorCommon.hh @@ -0,0 +1,14 @@ +// +// AsyncActorCommon.hh +// +// Copyright © 2022 Couchbase. All rights reserved. +// + +#pragma once +#include + +namespace NAMESPACE { + + + +} diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index e770490eb..95d2c4200 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -55,76 +55,48 @@ class AsyncTest { return bProvider(); } - Async provideSum() { - Log("provideSum: entry"); - string a, b; - BEGIN_ASYNC_RETURNING(string) + Async provideOne() { Log("provideSum: awaiting A"); - XAWAIT(a, provideA()); - Log("provideSum: awaiting B"); - XAWAIT(b, provideB()); - Log("provideSum: returning"); - return a + b; - END_ASYNC() + return provideA().then([=](string a) { + Log("provideSum: awaiting B"); + return a; + }); } - Async provideSumPlus() { - string a; - BEGIN_ASYNC_RETURNING(string) - XAWAIT(a, provideSum()); - return a + "!"; - END_ASYNC() + Async provideSum() { + Log("provideSum: awaiting A"); + return provideA().then([=](string a) { + Log("provideSum: awaiting B"); + return provideB().then([=](string b) { + Log("provideSum: returning"); + return a + b; + }); + }); } - Async XXprovideSumPlus() { - string a; - return Async(thisActor(), [=](AsyncFnState &_async_state_) mutable - -> std::optional { - switch (_async_state_.currentLine()) { - default: - if (_async_state_.await(provideSum(), 78)) return {}; - case 78: - a = _async_state_.awaited>() - ->extractResult(); - return a + "!"; - } + Async provideSumPlus() { + return provideSum().then([=](string a) { + return a + "!"; }); } Async provideImmediately() { - BEGIN_ASYNC_RETURNING(string) - return "immediately"; - END_ASYNC() - } - - - Async provideLoop() { - string n; - int sum = 0; - int i = 0; - BEGIN_ASYNC_RETURNING(int) - for (i = 0; i < 10; i++) { - XAWAIT(n, provideSum()); - //fprintf(stderr, "n=%f, i=%d, sum=%f\n", n, i, sum); - sum += n.size() * i; - } - return sum; - END_ASYNC() + return string("immediately"); } string provideNothingResult; void provideNothing() { - string a, b; - BEGIN_ASYNC() - XAWAIT(a, provideA()); - XAWAIT(b, provideB()); - provideNothingResult = a + b; - END_ASYNC() + provideA().then([=](string a) { + Log("provideSum: awaiting B"); + provideB().then([=](string b) { + provideNothingResult = a + b; + }); + }); } }; @@ -190,21 +162,6 @@ TEST_CASE_METHOD(AsyncTest, "Async, 2 levels", "[Async]") { } -TEST_CASE_METHOD(AsyncTest, "Async, loop", "[Async]") { - Async sum = provideLoop(); - for (int i = 1; i <= 10; i++) { - REQUIRE(!sum.ready()); - _aProvider->setResult("hi"); - _aProvider = nullptr; - REQUIRE(!sum.ready()); - _bProvider->setResult(" there"); - _bProvider = nullptr; - } - REQUIRE(sum.ready()); - REQUIRE(sum.result() == 360); -} - - TEST_CASE_METHOD(AsyncTest, "Async, immediately", "[Async]") { Async im = provideImmediately(); REQUIRE(im.ready()); @@ -273,38 +230,36 @@ class AsyncTestActor : public Actor { AsyncTestActor() :Actor(kC4Cpp_DefaultLog) { } Async download(string url) { - string contents; - BEGIN_ASYNC_RETURNING(string) - CHECK(currentActor() == this); - XAWAIT(contents, downloader(url)); - CHECK(currentActor() == this); - return contents; - END_ASYNC() + return asCurrentActor([=] { + CHECK(currentActor() == this); + return downloader(url).then([=](string contents) -> string { + // When `then` is used inside an Actor method, the lambda is called on its queue: + CHECK(currentActor() == this); + return contents; + }); + }); } Async download(string url1, string url2) { - optional> dl1, dl2; - string contents; - BEGIN_ASYNC_RETURNING(string) - CHECK(currentActor() == this); - dl1 = download(url1); - dl2 = download(url2); - XAWAIT(contents, *dl1); - CHECK(currentActor() == this); - XAWAIT(string contents2, *dl2); - return contents + " and " + contents2; - END_ASYNC() + return asCurrentActor([=] { + CHECK(currentActor() == this); + return download(url1).then([=](string contents1) { + return download(url2).then([=](string contents2) { + CHECK(currentActor() == this); + return contents1 + " and " + contents2; + }); + }); + }); } void testThen(string url) { - BEGIN_ASYNC() - downloader(url).then([=](string &&s) { - // When `then` is used inside an Actor method, the lambda must be called on its queue: - assert(currentActor() == this); - testThenResult = move(s); - testThenReady = true; + asCurrentActor([=] { + downloader(url).then([=](string &&s) { + assert(currentActor() == this); + testThenResult = move(s); + testThenReady = true; + }); }); - END_ASYNC() } atomic testThenReady = false; diff --git a/Networking/BLIP/BLIPConnection.cc b/Networking/BLIP/BLIPConnection.cc index 7ec28a772..29c0c6165 100644 --- a/Networking/BLIP/BLIPConnection.cc +++ b/Networking/BLIP/BLIPConnection.cc @@ -663,29 +663,27 @@ namespace litecore { namespace blip { /** Public API to send a new request. */ - void Connection::sendRequest(MessageBuilder &mb) { - Retained message = new MessageOut(this, mb, MessageNo{0}); + void Connection::sendRequest(BuiltMessage &&mb) { + Retained message = new MessageOut(this, move(mb), MessageNo{0}); DebugAssert(message->type() == kRequestType); send(message); } - Connection::AsyncResponse Connection::sendAsyncRequest(MessageBuilder& builder) { - auto provider = AsyncResponse::makeProvider(); - builder.onProgress = [provider, oldOnProgress=std::move(builder.onProgress)] + Connection::AsyncResponse Connection::sendAsyncRequest(BuiltMessage &&mb) { + auto asyncProvider = AsyncResponse::makeProvider(); + mb.onProgress = [asyncProvider, oldOnProgress=std::move(mb.onProgress)] (MessageProgress progress) { if (progress.state >= MessageProgress::kComplete) - provider->setResult(progress.reply); + asyncProvider->setResult(progress.reply); if (oldOnProgress) oldOnProgress(progress); }; - sendRequest(builder); - return provider; + sendRequest(move(mb)); + return asyncProvider; } - - /** Internal API to send an outgoing message (a request, response, or ACK.) */ void Connection::send(MessageOut *msg) { if (_compressionLevel == 0) diff --git a/Networking/BLIP/BLIPConnection.hh b/Networking/BLIP/BLIPConnection.hh index 1c0012c92..180099d1a 100644 --- a/Networking/BLIP/BLIPConnection.hh +++ b/Networking/BLIP/BLIPConnection.hh @@ -20,6 +20,7 @@ namespace litecore { namespace blip { class BLIPIO; + class BuiltMessage; class ConnectionDelegate; class MessageOut; @@ -65,14 +66,14 @@ namespace litecore { namespace blip { void terminate(); /** Sends a built message as a new request. */ - void sendRequest(MessageBuilder&); + void sendRequest(BuiltMessage&&); using AsyncResponse = actor::Async>; /** Sends a built message as a new request and returns an async value that can be used to get the response when it arrives. @note The response will immediately resolve to `nullptr` if the connection closes. */ - AsyncResponse sendAsyncRequest(MessageBuilder&); + AsyncResponse sendAsyncRequest(BuiltMessage&&); typedef std::function RequestHandler; diff --git a/Networking/BLIP/MessageBuilder.cc b/Networking/BLIP/MessageBuilder.cc index cdbf816c9..4cd5aa58a 100644 --- a/Networking/BLIP/MessageBuilder.cc +++ b/Networking/BLIP/MessageBuilder.cc @@ -51,6 +51,7 @@ namespace litecore { namespace blip { void MessageBuilder::setProfile(slice profile) { + Assert(!isResponse()); addProperty(kProfileProperty, profile); } @@ -139,4 +140,12 @@ namespace litecore { namespace blip { _wroteProperties = false; } + + BuiltMessage::BuiltMessage(MessageBuilder &builder) + :dataSource(std::move(builder.dataSource)) + ,onProgress(move(builder.onProgress)) + ,_flags(builder.flags()) + ,_payload(builder.finish()) + { } + } } diff --git a/Networking/BLIP/MessageBuilder.hh b/Networking/BLIP/MessageBuilder.hh index 3fa825a1f..d9e82fb2c 100644 --- a/Networking/BLIP/MessageBuilder.hh +++ b/Networking/BLIP/MessageBuilder.hh @@ -19,6 +19,8 @@ #include namespace litecore { namespace blip { + class BuiltMessage; + /** A callback to provide data for an outgoing message. When called, it should copy data to the location in the `buf` parameter, with a maximum length of `capacity`. It should @@ -29,7 +31,7 @@ namespace litecore { namespace blip { virtual ~IMessageDataSource() = default; }; - using MessageDataSource = std::unique_ptr; + using MessageDataSource = std::shared_ptr; /** A temporary object used to construct an outgoing message (request or response). @@ -50,6 +52,8 @@ namespace litecore { namespace blip { /** Constructs a MessageBuilder for a response. */ explicit MessageBuilder(MessageIn *inReplyTo); + bool isResponse() const {return type != kRequestType;} + void setProfile(slice profile); /** Adds a property. */ @@ -99,7 +103,7 @@ namespace litecore { namespace blip { protected: friend class MessageIn; - friend class MessageOut; + friend class BuiltMessage; FrameFlags flags() const; alloc_slice finish(); @@ -115,4 +119,22 @@ namespace litecore { namespace blip { bool _wroteProperties {false}; // Have _properties been written to _out yet? }; + + /** Intermediate value produced by a MessageBuilder, to be passed to the Connection. + (Unlike MessageBuilder this class is copyable, so instances can be captured by + `std::function`. That makes it useable by async code.) */ + class BuiltMessage { + public: + BuiltMessage(MessageBuilder&); + + MessageDataSource dataSource; + MessageProgressCallback onProgress; + + protected: + friend class MessageOut; + + FrameFlags _flags; + fleece::alloc_slice _payload; + }; + } } diff --git a/Networking/BLIP/MessageOut.cc b/Networking/BLIP/MessageOut.cc index 988a86d6a..afdb880a2 100644 --- a/Networking/BLIP/MessageOut.cc +++ b/Networking/BLIP/MessageOut.cc @@ -36,6 +36,19 @@ namespace litecore { namespace blip { { } + MessageOut::MessageOut(Connection *connection, + BuiltMessage &&built, + MessageNo number) + :MessageOut(connection, + built._flags, + move(built._payload), + move(built.dataSource), + number) + { + _onProgress = move(built.onProgress); + } + + void MessageOut::nextFrameToSend(Codec &codec, slice_ostream &dst, FrameFlags &outFlags) { outFlags = flags(); if (isAck()) { diff --git a/Networking/BLIP/MessageOut.hh b/Networking/BLIP/MessageOut.hh index c612d6153..696222014 100644 --- a/Networking/BLIP/MessageOut.hh +++ b/Networking/BLIP/MessageOut.hh @@ -33,13 +33,8 @@ namespace litecore { namespace blip { MessageNo number); MessageOut(Connection *connection, - MessageBuilder &builder, - MessageNo number) - :MessageOut(connection, (FrameFlags)0, builder.finish(), std::move(builder.dataSource), number) - { - _flags = builder.flags(); // finish() may update the flags, so set them after - _onProgress = std::move(builder.onProgress); - } + BuiltMessage &&built, + MessageNo number); void dontCompress() {_flags = (FrameFlags)(_flags & ~kCompressed);} void nextFrameToSend(Codec &codec, fleece::slice_ostream &dst, FrameFlags &outFlags); diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md index 391862972..26443cd9b 100644 --- a/Networking/BLIP/docs/Async.md +++ b/Networking/BLIP/docs/Async.md @@ -1,6 +1,6 @@ # The Async API -(Last updated Feb 7 2022 by Jens) +(Last updated Feb 24 2022 by Jens) **Async** is a major extension of LiteCore’s concurrency support, which should help us write clearer and safer multithreaded code in the future. It extends the functionality of Actors: so far, Actor methods have had to return `void` since they’re called asynchronously. Getting a value back from an Actor meant explicitly passing a callback function. @@ -8,19 +8,21 @@ The Async feature allows Actor methods to return values; it’s just that those > If this sounds familiar: yes, this is an implementation of async/await as found in C#, JavaScript, Rust, etc. (C++ itself is getting this feature too, but not until C++20, and even then it’s only half-baked.) -## 1. Asynchronous Values (Futures) +## 1. Asynchronous Values (Futures & Promises) -`Async` represents a value of type `T` that may not be available yet. This concept is also referred to as a ["future"](https://en.wikipedia.org/wiki/Futures_and_promises). You can keep it around like a normal value type, but you can’t get the underlying value until it becomes available. +`Async` represents a value of type `T` that may not be available until some future time. This concept is also referred to as a ["future"](https://en.wikipedia.org/wiki/Futures_and_promises). You can keep it around like a normal value type, but you can’t get the underlying value until it becomes available. -> You can think of `Async` as sort of like `std::optional`, but you can’t store a value in it yourself, only wait until something else (the *producer*) does. +> You can think of `Async` as sort of like `std::optional`, except you can’t store a value in it yourself, only wait until something else does ... but what? -An `Async` has a matching object, `AsyncProvider`, that belongs to whomever is responsible for creating that `T` value. (This is sometimes referred to as a “promise”.) The producer keeps it around, probably in a `Retained<>` wrapper, until such time as the result becomes available, then calls `setResult` on it to store the value into its matching `Async`. +An `Async` has a matching object, `AsyncProvider`, that was created along with it and which belongs to whomever is responsible for producing that `T` value. (This is sometimes referred to as a “promise”.) The producer keeps it around, probably in a `Retained<>` wrapper, until such time as the result becomes available, then calls `setResult` on it to store the value into its matching `Async`. #### Example Here’s a rather dumbed-down example of sending a request to a server and getting a response: ```c++ +static Retained> _curProvider; + Async getIntFromServer() { _curProvider = Async::makeProvider(); sendServerRequest(); @@ -31,10 +33,8 @@ Async getIntFromServer() { Internally, this uses an `AsyncProvider` reference to keep track of the current request (I told you this was dumbed down!), so when the response arrives it can store it into the provider and thereby into the caller’s `Async` value: ```c++ -static Retained> _curProvider; - static void receivedResponseFromServer(int result) { - _curProvider.setResult(result); + _curProvider.setResult(result); _curProvider = nullptr; } ``` @@ -52,25 +52,25 @@ cout << "Server says: " << i << "!\n"; Only … when is “later”, exactly? How do you know? -### Getting The Result With `then` +## 2. Getting The Result With `then` -You can’t call `Async::result()` before the result is available, or Bad Stuff happens, like a fatal exception. We don’t want anything to block; that’s the point of async! +You can’t call `Async::result()` until the result is available, or else Bad Stuff happens, like a fatal exception. We don’t want anything to block; that’s the point of async! There’s a safe `ready()` method that returns `true` after the result is available. But obviously it would be a bad idea to do something like `while (!request.ready()) { }` … -So how do you wait for the result? **You don’t.** Instead you let the Async call you, by calling its `then` method to register a lambda function that will be called with the result when it’s available: +So how do you wait for the result? **You don’t.** Instead you let the Async call _you_, by registering a callback. `Async`'s `then` method takes a lambda function that will be called with the result when it’s available: ```c++ Async request = getIntFromServer(); -request.then([](int i) { +request.then([=](int i) { std::cout << "The result is " << i << "!\n"; }); ``` -What if you need that lambda to return a value? That value won’t be available until later when the lambda runs, but you can get it now as an `Async`: +What if you need that lambda to return a value? That value won’t be available until later when the lambda runs, so it too is returned as an `Async`: ```c++ -Async message = getIntFromServer().then([](int i) { +Async message = getIntFromServer().then([=](int i) { return "The result is " + std::stoi(i) + "!"; }); ``` @@ -80,7 +80,7 @@ This works even if the inner lambda itself returns an `Async`: ```c++ extern Async storeIntOnServer(int); -Async message = getIntFromServer().then([](int i) { +Async status = getIntFromServer().then([=](int i) { return storeIntOnServer(i + 1); }); ``` @@ -88,207 +88,83 @@ Async message = getIntFromServer().then([](int i) { In this situation it can be useful to chain multiple `then` calls: ```c++ -Async message = getIntFromServer().then([](int i) { +Async message = getIntFromServer().then([=](int i) { return storeIntOnServer(i + 1); -}).then([](Status s) { +}).then([=](Status s) { return status == Ok ? "OK!" : "Failure"; }); ``` -## 2. Asynchronous Functions - -An asynchronous function is a function that can resolve `Async` values in a way that *appears* synchronous, but without actually blocking. It lets you write code that looks more linear, without a bunch of “…then…”s in it. The bad news is that it’s reliant on some weird macros that uglify your code a bit. - -An async function always returns an `Async` result, or void, since if an `Async` value it's resolving isn't available, the function itself has to return without (yet) providing a result. - -> This is very much modeled on the "async/await" feature found in many other languages, like C#, JavaScript and Swift. C++20 has it too, under the name "coroutines", but we can't use C++20 yet. - -Here's what an async function looks like: - -```c++ - Async anAsyncFunction() { - BEGIN_ASYNC_RETURNING(T) - ... - return t; - END_ASYNC() - } -``` - -If the function doesn't return a result, and a caller won't need to know when it finishes, it doesn't need a return value at all, and looks like this: +### Be Careful With Captures! -```c++ -void aVoidAsyncFunction() { - BEGIN_ASYNC() - ... - END_ASYNC() -} -``` - -In between `BEGIN_ASYNC` and `END_ASYNC` you can "unwrap" `Async` values, such as those returned by other asynchronous functions, by using the `AWAIT()` macro. The first parameter is the variable to assign the result to, and the second is the expression returning the async result: +It’s worth repeating the usual warnings about lambdas that can be called after the enclosing scope returns: **don’t capture by reference** (don’t use `[&]`) and **don’t capture pointers or `slice`s**. Here C++14’s capture-initializer syntax can be helpful: ```c++ -AWAIT(n, someOtherAsyncFunction()); +slice str = .....; // slices are not safe to capture! +somethingAsync().then([str = alloc_slice(str)] // capture `str` as `alloc_slice` + (int i) { ... }); ``` -This means “call `someOtherAsyncFunction()` [which returns an `Async`], *suspend this function* until that `Async`’s result becomes available, then assign the result to `n`.” - -The weird part is that “suspend” doesn’t actually mean “block the current thread.” Instead it *temporarily returns* from the function (giving the caller an Async value as a placeholder), but when the value becomes available it *resumes* the function where it left off. This reduces the need for multiple threads, and in an Actor it lets you handle other messages while the current one is suspended. - -> TMI: `AWAIT` is a macro that hides some very weird control flow. It first evaluates the second parameter to get its `Async` value. If that value isn't yet available, `AWAIT` _causes the enclosing function to return_. (Obviously it returns an unavailable `Async` value.) It also registers as an observer of the async value, so when its result does become available, the enclosing function _resumes_ right at the line where it left off (🤯), assigns the result to the variable, and continues. If you want even more gory details, see the appendix. +One remaining problem is that **you can’t capture uncopyable types, like unique_ptr**. If you try you’ll get strange error messages from down in the standard library headers. The root problem is that `std::function` is copyable, so it requires that lambdas be copyable, which means their captured values have to be copyable. Facebook’s *folly* library has an alternative [Function](https://github.com/facebook/folly/blob/main/folly/docs/Function.md) class that avoids this problem, so if this turns out to be enough of a headache we could consider adopting that. -### Parameters Of ASYNC functions +## 3. Async and Actors -You need to stay aware of the fact that an async function can be suspended, either in the `BEGIN_ASYNC()` or in an `AWAIT()`. One immediate consequence is that **the function shouldn’t take parameters that are pointers or references**, or things that behave like them (notably `slice`), because their values are likely to be garbage after the function’s been suspended and resumed. The same goes for any local variables in the function that are used across suspension points. +### Running `then` on the Actor’s Thread -| Unsafe | Safe | -| -------------------------- | ------------------------------------ | -| `MyRefCountedClass*` | `Retained` | -| `MyStruct*` or `MyStruct&` | `MyStruct` or `unique_ptr` | -| `slice` | `alloc_slice` | -| `string_view` | `string` | +By default, a `then()` callback is called immediately when the provider’s `setResult()` is called, i.e. on the same thread the provider is running on. -(I feel compelled to admit that it is actually safe to have such parameters … as long as you use them *only before* the `BEGIN_ASYNC` call, not after. This seems like a rare case, though; it might happen in a method that can usually return immediately but only sometimes needs to go the async route.) - -### Variable Scope Inside ASYNC functions - -The weirdness inside `AWAIT()` places some restrictions on your code. Most importantly, **a variable declared between `BEGIN_ASYNC()` and `END_ASYNC()` cannot have a scope that extends across an `AWAIT`**: +But Actors want everything to run on their thread. For that case, `Async` has an `on(Actor*)` method that lets you specify that a subsequent `then()` should schedule its callback on the Actor’s thread. ```c++ -BEGIN_ASYNC() -int foo = ....; -AWAIT(int n, someOtherAsyncFunction()); // ERROR: "Cannot jump from switch..." -``` - -> TMI: This is because the macro expansion of an async function wraps a `switch` statement around its body, and the expansion of `AWAIT()` contains a `case` label. The C++ language does not allow a jump to a `case` to skip past a variable declaration. - -If you want to use a variable across `AWAIT` calls, you must **declare it _before_ the `BEGIN_ASYNC`** -- its scope then includes the entire async function: - -```c++ -int foo; -BEGIN_ASYNC() -... -foo = ....; -AWAIT(int n, someOtherAsyncFunction()); // OK! -foo += n; -``` - -Or if the variable isn't used after the next `AWAIT`, just use braces to limit its scope: - -```c++ -BEGIN_ASYNC() -{ - int foo = ....; +void MyActor::downloadInt() { + getIntFromServer() .on(this) .then([=](int i) { + // This code runs on the Actor's thread + _myInt = i; + }); } -AWAIT(int n, someOtherAsyncFunction()); // OK! ``` -## 3. Threading and Actors - -By default, an async function starts immediately (as you’d expect) and runs until it either returns a value, or blocks in an AWAIT call on an Async value that isn’t ready. In the latter case, it returns to the caller, but its Async result isn’t ready yet. - -When the `Async` value it's waiting for becomes available, the blocked function resumes *immediately*. That means: when the provider's `setResult` method is called, or when the async method returning that value finally returns a result, the waiting method will synchronously resume. +### Implementing Async Actor Methods -### Async and Actors - -These are reasonable behaviors in single- threaded code … not for [Actors](Actors.md), though. An Actors is single-threaded, so code belonging to an Actor should only run “on the Actor’s queue”, i.e. when no other code belonging to that Actor is running. - -Fortunately, `BEGIN_ASYNC` and `AWAIT` are aware of Actors, and have special behaviors when the function they’re used in is a method of an Actor subclass: - -* `BEGIN_ASYNC` checks whether the current thread is already running as that Actor. If not, it doesn’t start yet, but schedules the function on the Actor’s queue. -* When `AWAIT` resumes the function, it schedules it on the Actor's queue. - -### Easier Actors With Async - -One benefit of this is that Actor methods using `BEGIN/END_ASYNC` don’t need the usual idiom where the public method enqueues a call to a matching private method: +A public method of an Actor can be called on any thread, so normally it just enqueues a call to the real method, which is private and (by convention) has an “_” prefix on its name: ```c++ -// The old way ... In Twiddler.hh: -class Twiddler : public Actor { +class MyActor : public Actor { public: - void twiddle(int n) {enqueue(FUNCTION_TO_QUEUE(Twiddler::_twiddle), n);} + void start() { enqueue(FUNCTION_TO_QUEUE(MyActor::_start)); } private: - void _twiddle(int n); -}; - -// The old way ... In Twiddler.cc: -void Twiddler::_twiddle(int n) { - ... actual implementation ... -} + void _start() { /* This code runs on the Actor's thread */ } ``` -Instead, BEGIN_ASYNC lets the public method enqueue itself and return, without the need for a separate method: +A new addition to Actor provides a way to do this without having to have two methods. It also makes it easy to return an Async value from an Actor method. All you do is wrap the body of the method in a lambda passed to `asCurrentActor()`: ```c++ -// The new way ... In Twiddler.hh: -class Twiddler : public Actor { +class MyActor : public Actor { public: - void twiddle(int n); -}; - -// The new way ... In Twiddler.cc: -void Twiddler::twiddle(int n) { - BEGIN_ASYNC() - ... actual implementation ... - END_ASYNC() -} + void start() { + return asCurrentActor([=] { /* This code runs on the Actor's thread */ }); + } + + Async getStatus() { + return asCurrentActor([=] { + // This code runs on the Actor's thread + return _status; + }); + } ``` -## 4. Appendix: How Async Functions Work - -The weird part of async functions is the way the `AWAIT()` macro can *suspend* the function in the middle, and then *resume* it later when the async value is ready. This seems like black magic until you find out hot it works; it’s actually a clever technique for implementing coroutines in C invented by [Simon Tatham](https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html), which is based on an earlier hack called [Duff’s Device](https://en.wikipedia.org/wiki/Duff%27s_device), a weird (ab)use of the `switch` statement. +As a bonus, if `asCurrentActor` is called on the Actor’s thread, it just calls the function immediately without enqueuing it, which is faster. -Let’s look at a simple async function that calls another async function: - -```c++ -Async provideSum(); - -Async provideSumPlus() { - string a; - BEGIN_ASYNC_RETURNING(string) // (a) - Log("Entering provideSumPlus"); - AWAIT(a, provideSum()); // (b) - return a + "!"; - END_ASYNC() // (c) -} -``` +# Exceptions -If we run this through the preprocessor, we get: +Any Async value (regardless of its type parameter) can hold an exception. If the code producing the value fails with an exception, you can catch it and set it as the result with `setException`. ```c++ -Async provideSumPlus() { - string a; - return Async(thisActor(), [=](AsyncState &_async_state_) mutable // (a) - -> std::optional { // - switch (_async_state_.currentLine()) { // - default: // - Log("Entering provideSumPlus"); - if (_async_state_._await(provideSum(), 78)) return {}; // (b) - case 78: // - a = _async_state_.awaited>()// - ->extractResult(); // - return a + "!"; - } // (c) - }); // +try { + ... + provider->setResult(result); +} catch (...) { + provider->setException(std::current_exception()); } ``` -`BEGIN_ASYNC_RETURNING(string)` turns into `return Async(...)`, where the constructor parameters are `thisActor()`, and a lambda that contains the rest of the function wrapped in a `switch` statement. - -`thisActor()` is simple: within the scope of an Actor method, it’s an inline method that returns `this`. Otherwise, it’s a global function that returns `nullptr`. So this call evaluates to the Actor implementing this function/method, if any. - -The `Async` constructor either calls the lambda immediately or (if it’s in an Actor method) schedules it to be called on the Actor’s queue. The function ends up returning this Async instance, whether or not it’s been resolved. - -The really interesting stuff happens in that lambda. It’s passed an `AsyncState` value, which stores some state while the function is suspended; one piece of state is *which line of code it suspended at*, initially 0. - -1. The first thing the lambda does is enter a `switch` on the state’s `currentLine()` value. This will of course jump to the corresponding label. The first time the lambda is called, `currentLine()` is 0, so… -2. The PC jumps to the `default:` label, which is right at the start of the code we wrote. -3. The function writes to the log, then hits the magic AWAIT call, which has turned into: -4. `provideSum()` is called. This returns an `Async` value. -5. We call `_async_state_.await()` and pass it this Async value and the integer 78, which happens to be the current source line, as produced by the `__LINE__` macro. This function stores the value and line number into the AsyncState. The value returned is false if the Async has a result already, true if it doesn’t. Let’s assume the latter, since it’s more interesting. -6. Back in `provideSumPlus()`, the `false` result causes the lambda to return early. The lambda’s return type is `optional`, so the return value is `nullopt`. -7. The caller of the lambda (never mind who) sees that it’s unfinished since it returned `nullopt`. So it gets put into the suspended state. A listener is added to the Async value it’s waiting on, so that when that Async’s result arrives, the lambda will be called again. -8. After the result arrives, the lambda restarts. This time, `_async_state_.currentLine()` returns 78 (stored into it in step 5), so the `switch` statement jumps to `case 78`. -9. The next line is messy due to some template gunk. `_async_state_.awaited<...>()` returns the AsyncProvider of the Async that we were waiting for, and `extractResult()` gets the result from that provider. That value is assigned to our variable `a`. -10. Now we’re back in regular code. We simply append `“!”` to `a` and return that. -11. This time the caller of the lambda gets a real value and knows the function is done. It stuffs that value into the result of the Async it created way at the start (the one the top-level function returned) and notifies listeners that it’s available. -12. At this point the Async value returned by provideSumPlus() has a result, and whatever’s awaiting that result can run. diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 66ff6ed87..c1e561a0a 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -53,19 +53,19 @@ namespace litecore::client { void ConnectedClient::start() { - BEGIN_ASYNC() - logInfo("Connecting..."); - Assert(_status == kC4Stopped); - setStatus(kC4Connecting); - connection().start(); - _selfRetain = this; // retain myself while the connection is open - END_ASYNC(); + asCurrentActor([=] { + logInfo("Connecting..."); + Assert(_status == kC4Stopped); + setStatus(kC4Connecting); + connection().start(); + _selfRetain = this; // retain myself while the connection is open + }); } void ConnectedClient::stop() { - BEGIN_ASYNC() - _disconnect(websocket::kCodeNormal, {}); - END_ASYNC(); + asCurrentActor([=] { + _disconnect(websocket::kCodeNormal, {}); + }); } @@ -88,65 +88,65 @@ namespace litecore::client { void ConnectedClient::onHTTPResponse(int status, const websocket::Headers &headers) { - BEGIN_ASYNC() - logVerbose("Got HTTP response from server, status %d", status); - if (_delegate) - _delegate->clientGotHTTPResponse(this, status, headers); - if (status == 101 && !headers["Sec-WebSocket-Protocol"_sl]) { - gotError(C4Error::make(WebSocketDomain, kWebSocketCloseProtocolError, - "Incompatible replication protocol " - "(missing 'Sec-WebSocket-Protocol' response header)"_sl)); - } - END_ASYNC() + asCurrentActor([=] { + logVerbose("Got HTTP response from server, status %d", status); + if (_delegate) + _delegate->clientGotHTTPResponse(this, status, headers); + if (status == 101 && !headers["Sec-WebSocket-Protocol"_sl]) { + gotError(C4Error::make(WebSocketDomain, kWebSocketCloseProtocolError, + "Incompatible replication protocol " + "(missing 'Sec-WebSocket-Protocol' response header)"_sl)); + } + }); } void ConnectedClient::onConnect() { - BEGIN_ASYNC() + asCurrentActor([=] { logInfo("Connected!"); if (_status != kC4Stopping) // skip this if stop() already called setStatus(kC4Idle); - END_ASYNC() + }); } void ConnectedClient::onClose(Connection::CloseStatus status, Connection::State state) { - BEGIN_ASYNC() - logInfo("Connection closed with %-s %d: \"%.*s\" (state=%d)", - status.reasonName(), status.code, FMTSLICE(status.message), state); + asCurrentActor([=]() mutable { + logInfo("Connection closed with %-s %d: \"%.*s\" (state=%d)", + status.reasonName(), status.code, FMTSLICE(status.message), state); - bool closedByPeer = (_status != kC4Stopping); - setStatus(kC4Stopped); + bool closedByPeer = (_status != kC4Stopping); + setStatus(kC4Stopped); - _connectionClosed(); - - if (status.isNormal() && closedByPeer) { - logInfo("I didn't initiate the close; treating this as code 1001 (GoingAway)"); - status.code = websocket::kCodeGoingAway; - status.message = alloc_slice("WebSocket connection closed by peer"); - } + _connectionClosed(); - static const C4ErrorDomain kDomainForReason[] = {WebSocketDomain, POSIXDomain, - NetworkDomain, LiteCoreDomain}; + if (status.isNormal() && closedByPeer) { + logInfo("I didn't initiate the close; treating this as code 1001 (GoingAway)"); + status.code = websocket::kCodeGoingAway; + status.message = alloc_slice("WebSocket connection closed by peer"); + } - // If this was an unclean close, set my error property: - if (status.reason != websocket::kWebSocketClose || status.code != websocket::kCodeNormal) { - int code = status.code; - C4ErrorDomain domain; - if (status.reason < sizeof(kDomainForReason)/sizeof(C4ErrorDomain)) { - domain = kDomainForReason[status.reason]; - } else { - domain = LiteCoreDomain; - code = kC4ErrorRemoteError; + static const C4ErrorDomain kDomainForReason[] = {WebSocketDomain, POSIXDomain, + NetworkDomain, LiteCoreDomain}; + + // If this was an unclean close, set my error property: + if (status.reason != websocket::kWebSocketClose || status.code != websocket::kCodeNormal) { + int code = status.code; + C4ErrorDomain domain; + if (status.reason < sizeof(kDomainForReason)/sizeof(C4ErrorDomain)) { + domain = kDomainForReason[status.reason]; + } else { + domain = LiteCoreDomain; + code = kC4ErrorRemoteError; + } + gotError(C4Error::make(domain, code, status.message)); } - gotError(C4Error::make(domain, code, status.message)); - } - if (_delegate) - _delegate->clientConnectionClosed(this, status); + if (_delegate) + _delegate->clientConnectionClosed(this, status); - _selfRetain = nullptr; // balances the self-retain in start() - END_ASYNC() + _selfRetain = nullptr; // balances the self-retain in start() + }); } @@ -186,45 +186,42 @@ namespace litecore::client { slice unlessRevID_, bool asFleece) { - BEGIN_ASYNC_RETURNING_CAPTURING(DocResponseOrError, - this, - docID = alloc_slice(docID_), - collectionID = alloc_slice(collectionID_), - unlessRevID = alloc_slice(unlessRevID_), - asFleece) - logInfo("getDoc(\"%.*s\")", FMTSLICE(docID)); + // Not yet running on Actor thread... + logInfo("getDoc(\"%.*s\")", FMTSLICE(docID_)); + alloc_slice docID(docID_); MessageBuilder req("getRev"); req["id"] = docID; - req["ifNotRev"] = unlessRevID; - - AWAIT(Retained, response, sendAsyncRequest(req)); - logInfo("...getDoc got response"); - - if (C4Error err = responseError(response)) - return err; - - DocResponse docResponse { - docID, - alloc_slice(response->property("rev")), - response->body(), - response->boolProperty("deleted") - }; - - if (asFleece && docResponse.body) { - FLError flErr; - docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); - if (!docResponse.body) - return C4Error::make(FleeceDomain, flErr, "Unparseable JSON response from server"); - } - return docResponse; - END_ASYNC() + req["ifNotRev"] = unlessRevID_; + + return sendAsyncRequest(req) + .then([=](Retained response) -> DocResponseOrError { + logInfo("...getDoc got response"); + + if (C4Error err = responseError(response)) + return err; + + DocResponse docResponse { + docID, + alloc_slice(response->property("rev")), + response->body(), + response->boolProperty("deleted") + }; + + if (asFleece && docResponse.body) { + FLError flErr; + docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); + if (!docResponse.body) + return C4Error::make(FleeceDomain, flErr, "Unparseable JSON response from server"); + } + return docResponse; + }); } Async ConnectedClient::getBlob(C4BlobKey blobKey, bool compress) { - BEGIN_ASYNC_RETURNING(BlobOrError) + // Not yet running on Actor thread... auto digest = blobKey.digestString(); logInfo("getAttachment(<%s>)", digest.c_str()); MessageBuilder req("getAttachment"); @@ -232,13 +229,14 @@ namespace litecore::client { if (compress) req["compress"] = "true"; - AWAIT(Retained, response, sendAsyncRequest(req)); - logInfo("...getAttachment got response"); + return sendAsyncRequest(req) + .then([=](Retained response) -> BlobOrError { + logInfo("...getAttachment got response"); - if (C4Error err = responseError(response)) - return err; - return response->body(); - END_ASYNC() + if (C4Error err = responseError(response)) + return err; + return response->body(); + }); } @@ -249,74 +247,68 @@ namespace litecore::client { C4RevisionFlags revisionFlags, slice fleeceData_) { - BEGIN_ASYNC_RETURNING_CAPTURING(C4Error, - this, - docID = alloc_slice(docID_), - collectionID = alloc_slice(collectionID_), - revID = alloc_slice(revID_), - parentRevID = alloc_slice(parentRevID_), - revisionFlags, - fleeceData = alloc_slice(fleeceData_)) - logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID), FMTSLICE(revID)); + // Not yet running on Actor thread... + logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID_), FMTSLICE(revID_)); MessageBuilder req("putRev"); req.compressed = true; - req["id"] = docID; - req["rev"] = revID; - req["history"] = parentRevID; + req["id"] = docID_; + req["rev"] = revID_; + req["history"] = parentRevID_; if (revisionFlags & kRevDeleted) req["deleted"] = "1"; - if (fleeceData.size > 0) { + if (fleeceData_.size > 0) { // TODO: Encryption!! // TODO: Convert blobs to legacy attachments - req.jsonBody().writeValue(FLValue_FromData(fleeceData, kFLTrusted)); + req.jsonBody().writeValue(FLValue_FromData(fleeceData_, kFLTrusted)); } else { req.write("{}"); } - AWAIT(Retained, response, sendAsyncRequest(req)); - logInfo("...putDoc got response"); - - return responseError(response); - END_ASYNC() + return sendAsyncRequest(req) + .then([=](Retained response) -> C4Error { + logInfo("...putDoc got response"); + return responseError(response); + }); } Async ConnectedClient::observeCollection(slice collectionID_, CollectionObserver callback_) { - BEGIN_ASYNC_RETURNING_CAPTURING(C4Error, - this, - collectionID = alloc_slice(collectionID_), - observe = !!callback_, - callback = move(callback_)) - logInfo("observeCollection(%.*s)", FMTSLICE(collectionID)); - - bool sameSubState = (observe == !!_observer); - _observer = move(callback); - if (sameSubState) - return {}; - - MessageBuilder req; - if (observe) { - if (!_registeredChangesHandler) { - registerHandler("changes", &ConnectedClient::handleChanges); - _registeredChangesHandler = true; + return asCurrentActor([this, + collectionID = alloc_slice(collectionID_), + observe = !!callback_, + callback = move(callback_)] () -> Async { + logInfo("observeCollection(%.*s)", FMTSLICE(collectionID)); + + bool sameSubState = (observe == !!_observer); + _observer = move(callback); + if (sameSubState) + return C4Error{}; + + MessageBuilder req; + if (observe) { + if (!_registeredChangesHandler) { + registerHandler("changes", &ConnectedClient::handleChanges); + _registeredChangesHandler = true; + } + req.setProfile("subChanges"); + req["since"] = "NOW"; + req["continuous"] = true; + } else { + req.setProfile("unsubChanges"); } - req.setProfile("subChanges"); - req["since"] = "NOW"; - req["continuous"] = true; - } else { - req.setProfile("unsubChanges"); - } - AWAIT(Retained, response, sendAsyncRequest(req)); - logInfo("...observeCollection got response"); - C4Error error = responseError(response); - if (!error) - _observing = observe; - return error; - END_ASYNC() + return sendAsyncRequest(req) + .then([=](Retained response) -> C4Error { + logInfo("...observeCollection got response"); + C4Error error = responseError(response); + if (!error) + _observing = observe; + return error; + }); + }); } diff --git a/Replicator/Puller.cc b/Replicator/Puller.cc index 07b352633..775a94a34 100644 --- a/Replicator/Puller.cc +++ b/Replicator/Puller.cc @@ -62,68 +62,68 @@ namespace litecore { namespace repl { // Starting an active pull. void Puller::start(RemoteSequence sinceSequence) { - BEGIN_ASYNC(); - _lastSequence = sinceSequence; - _missingSequences.clear(sinceSequence); - alloc_slice sinceStr = _lastSequence.toJSON(); - logInfo("Starting pull from remote seq '%.*s'", SPLAT(sinceStr)); - - Signpost::begin(Signpost::blipSent); - MessageBuilder msg("subChanges"_sl); - if (sinceStr) - msg["since"_sl] = sinceStr; - if (_options->pull == kC4Continuous) - msg["continuous"_sl] = "true"_sl; - msg["batch"_sl] = tuning::kChangesBatchSize; - msg["versioning"] = _db->usingVersionVectors() ? "version-vectors" : "rev-trees"; - if (_skipDeleted) - msg["activeOnly"_sl] = "true"_sl; - if (_options->enableAutoPurge() || progressNotificationLevel() > 0) { - msg["revocations"] = "true"; // Enable revocation notification in "changes" (SG 3.0) - logInfo("msg[\"revocations\"]=\"true\" due to enableAutoPurge()=%d or progressNotificationLevel()=%d > 0", - _options->enableAutoPurge(), progressNotificationLevel()); - } + asCurrentActor([=]() { + _lastSequence = sinceSequence; + _missingSequences.clear(sinceSequence); + alloc_slice sinceStr = _lastSequence.toJSON(); + logInfo("Starting pull from remote seq '%.*s'", SPLAT(sinceStr)); + + Signpost::begin(Signpost::blipSent); + MessageBuilder msg("subChanges"_sl); + if (sinceStr) + msg["since"_sl] = sinceStr; + if (_options->pull == kC4Continuous) + msg["continuous"_sl] = "true"_sl; + msg["batch"_sl] = tuning::kChangesBatchSize; + msg["versioning"] = _db->usingVersionVectors() ? "version-vectors" : "rev-trees"; + if (_skipDeleted) + msg["activeOnly"_sl] = "true"_sl; + if (_options->enableAutoPurge() || progressNotificationLevel() > 0) { + msg["revocations"] = "true"; // Enable revocation notification in "changes" (SG 3.0) + logInfo("msg[\"revocations\"]=\"true\" due to enableAutoPurge()=%d or progressNotificationLevel()=%d > 0", + _options->enableAutoPurge(), progressNotificationLevel()); + } - auto channels = _options->channels(); - if (channels) { - stringstream value; - unsigned n = 0; - for (Array::iterator i(channels); i; ++i) { - slice name = i.value().asString(); - if (name) { - if (n++) - value << ","; - value << name.asString(); + auto channels = _options->channels(); + if (channels) { + stringstream value; + unsigned n = 0; + for (Array::iterator i(channels); i; ++i) { + slice name = i.value().asString(); + if (name) { + if (n++) + value << ","; + value << name.asString(); + } + } + msg["filter"_sl] = "sync_gateway/bychannel"_sl; + msg["channels"_sl] = value.str(); + } else { + slice filter = _options->filter(); + if (filter) { + msg["filter"_sl] = filter; + for (Dict::iterator i(_options->filterParams()); i; ++i) + msg[i.keyString()] = i.value().asString(); } } - msg["filter"_sl] = "sync_gateway/bychannel"_sl; - msg["channels"_sl] = value.str(); - } else { - slice filter = _options->filter(); - if (filter) { - msg["filter"_sl] = filter; - for (Dict::iterator i(_options->filterParams()); i; ++i) - msg[i.keyString()] = i.value().asString(); - } - } - auto docIDs = _options->docIDs(); - if (docIDs) { - auto &enc = msg.jsonBody(); - enc.beginDict(); - enc.writeKey("docIDs"_sl); - enc.writeValue(docIDs); - enc.endDict(); - } - - AWAIT(Retained, reply, sendAsyncRequest(msg)); + auto docIDs = _options->docIDs(); + if (docIDs) { + auto &enc = msg.jsonBody(); + enc.beginDict(); + enc.writeKey("docIDs"_sl); + enc.writeValue(docIDs); + enc.endDict(); + } - if (reply && reply->isError()) { - gotError(reply); - _fatalError = true; - } - Signpost::end(Signpost::blipSent); - END_ASYNC(); + sendAsyncRequest(msg).then([this](Retained reply) { + if (reply && reply->isError()) { + gotError(reply); + _fatalError = true; + } + Signpost::end(Signpost::blipSent); + }); + }); } diff --git a/Replicator/Pusher+Attachments.cc b/Replicator/Pusher+Attachments.cc index e49015ade..63671266e 100644 --- a/Replicator/Pusher+Attachments.cc +++ b/Replicator/Pusher+Attachments.cc @@ -125,7 +125,7 @@ namespace litecore::repl { if (progressNotificationLevel() >= 2) repl->onBlobProgress(progress); - reply.dataSource = make_unique(this, move(blob), progress); + reply.dataSource = make_shared(this, move(blob), progress); req->respond(reply); } diff --git a/Replicator/Pusher+Revs.cc b/Replicator/Pusher+Revs.cc index 2fed9df70..4f95cdcbb 100644 --- a/Replicator/Pusher+Revs.cc +++ b/Replicator/Pusher+Revs.cc @@ -47,9 +47,9 @@ namespace litecore::repl { // Creates a revision message from a RevToSend. Returns a BLIP error code. bool Pusher::buildRevisionMessage(RevToSend *request, - MessageBuilder &msg, - slice ifNotRevID, - C4Error *outError) + MessageBuilder &msg, + slice ifNotRevID, + C4Error *outError) { // Get the document & revision: C4Error c4err = {}; @@ -103,6 +103,8 @@ namespace litecore::repl { msg["rev"_sl] = fullRevID; msg["sequence"_sl] = uint64_t(request->sequence); if (root) { + if (!msg.isResponse()) + msg.setProfile("rev"); if (request->noConflicts) msg["noconflicts"_sl] = true; auto revisionFlags = doc->selectedRev().flags; @@ -161,7 +163,6 @@ namespace litecore::repl { MessageBuilder msg; C4Error c4err; if (buildRevisionMessage(request, msg, {}, &c4err)) { - msg.setProfile("rev"); logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", SPLAT(request->docID), SPLAT(request->revID)); sendRequest(msg, [this, request](MessageProgress progress) { diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index 721df7a29..e4cd1b86e 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -241,7 +241,6 @@ namespace litecore { namespace repl { bool const proposedChanges = _proposeChanges; auto changes = make_shared(move(in_changes)); - BEGIN_ASYNC() MessageBuilder req(proposedChanges ? "proposeChanges"_sl : "changes"_sl); if(proposedChanges) { req[kConflictIncludesRevProperty] = "true"_sl; @@ -291,76 +290,75 @@ namespace litecore { namespace repl { increment(_changeListsInFlight); //---- SEND REQUEST AND WAIT FOR REPLY ---- - AWAIT(Retained, reply, sendAsyncRequest(req)); - if (!reply) - return; + sendAsyncRequest(req).then([=](Retained reply) { + if (!reply) + return; - // Got reply to the "changes" or "proposeChanges": - if (!changes->empty()) { - logInfo("Got response for %zu local changes (sequences from %" PRIu64 ")", - changes->size(), (uint64_t)changes->front()->sequence); - } - decrement(_changeListsInFlight); - _changesFeed.setFindForeignAncestors(getForeignAncestors()); - if (!proposedChanges && reply->isError()) { - auto err = reply->getError(); - if (err.code == 409 && (err.domain == kBLIPErrorDomain || err.domain == "HTTP"_sl)) { - if (!_proposeChanges && !_proposeChangesKnown) { - // Caller is in no-conflict mode, wants 'proposeChanges' instead; retry - logInfo("Server requires 'proposeChanges'; retrying..."); - _proposeChanges = true; - _changesFeed.setFindForeignAncestors(getForeignAncestors()); - sendChanges(move(*changes)); - } else { - logError("Server does not allow '%s'; giving up", - (_proposeChanges ? "proposeChanges" : "changes")); - for(RevToSend* change : *changes) - doneWithRev(change, false, false); - gotError(C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, - "Incompatible with server replication protocol (changes)"_sl)); + // Got reply to the "changes" or "proposeChanges": + if (!changes->empty()) { + logInfo("Got response for %zu local changes (sequences from %" PRIu64 ")", + changes->size(), (uint64_t)changes->front()->sequence); + } + decrement(_changeListsInFlight); + _changesFeed.setFindForeignAncestors(getForeignAncestors()); + if (!proposedChanges && reply->isError()) { + auto err = reply->getError(); + if (err.code == 409 && (err.domain == kBLIPErrorDomain || err.domain == "HTTP"_sl)) { + if (!_proposeChanges && !_proposeChangesKnown) { + // Caller is in no-conflict mode, wants 'proposeChanges' instead; retry + logInfo("Server requires 'proposeChanges'; retrying..."); + _proposeChanges = true; + _changesFeed.setFindForeignAncestors(getForeignAncestors()); + sendChanges(move(*changes)); + } else { + logError("Server does not allow '%s'; giving up", + (_proposeChanges ? "proposeChanges" : "changes")); + for(RevToSend* change : *changes) + doneWithRev(change, false, false); + gotError(C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, + "Incompatible with server replication protocol (changes)"_sl)); + } + return; } - return; } - } - _proposeChangesKnown = true; - - // Request another batch of changes from the db: - maybeGetMoreChanges(); - - if (reply->isError()) { - for(RevToSend* change : *changes) - doneWithRev(change, false, false); - gotError(reply); - return; - } + _proposeChangesKnown = true; - // OK, now look at the successful response: - int maxHistory = (int)max(1l, reply->intProperty("maxHistory"_sl, - tuning::kDefaultMaxHistory)); - bool legacyAttachments = !reply->boolProperty("blobs"_sl); - if (!_deltasOK && reply->boolProperty("deltas"_sl) - && !_options->properties[kC4ReplicatorOptionDisableDeltas].asBool()) - _deltasOK = true; + // Request another batch of changes from the db: + maybeGetMoreChanges(); - // The response body consists of an array that parallels the `changes` array I sent: - Array::iterator iResponse(reply->JSONBody().asArray()); - for (RevToSend *change : *changes) { - change->maxHistory = maxHistory; - change->legacyAttachments = legacyAttachments; - change->deltaOK = _deltasOK; - bool queued = proposedChanges ? handleProposedChangeResponse(change, *iResponse) - : handleChangeResponse(change, *iResponse); - if (queued) { - logVerbose("Queueing rev '%.*s' #%.*s (seq #%" PRIu64 ") [%zu queued]", - SPLAT(change->docID), SPLAT(change->revID), (uint64_t)change->sequence, - _revQueue.size()); + if (reply->isError()) { + for(RevToSend* change : *changes) + doneWithRev(change, false, false); + gotError(reply); + return; } - if (iResponse) - ++iResponse; - } - maybeSendMoreRevs(); - END_ASYNC() + // OK, now look at the successful response: + int maxHistory = (int)max(1l, reply->intProperty("maxHistory"_sl, + tuning::kDefaultMaxHistory)); + bool legacyAttachments = !reply->boolProperty("blobs"_sl); + if (!_deltasOK && reply->boolProperty("deltas"_sl) + && !_options->properties[kC4ReplicatorOptionDisableDeltas].asBool()) + _deltasOK = true; + + // The response body consists of an array that parallels the `changes` array I sent: + Array::iterator iResponse(reply->JSONBody().asArray()); + for (RevToSend *change : *changes) { + change->maxHistory = maxHistory; + change->legacyAttachments = legacyAttachments; + change->deltaOK = _deltasOK; + bool queued = proposedChanges ? handleProposedChangeResponse(change, *iResponse) + : handleChangeResponse(change, *iResponse); + if (queued) { + logVerbose("Queueing rev '%.*s' #%.*s (seq #%" PRIu64 ") [%zu queued]", + SPLAT(change->docID), SPLAT(change->revID), (uint64_t)change->sequence, + _revQueue.size()); + } + if (iResponse) + ++iResponse; + } + maybeSendMoreRevs(); + }); } diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index 5bf570535..8bca3b86b 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -531,8 +531,6 @@ namespace litecore { namespace repl { // Get the remote checkpoint, after we've got the local one and the BLIP connection is up. void Replicator::getRemoteCheckpoint(bool refresh) { - BEGIN_ASYNC() - if (_remoteCheckpointRequested) return; // already in progress if (!_remoteCheckpointDocID) @@ -552,41 +550,40 @@ namespace litecore { namespace repl { if (!refresh && !_hadLocalCheckpoint) startReplicating(); - AWAIT(Retained, response, sendAsyncRequest(msg)); - - // ...after the checkpoint is received: - Signpost::end(Signpost::blipSent); - Checkpoint remoteCheckpoint; + sendAsyncRequest(msg).then([=](Retained response) { + // ...after the checkpoint is received: + Signpost::end(Signpost::blipSent); + Checkpoint remoteCheckpoint; - if (!response) - return; - if (response->isError()) { - auto err = response->getError(); - if (!(err.domain == "HTTP"_sl && err.code == 404)) - return gotError(response); - logInfo("No remote checkpoint '%.*s'", SPLAT(_remoteCheckpointDocID)); - _remoteCheckpointRevID.reset(); - } else { - remoteCheckpoint.readJSON(response->body()); - _remoteCheckpointRevID = response->property("rev"_sl); - logInfo("Received remote checkpoint (rev='%.*s'): %.*s", - SPLAT(_remoteCheckpointRevID), SPLAT(response->body())); - } - _remoteCheckpointReceived = true; + if (!response) + return; + if (response->isError()) { + auto err = response->getError(); + if (!(err.domain == "HTTP"_sl && err.code == 404)) + return gotError(response); + logInfo("No remote checkpoint '%.*s'", SPLAT(_remoteCheckpointDocID)); + _remoteCheckpointRevID.reset(); + } else { + remoteCheckpoint.readJSON(response->body()); + _remoteCheckpointRevID = response->property("rev"_sl); + logInfo("Received remote checkpoint (rev='%.*s'): %.*s", + SPLAT(_remoteCheckpointRevID), SPLAT(response->body())); + } + _remoteCheckpointReceived = true; - if (!refresh && _hadLocalCheckpoint) { - // Compare checkpoints, reset if mismatched: - bool valid = _checkpointer.validateWith(remoteCheckpoint); - if (!valid && _pusher) - _pusher->checkpointIsInvalid(); + if (!refresh && _hadLocalCheckpoint) { + // Compare checkpoints, reset if mismatched: + bool valid = _checkpointer.validateWith(remoteCheckpoint); + if (!valid && _pusher) + _pusher->checkpointIsInvalid(); - // Now we have the checkpoints! Time to start replicating: - startReplicating(); - } + // Now we have the checkpoints! Time to start replicating: + startReplicating(); + } - if (_checkpointJSONToSave) - saveCheckpointNow(); // _saveCheckpoint() was waiting for _remoteCheckpointRevID - END_ASYNC() + if (_checkpointJSONToSave) + saveCheckpointNow(); // _saveCheckpoint() was waiting for _remoteCheckpointRevID + }); } @@ -603,7 +600,6 @@ namespace litecore { namespace repl { void Replicator::saveCheckpointNow() { alloc_slice json = move(_checkpointJSONToSave); - BEGIN_ASYNC() // Switch to the permanent checkpoint ID: alloc_slice checkpointID = _checkpointer.checkpointID(); if (checkpointID != _remoteCheckpointDocID) { @@ -622,44 +618,43 @@ namespace litecore { namespace repl { msg << json; Signpost::begin(Signpost::blipSent); - AWAIT(Retained, response, sendAsyncRequest(msg)); - - Signpost::end(Signpost::blipSent); - if (!response) - return; - else if (response->isError()) { - Error responseErr = response->getError(); - if (responseErr.domain == "HTTP"_sl && responseErr.code == 409) { - // On conflict, read the remote checkpoint to get the real revID: - _checkpointJSONToSave = json; // move() has no effect here - _remoteCheckpointRequested = _remoteCheckpointReceived = false; - getRemoteCheckpoint(true); + sendAsyncRequest(msg).then([this,json](Retained response) { + Signpost::end(Signpost::blipSent); + if (!response) + return; + else if (response->isError()) { + Error responseErr = response->getError(); + if (responseErr.domain == "HTTP"_sl && responseErr.code == 409) { + // On conflict, read the remote checkpoint to get the real revID: + _checkpointJSONToSave = json; // move() has no effect here + _remoteCheckpointRequested = _remoteCheckpointReceived = false; + getRemoteCheckpoint(true); + } else { + gotError(response); + warn("Failed to save remote checkpoint!"); + // If the checkpoint didn't save, something's wrong; but if we don't mark it as + // saved, the replicator will stay busy (see computeActivityLevel, line 169). + _checkpointer.saveCompleted(); + } } else { - gotError(response); - warn("Failed to save remote checkpoint!"); - // If the checkpoint didn't save, something's wrong; but if we don't mark it as - // saved, the replicator will stay busy (see computeActivityLevel, line 169). + // Remote checkpoint saved, so update local one: + _remoteCheckpointRevID = response->property("rev"_sl); + logInfo("Saved remote checkpoint '%.*s' as rev='%.*s'", + SPLAT(_remoteCheckpointDocID), SPLAT(_remoteCheckpointRevID)); + + try { + _db->useLocked([&](C4Database *db) { + _db->markRevsSyncedNow(); + _checkpointer.write(db, json); + }); + logInfo("Saved local checkpoint '%.*s': %.*s", + SPLAT(_remoteCheckpointDocID), SPLAT(json)); + } catch (...) { + gotError(C4Error::fromCurrentException()); + } _checkpointer.saveCompleted(); } - } else { - // Remote checkpoint saved, so update local one: - _remoteCheckpointRevID = response->property("rev"_sl); - logInfo("Saved remote checkpoint '%.*s' as rev='%.*s'", - SPLAT(_remoteCheckpointDocID), SPLAT(_remoteCheckpointRevID)); - - try { - _db->useLocked([&](C4Database *db) { - _db->markRevsSyncedNow(); - _checkpointer.write(db, json); - }); - logInfo("Saved local checkpoint '%.*s': %.*s", - SPLAT(_remoteCheckpointDocID), SPLAT(json)); - } catch (...) { - gotError(C4Error::fromCurrentException()); - } - _checkpointer.saveCompleted(); - } - END_ASYNC() + }); } diff --git a/Replicator/Worker.cc b/Replicator/Worker.cc index d1598910b..f1fc73116 100644 --- a/Replicator/Worker.cc +++ b/Replicator/Worker.cc @@ -126,14 +126,19 @@ namespace litecore { namespace repl { } - Worker::AsyncResponse Worker::sendAsyncRequest(blip::MessageBuilder& builder) { - Assert(isCurrentActor()); - increment(_pendingResponseCount); - builder.onProgress = [=](MessageProgress progress) { - if (progress.state >= MessageProgress::kComplete) - enqueue(FUNCTION_TO_QUEUE(Worker::_endAsyncRequest)); - }; - return connection().sendAsyncRequest(builder); + Worker::AsyncResponse Worker::sendAsyncRequest(blip::BuiltMessage &&built) { + if (isCurrentActor()) { + increment(_pendingResponseCount); + built.onProgress = [=](MessageProgress progress) { + if (progress.state >= MessageProgress::kComplete) + enqueue(FUNCTION_TO_QUEUE(Worker::_endAsyncRequest)); + }; + return connection().sendAsyncRequest(move(built)); + } else { + return asCurrentActor([this, built=move(built)]() mutable { + return sendAsyncRequest(move(built)); + }); + } } diff --git a/Replicator/Worker.hh b/Replicator/Worker.hh index aecd523c6..7829c9be8 100644 --- a/Replicator/Worker.hh +++ b/Replicator/Worker.hh @@ -155,8 +155,9 @@ namespace litecore { namespace repl { using AsyncResponse = actor::Async>; /// Sends a BLIP request, like `sendRequest` but returning the response asynchronously. + /// This method does not need to be called on the Worker's thread. /// Note: The response object will be nullptr if the connection closed. - AsyncResponse sendAsyncRequest(blip::MessageBuilder& builder); + AsyncResponse sendAsyncRequest(blip::BuiltMessage&&); /// The number of BLIP responses I'm waiting for. int pendingResponseCount() const {return _pendingResponseCount;} diff --git a/Xcode/LiteCore.xcodeproj/xcshareddata/xcschemes/LiteCore C++ Tests.xcscheme b/Xcode/LiteCore.xcodeproj/xcshareddata/xcschemes/LiteCore C++ Tests.xcscheme index 9f0be23a6..20f3bf84a 100644 --- a/Xcode/LiteCore.xcodeproj/xcshareddata/xcschemes/LiteCore C++ Tests.xcscheme +++ b/Xcode/LiteCore.xcodeproj/xcshareddata/xcschemes/LiteCore C++ Tests.xcscheme @@ -65,7 +65,26 @@ argument = "--break" isEnabled = "YES"> + + + + + + + + + + Date: Thu, 24 Feb 2022 17:31:16 -0800 Subject: [PATCH 25/78] Async: Improved error/exception handling --- C/c4Error.cc | 25 ++++-- LiteCore/Support/Actor.hh | 25 ++---- LiteCore/Support/Async.cc | 34 ++++++-- LiteCore/Support/Async.hh | 105 ++++++++++++----------- LiteCore/Support/AsyncActorCommon.hh | 23 ++++- LiteCore/Support/Error.hh | 3 + LiteCore/tests/AsyncTest.cc | 33 +++++++ Networking/BLIP/docs/Async.md | 19 +++- Xcode/LiteCore.xcodeproj/project.pbxproj | 4 +- 9 files changed, 180 insertions(+), 91 deletions(-) diff --git a/C/c4Error.cc b/C/c4Error.cc index 46d670c48..9cb8a5616 100644 --- a/C/c4Error.cc +++ b/C/c4Error.cc @@ -241,15 +241,26 @@ C4Error C4Error::fromCurrentException() noexcept { } +static string getMessage(const C4Error &c4err) { + auto info = ErrorTable::instance().copy(c4err); + return info ? info->message : ""; +} + + +namespace litecore { + // Declared in Error.hh + error::error(const C4Error &c4err) + :error(error::Domain(c4err.domain), c4err.code, getMessage(c4err)) + { + if (auto info = ErrorTable::instance().copy(c4err)) + backtrace = info->backtrace; + } +} + + [[noreturn]] __cold void C4Error::raise() const { - if (auto info = ErrorTable::instance().copy(*this); info) { - error e(error::Domain(domain), code, info->message); - e.backtrace = info->backtrace; - throw e; - } else { - error::_throw(error::Domain(domain), code); - } + throw litecore::error(*this); } diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index 442c9e015..396308bda 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -11,6 +11,7 @@ // #pragma once +#include "AsyncActorCommon.hh" #include "ThreadedMailbox.hh" #include "Logging.hh" #include @@ -29,9 +30,7 @@ #endif -namespace litecore { namespace actor { - class Actor; - template class Async; +namespace litecore::actor { //// Some support code for asynchronize(), from http://stackoverflow.com/questions/42124866 template @@ -62,15 +61,6 @@ namespace litecore { namespace actor { #define FUNCTION_TO_QUEUE(METHOD) #METHOD, &METHOD - namespace { - // Magic template gunk. `unwrap_async` removes a layer of `Async<...>` from a type: - // - `unwrap_async` is `string`. - // - `unwrap_async> is `string`. - template T _unwrap_async(T*); - template T _unwrap_async(Async*); - template using unwrap_async = decltype(_unwrap_async((T*)nullptr)); - } - /** Abstract base actor class. Subclasses should implement their public methods as calls to `enqueue` that pass the parameter values through, and name a matching private implementation method; for example: @@ -174,7 +164,6 @@ namespace litecore { namespace actor { _mailbox.logStats(); } - private: friend class ThreadedMailbox; friend class GCDMailbox; @@ -221,13 +210,9 @@ namespace litecore { namespace actor { Mailbox _mailbox; }; - -#ifndef _THISACTOR_DEFINED -#define _THISACTOR_DEFINED - static inline Actor* thisActor() {return nullptr;} -#endif - #undef ACTOR_BIND_METHOD +#undef ACTOR_BIND_METHOD0 #undef ACTOR_BIND_FN +#undef ACTOR_BIND_FN0 -} } +} diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 8dad20e6e..3bb08fb5d 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -12,6 +12,8 @@ #include "Async.hh" #include "Actor.hh" +#include "c4Error.h" +#include "c4Internal.hh" #include "Logging.hh" #include "betterassert.hh" @@ -79,17 +81,32 @@ namespace litecore::actor { } - void AsyncProviderBase::setException(std::exception_ptr x) { + C4Error AsyncProviderBase::c4Error() const { + return _error ? C4Error::fromException(*_error) : C4Error{}; + } + + + void AsyncProviderBase::setError(const C4Error &c4err) { + precondition(c4err.code != 0); unique_lock lock(_mutex); - precondition(!_exception); - _exception = x; + precondition(!_error); + _error = make_unique(c4err); _gotResult(lock); } - void AsyncProviderBase::rethrowException() const { - if (_exception) - rethrow_exception(_exception); + void AsyncProviderBase::setError(const std::exception &x) { + auto e = litecore::error::convertException(x); + unique_lock lock(_mutex); + precondition(!_error); + _error = make_unique(move(e)); + _gotResult(lock); + } + + + void AsyncProviderBase::throwIfError() const { + if (_error) + throw *_error; } @@ -101,6 +118,11 @@ namespace litecore::actor { } + C4Error AsyncBase::c4Error() const { + return _provider->c4Error(); + } + + // Simple class that observes an AsyncProvider and can block until it's ready. class BlockingObserver : public AsyncObserver { public: diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 72a3f8a38..4d5b89946 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -11,6 +11,8 @@ // #pragma once +#include "AsyncActorCommon.hh" +#include "Error.hh" #include "RefCounted.hh" #include "InstanceCounted.hh" #include @@ -20,6 +22,8 @@ #include #include +struct C4Error; + namespace litecore::actor { using fleece::RefCounted; using fleece::Retained; @@ -57,14 +61,16 @@ namespace litecore::actor { template T& result(); template T extractResult(); - /// Returns the exception result, else nullptr. - std::exception_ptr exception() const {return _exception;} + /// Returns the error/exception result, else nullptr. + litecore::error* error() const {return _error.get();} + C4Error c4Error() const; - /// Sets an exception as the result. This will wake up observers. - void setException(std::exception_ptr); + /// Sets an error as the result. This will wake up observers. + void setError(const std::exception&); + void setError(const C4Error&); - /// If the result is an exception, re-throws it. Else does nothing. - void rethrowException() const; + /// If the result is an error, throws it as an exception. Else does nothing. + void throwIfError() const; void setObserver(AsyncObserver*, Actor* =nullptr); @@ -80,7 +86,7 @@ namespace litecore::actor { private: AsyncObserver* _observer {nullptr}; // AsyncObserver waiting on me Retained _observerActor; // Actor the observer was running on - std::exception_ptr _exception {nullptr}; // Exception if provider failed + std::unique_ptr _error; // Error if provider failed std::atomic _ready {false}; // True when result is ready }; @@ -141,7 +147,7 @@ namespace litecore::actor { } catch (...) { if (!duringCallback) throw; - setException(std::current_exception()); + setError(std::current_exception()); } } @@ -156,7 +162,7 @@ namespace litecore::actor { T& result() & { std::unique_lock _lock(_mutex); - rethrowException(); + throwIfError(); precondition(_result); return *_result; } @@ -167,19 +173,19 @@ namespace litecore::actor { T extractResult() { std::unique_lock _lock(_mutex); - rethrowException(); + throwIfError(); precondition(_result); return *std::move(_result); } template Async _now(std::function(T&&)> &callback) { - if (auto x = exception()) - return Async(x); + if (auto x = error()) + return Async(*x); try { return callback(extractResult()); - } catch (...) { - return Async(std::current_exception()); + } catch (const std::exception &x) { + return Async(error::convertException(x)); } } @@ -190,21 +196,6 @@ namespace litecore::actor { #pragma mark - ASYNC: - /// Compile-time utility that pulls the result type out of an Async type. - /// If `T` is `Async`, or a reference thereto, then `async_result_type` is X. - template - using async_result_type = typename std::remove_reference_t::ResultType; - - namespace { - // Magic template gunk. `unwrap_async` removes a layer of `Async<...>` from a type: - // - `unwrap_async` is `string`. - // - `unwrap_async> is `string`. - template T _unwrap_async(T*); - template T _unwrap_async(Async*); - template using unwrap_async = decltype(_unwrap_async((T*)nullptr)); - } - - // base class of Async class AsyncBase { public: @@ -214,8 +205,10 @@ namespace litecore::actor { /// Returns true once the result is available. bool ready() const {return _provider->ready();} - /// Returns the exception result, else nullptr. - std::exception_ptr exception() const {return _provider->exception();} + /// Returns the error result, else nullptr. + litecore::error* error() const {return _provider->error();} + + C4Error c4Error() const; /// Blocks the current thread (i.e. doesn't return) until the result is available. /// \warning This is intended for use in unit tests. Please don't use it otherwise unless @@ -247,11 +240,17 @@ namespace litecore::actor { :AsyncBase(AsyncProvider::createReady(std::forward(t))) { } - /// Creates an already-resolved Async with an exception. - explicit Async(std::exception_ptr x) + /// Creates an already-resolved Async with an error. + Async(const litecore::error &x) + :AsyncBase(makeProvider()) + { + _provider->setError(x); + } + + Async(const C4Error &err) :AsyncBase(makeProvider()) { - _provider->setException(x); + _provider->setError(err); } /// Invokes the callback when the result is ready. @@ -276,22 +275,33 @@ namespace litecore::actor { /// - `Async x = a.then([](T) -> X { ... });` /// - `Async x = a.then([](T) -> Async { ... });` template + [[nodiscard]] auto then(LAMBDA &&callback) && { using U = unwrap_async>; // return type w/o Async<> return _then(std::forward(callback)); } + void then(std::function callback, std::function errorCallback) && { + Waiter::start(provider(), _onActor, [=](auto &provider) { + if (provider.error()) + errorCallback(provider.c4Error()); + else + callback(provider.extractResult()); + }); + } + + /// Returns the result. (Throws an exception if the result is not yet available.) - /// If the result contains an exception, throws that exception. + /// If the result contains an error, throws that as an exception. T& result() & {return _provider->result();} T result() && {return _provider->result();} /// Move-returns the result. (Throws an exception if the result is not yet available.) - /// If the result contains an exception, throws that exception. + /// If the result contains an error, throws that as an exception. T extractResult() {return _provider->extractResult();} /// Blocks the current thread until the result is available, then returns it. - /// If the result contains an exception, throws that exception. + /// If the result contains an error, throws that as an exception. /// \warning This is intended for use in unit tests. Please don't use it otherwise unless /// absolutely necessary; use `then()` or `AWAIT()` instead. T& blockingResult() { @@ -311,20 +321,21 @@ namespace litecore::actor { // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. template + [[nodiscard]] typename Async::ThenReturnType _then(std::function &&callback) { auto uProvider = Async::makeProvider(); if (canCallNow()) { // Result is available now, so call the callback: - if (auto x = exception()) - uProvider->setException(x); + if (auto x = error()) + uProvider->setError(x); else uProvider->setResultFromCallback([&]{return callback(extractResult());}); } else { // Create an AsyncWaiter to wait on the provider: Waiter::start(this->provider(), _onActor, [uProvider,callback](auto &provider) { - if (auto x = provider.exception()) - uProvider->setException(x); + if (auto x = provider.error()) + uProvider->setError(x); else { uProvider->setResultFromCallback([&]{return callback(provider.extractResult());}); } @@ -346,6 +357,7 @@ namespace litecore::actor { // Implements `then` where the lambda returns `Async`. template + [[nodiscard]] Async _then(std::function(T&&)> &&callback) { if (canCallNow()) { // If I'm ready, just call the callback and pass on the Async it returns: @@ -359,6 +371,8 @@ namespace litecore::actor { std::move(asyncU).then([uProvider](U &&uresult) { // Then finally resolve the async I returned: uProvider->setResult(std::forward(uresult)); + }, [uProvider](C4Error err) { + uProvider->setError(err); }); }); return uProvider->asyncValue(); @@ -371,15 +385,6 @@ namespace litecore::actor { //---- Implementation gunk... -#ifndef _THISACTOR_DEFINED -#define _THISACTOR_DEFINED - // Used by `BEGIN_ASYNC` macros. Returns the lexically enclosing actor instance, else NULL. - // (How? Outside of an Actor method, `thisActor()` refers to the function below. - // In an Actor method, it refers to `Actor::thisActor()`, which returns `this`.) - static inline Actor* thisActor() {return nullptr;} -#endif - - template T& AsyncProviderBase::result() { return dynamic_cast*>(this)->result(); diff --git a/LiteCore/Support/AsyncActorCommon.hh b/LiteCore/Support/AsyncActorCommon.hh index 4d85c5586..f7febc45e 100644 --- a/LiteCore/Support/AsyncActorCommon.hh +++ b/LiteCore/Support/AsyncActorCommon.hh @@ -5,10 +5,29 @@ // #pragma once -#include +#include -namespace NAMESPACE { +namespace litecore::actor { + class Actor; + template class Async; + /** Outside of an Actor method, `thisActor` evaluates to `nullptr`. + (Inside of one, it calls the Actor method `thisActor` that returns `this`.) */ + static inline Actor* thisActor() {return nullptr;} + + + // Compile-time utility that pulls the result type out of an Async type. + // If `T` is `Async`, or a reference thereto, then `async_result_type` is X. + template + using async_result_type = typename std::remove_reference_t::ResultType; + + + // Magic template gunk. `unwrap_async` removes a layer of `Async<...>` from a type: + // - `unwrap_async` is `string`. + // - `unwrap_async> is `string`. + template T _unwrap_async(T*); + template T _unwrap_async(Async*); + template using unwrap_async = decltype(_unwrap_async((T*)nullptr)); } diff --git a/LiteCore/Support/Error.hh b/LiteCore/Support/Error.hh index 58b2362da..4090e7835 100644 --- a/LiteCore/Support/Error.hh +++ b/LiteCore/Support/Error.hh @@ -20,6 +20,8 @@ #undef check +struct C4Error; + namespace fleece { class Backtrace; } @@ -93,6 +95,7 @@ namespace litecore { error (Domain, int code ); error(error::Domain, int code, const std::string &what); explicit error (LiteCoreError e) :error(LiteCore, e) {} + explicit error (const C4Error&); // This is implemented in c4Error.cc error& operator= (const error &e); diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 95d2c4200..39c80916a 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -88,6 +88,16 @@ class AsyncTest { } + Async provideError() { + return provideA().then([](string a) -> Async { + if (a.empty()) + return C4Error::make(LiteCoreDomain, kC4ErrorInvalidParameter, "Empty!"); + else + return a; + }); + } + + string provideNothingResult; void provideNothing() { @@ -222,6 +232,29 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning async T", "[Async]") { } +TEST_CASE_METHOD(AsyncTest, "Async Error", "[Async]") { + Async r = provideError(); + REQUIRE(!r.ready()); + SECTION("no error") { + _aProvider->setResult("hi"); + REQUIRE(r.ready()); + CHECK(!r.error()); + CHECK(r.c4Error() == C4Error{}); + CHECK(r.result() == "hi"); + } + SECTION("error") { + _aProvider->setResult(""); + REQUIRE(r.ready()); + auto e = r.error(); + REQUIRE(e); + CHECK(e->domain == error::LiteCore); + CHECK(e->code == error::InvalidParameter); + C4Error c4e = r.c4Error(); + CHECK(c4e == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + + #pragma mark - WITH ACTORS: diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md index 26443cd9b..8b948dcc0 100644 --- a/Networking/BLIP/docs/Async.md +++ b/Networking/BLIP/docs/Async.md @@ -155,16 +155,27 @@ public: As a bonus, if `asCurrentActor` is called on the Actor’s thread, it just calls the function immediately without enqueuing it, which is faster. -# Exceptions +# Exceptions & C4Errors -Any Async value (regardless of its type parameter) can hold an exception. If the code producing the value fails with an exception, you can catch it and set it as the result with `setException`. +### Providing an Error Result + +Any Async value (regardless of its type parameter) can resolve to an error instead of a result. You can store one by calling `setError()` on the provider. The parameter can be either a `C4Error` or a `std::exception`. + + If the code producing the value throws an exception, you can catch it and set it as the result with `setError()`. ```c++ try { ... provider->setResult(result); -} catch (...) { - provider->setException(std::current_exception()); +} catch (const std::exception &x) { + provider->setError(x)); } ``` +> Note: `asCurrentActor()` catches exceptions thrown by its lambda and returns them as an error on the returned Async. + +### Handling An Error + +On the receiving side, you can check the error in an Async by calling its `error()` or `c4Error()` methods. + +> **Warning:** If you call `result` and there’s an error, it will be thrown as an exception!) diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index bda8d6dc2..e8eb4c719 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1005,7 +1005,6 @@ 2744B331241854F2005A194D /* WebSocketImpl.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebSocketImpl.cc; sourceTree = ""; }; 2744B332241854F2005A194D /* WebSocketProtocol.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = WebSocketProtocol.hh; sourceTree = ""; }; 2744B334241854F2005A194D /* Codec.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Codec.cc; sourceTree = ""; }; - 2744B335241854F2005A194D /* ActorProperty.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ActorProperty.hh; sourceTree = ""; }; 2744B336241854F2005A194D /* Async.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Async.cc; sourceTree = ""; }; 2744B337241854F2005A194D /* Actor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Actor.cc; sourceTree = ""; }; 2744B338241854F2005A194D /* ThreadedMailbox.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ThreadedMailbox.hh; sourceTree = ""; }; @@ -1636,6 +1635,7 @@ 27FDF13E1DA84EE70087B4E6 /* SQLiteFleeceUtil.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SQLiteFleeceUtil.hh; sourceTree = ""; }; 27FDF1421DAC22230087B4E6 /* SQLiteFunctionsTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SQLiteFunctionsTest.cc; sourceTree = ""; }; 27FDF1A21DAD79450087B4E6 /* LiteCore-dylib_Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "LiteCore-dylib_Release.xcconfig"; sourceTree = ""; }; + 27FF5CAF27C83A8F00CFFA43 /* AsyncActorCommon.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AsyncActorCommon.hh; sourceTree = ""; }; 42B6B0DD25A6A9D9004B20A7 /* URLTransformer.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = URLTransformer.hh; sourceTree = ""; }; 42B6B0E125A6A9D9004B20A7 /* URLTransformer.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = URLTransformer.cc; sourceTree = ""; }; 720EA3F51BA7EAD9002B8416 /* libLiteCore.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libLiteCore.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2081,7 +2081,7 @@ 274C1DC425C4A79C00B0EEAC /* ChannelManifest.hh */, 2744B341241854F2005A194D /* Async.hh */, 2744B336241854F2005A194D /* Async.cc */, - 2744B335241854F2005A194D /* ActorProperty.hh */, + 27FF5CAF27C83A8F00CFFA43 /* AsyncActorCommon.hh */, ); name = Actors; sourceTree = ""; From 6542a0658c99dcdb42489e78b279799d0cc4e874 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 25 Feb 2022 09:47:11 -0800 Subject: [PATCH 26/78] Async: Simplified the way providers notify --- LiteCore/Support/Actor.hh | 2 +- LiteCore/Support/Async.cc | 74 ++++++++++++++++---------------------- LiteCore/Support/Async.hh | 76 ++++++++++----------------------------- 3 files changed, 50 insertions(+), 102 deletions(-) diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index 396308bda..246d75c80 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -167,7 +167,7 @@ namespace litecore::actor { private: friend class ThreadedMailbox; friend class GCDMailbox; - friend class AsyncObserver; + friend class AsyncProviderBase; template friend class ActorBatcher; template friend class ActorCountBatcher; diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 3bb08fb5d..2215478c2 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -23,18 +23,6 @@ namespace litecore::actor { #pragma mark - ASYNC OBSERVER: - void AsyncObserver::notifyAsyncResultAvailable(AsyncProviderBase *ctx, Actor *actor) { - if (actor && actor != Actor::currentActor()) { - // Schedule a call on my Actor: - actor->enqueueOther("AsyncObserver::asyncResultAvailable", this, - &AsyncObserver::asyncResultAvailable, retained(ctx)); - } else { - // ... or call it synchronously: - asyncResultAvailable(ctx); - } - } - - #pragma mark - ASYNC PROVIDER BASE: @@ -49,20 +37,33 @@ namespace litecore::actor { } - void AsyncProviderBase::setObserver(AsyncObserver *o, Actor *actor) { + void AsyncProviderBase::setObserver(Actor *actor, Observer observer) { { unique_lock _lock(_mutex); precondition(!_observer); // Presumably I wasn't ready when the caller decided to call `setObserver` on me; // but I might have become ready in between then and now, so check for that. if (!_ready) { - _observer = o; + _observer = move(observer); _observerActor = actor ? actor : Actor::currentActor(); return; } } // if I am ready, call the observer now: - o->notifyAsyncResultAvailable(this, actor); + notifyObserver(observer, actor); + } + + + void AsyncProviderBase::notifyObserver(Observer &observer, Actor *actor) { + if (actor && actor != Actor::currentActor()) { + // Schedule a call on my Actor: + actor->asCurrentActor([observer, provider=fleece::retained(this)] { + observer(*provider); + }); + } else { + // ... or call it synchronously: + observer(*this); + } } @@ -70,14 +71,15 @@ namespace litecore::actor { // on entry, `_mutex` is locked by `lock` precondition(!_ready); _ready = true; - auto observer = _observer; + auto observer = move(_observer); _observer = nullptr; - auto observerActor = std::move(_observerActor); + auto observerActor = move(_observerActor); + _observerActor = nullptr; lock.unlock(); if (observer) - observer->notifyAsyncResultAvailable(this, observerActor); + notifyObserver(observer, observerActor); } @@ -123,35 +125,19 @@ namespace litecore::actor { } - // Simple class that observes an AsyncProvider and can block until it's ready. - class BlockingObserver : public AsyncObserver { - public: - BlockingObserver(AsyncProviderBase *provider) - :_provider(provider) - { } - - void wait() { - unique_lock lock(_mutex); - _provider->setObserver(this); - _cond.wait(lock, [&]{return _provider->ready();}); - } - private: - void asyncResultAvailable(Retained) override { - unique_lock lock(_mutex); - _cond.notify_one(); - } - - mutex _mutex; - condition_variable _cond; - AsyncProviderBase* _provider; - }; - - void AsyncBase::blockUntilReady() { if (!ready()) { precondition(Actor::currentActor() == nullptr); // would deadlock if called by an Actor - BlockingObserver obs(_provider); - obs.wait(); + mutex _mutex; + condition_variable _cond; + + _provider->setObserver(nullptr, [&](AsyncProviderBase &provider) { + unique_lock lock(_mutex); + _cond.notify_one(); + }); + + unique_lock lock(_mutex); + _cond.wait(lock, [&]{return ready();}); } } diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 4d5b89946..f058717c6 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -37,16 +37,6 @@ namespace litecore::actor { // *** For full documentation, read Networking/BLIP/docs/Async.md *** - // Interface for observing when an Async value becomes available. - class AsyncObserver { - public: - virtual ~AsyncObserver() = default; - void notifyAsyncResultAvailable(AsyncProviderBase*, Actor*); - protected: - virtual void asyncResultAvailable(Retained) =0; - }; - - #pragma mark - ASYNCPROVIDER: @@ -72,7 +62,9 @@ namespace litecore::actor { /// If the result is an error, throws it as an exception. Else does nothing. void throwIfError() const; - void setObserver(AsyncObserver*, Actor* =nullptr); + using Observer = std::function; + + void setObserver(Actor*, Observer); protected: friend class AsyncBase; @@ -81,10 +73,11 @@ namespace litecore::actor { explicit AsyncProviderBase(Actor *actorOwningFn, const char *functionName); ~AsyncProviderBase(); void _gotResult(std::unique_lock&); + void notifyObserver(Observer &observer, Actor *actor); std::mutex mutable _mutex; private: - AsyncObserver* _observer {nullptr}; // AsyncObserver waiting on me + Observer _observer; Retained _observerActor; // Actor the observer was running on std::unique_ptr _error; // Error if provider failed std::atomic _ready {false}; // True when result is ready @@ -282,11 +275,11 @@ namespace litecore::actor { } void then(std::function callback, std::function errorCallback) && { - Waiter::start(provider(), _onActor, [=](auto &provider) { + _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { if (provider.error()) errorCallback(provider.c4Error()); else - callback(provider.extractResult()); + callback(provider.extractResult()); }); } @@ -313,8 +306,6 @@ namespace litecore::actor { using ThenReturnType = Async; private: - class Waiter; // defined below - AsyncProvider* provider() { return (AsyncProvider*)_provider.get(); } @@ -332,12 +323,13 @@ namespace litecore::actor { else uProvider->setResultFromCallback([&]{return callback(extractResult());}); } else { - // Create an AsyncWaiter to wait on the provider: - Waiter::start(this->provider(), _onActor, [uProvider,callback](auto &provider) { + _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { if (auto x = provider.error()) uProvider->setError(x); else { - uProvider->setResultFromCallback([&]{return callback(provider.extractResult());}); + uProvider->setResultFromCallback([&]{ + return callback(provider.extractResult()); + }); } }); } @@ -347,12 +339,13 @@ namespace litecore::actor { // Implements `then` where the lambda returns void. (Specialization of above method.) template<> void _then(std::function &&callback) { - if (canCallNow()) + if (canCallNow()) { callback(extractResult()); - else - Waiter::start(provider(), _onActor, [=](auto &provider) { - callback(provider.extractResult()); + } else { + _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { + callback(provider.extractResult()); }); + } } // Implements `then` where the lambda returns `Async`. @@ -365,9 +358,10 @@ namespace litecore::actor { } else { // Otherwise wait for my result... auto uProvider = Async::makeProvider(); - Waiter::start(provider(), _onActor, [=] (auto &provider) mutable { + _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) mutable { // Invoke the callback, then wait to resolve the Async it returns: - auto asyncU = provider._now(callback); + auto &tProvider = dynamic_cast&>(provider); + auto asyncU = tProvider._now(callback); std::move(asyncU).then([uProvider](U &&uresult) { // Then finally resolve the async I returned: uProvider->setResult(std::forward(uresult)); @@ -396,41 +390,9 @@ namespace litecore::actor { } - // Internal class used by `Async::then()`, above - template - class Async::Waiter : public AsyncObserver { - public: - using Callback = std::function&)>; - - static void start(AsyncProvider *provider, Actor *onActor, Callback &&callback) { - (void) new Waiter(provider, onActor, std::forward(callback)); - } - - protected: - Waiter(AsyncProvider *provider, Actor *onActor, Callback &&callback) - :_callback(std::move(callback)) - { - provider->setObserver(this, onActor); - } - - void asyncResultAvailable(Retained ctx) override { - auto provider = dynamic_cast*>(ctx.get()); - _callback(*provider); - delete this; // delete myself when done! - } - private: - Callback _callback; - }; - - // Specialization of AsyncProvider for use in functions with no return value (void). - // Not used directly, but it's used as part of the implementation of void-returning async fns. template <> class AsyncProvider : public AsyncProviderBase { - public: -// static Retained create() {return new AsyncProvider;} -// AsyncProvider() = default; - private: friend class Async; From e064af4c8af9ba970b46f17e9838b2bea6074f59 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Tue, 1 Mar 2022 17:51:56 -0800 Subject: [PATCH 27/78] Async: Error handling working, whew --- C/c4Error.cc | 14 +- LiteCore/Support/Actor.hh | 4 +- LiteCore/Support/Async.cc | 65 ++-- LiteCore/Support/Async.hh | 338 ++++++++++-------- LiteCore/Support/AsyncActorCommon.hh | 1 + LiteCore/Support/Error.cc | 19 + LiteCore/Support/Error.hh | 1 + LiteCore/Support/Result.hh | 70 ++++ LiteCore/tests/AsyncTest.cc | 82 +++-- Replicator/ConnectedClient/ConnectedClient.cc | 28 +- Replicator/ConnectedClient/ConnectedClient.hh | 33 +- Replicator/Puller.cc | 2 +- Replicator/Pusher.cc | 4 +- Replicator/Replicator.cc | 6 +- Replicator/tests/ConnectedClientTest.cc | 26 +- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 + 16 files changed, 411 insertions(+), 284 deletions(-) create mode 100644 LiteCore/Support/Result.hh diff --git a/C/c4Error.cc b/C/c4Error.cc index 9cb8a5616..71943a9f9 100644 --- a/C/c4Error.cc +++ b/C/c4Error.cc @@ -225,19 +225,7 @@ C4Error C4Error::fromException(const exception &x) noexcept { __cold C4Error C4Error::fromCurrentException() noexcept { - // This rigamarole recovers the current exception being thrown... - auto xp = std::current_exception(); - if (xp) { - try { - std::rethrow_exception(xp); - } catch(const std::exception& x) { - // Now we have the exception, so we can record it in outError: - return C4Error::fromException(x); - } catch (...) { } - } - return ErrorTable::instance().makeError( - LiteCoreDomain, kC4ErrorUnexpectedError, - {"Unknown C++ exception", Backtrace::capture(1)}); + return fromException(error::convertCurrentException()); } diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index 246d75c80..581e5b4ef 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -202,7 +202,9 @@ namespace litecore::actor { Async _asCurrentActor(std::function()> fn) { auto provider = Async::makeProvider(); asCurrentActor([fn,provider] { - fn().then([=](U result) { provider->setResult(std::move(result)); }); + fn().thenProvider([=](AsyncProvider &fnProvider) { + provider->setResult(std::move(fnProvider).result()); + }); }); return provider; } diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 2215478c2..1cbb8207b 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -83,33 +83,33 @@ namespace litecore::actor { } - C4Error AsyncProviderBase::c4Error() const { - return _error ? C4Error::fromException(*_error) : C4Error{}; - } - - - void AsyncProviderBase::setError(const C4Error &c4err) { - precondition(c4err.code != 0); - unique_lock lock(_mutex); - precondition(!_error); - _error = make_unique(c4err); - _gotResult(lock); - } - - - void AsyncProviderBase::setError(const std::exception &x) { - auto e = litecore::error::convertException(x); - unique_lock lock(_mutex); - precondition(!_error); - _error = make_unique(move(e)); - _gotResult(lock); - } - - - void AsyncProviderBase::throwIfError() const { - if (_error) - throw *_error; - } +// C4Error AsyncProviderBase::c4Error() const { +// return _error ? C4Error::fromException(*_error) : C4Error{}; +// } +// +// +// void AsyncProviderBase::setError(const C4Error &c4err) { +// precondition(c4err.code != 0); +// unique_lock lock(_mutex); +// precondition(!_error); +// _error = make_unique(c4err); +// _gotResult(lock); +// } +// +// +// void AsyncProviderBase::setError(const std::exception &x) { +// auto e = litecore::error::convertException(x); +// unique_lock lock(_mutex); +// precondition(!_error); +// _error = make_unique(move(e)); +// _gotResult(lock); +// } +// +// +// void AsyncProviderBase::throwIfError() const { +// if (_error) +// throw *_error; +// } #pragma mark - ASYNC BASE: @@ -120,9 +120,9 @@ namespace litecore::actor { } - C4Error AsyncBase::c4Error() const { - return _provider->c4Error(); - } +// C4Error AsyncBase::c4Error() const { +// return _provider->c4Error(); +// } void AsyncBase::blockUntilReady() { @@ -141,4 +141,9 @@ namespace litecore::actor { } } + + void assertNoAsyncError(C4Error err) { + Assert(!err, "Unexpected error in Async value, %s", err.description().c_str()); + } + } diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index f058717c6..a077c9d61 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -14,6 +14,7 @@ #include "AsyncActorCommon.hh" #include "Error.hh" #include "RefCounted.hh" +#include "Result.hh" #include "InstanceCounted.hh" #include #include @@ -37,6 +38,12 @@ namespace litecore::actor { // *** For full documentation, read Networking/BLIP/docs/Async.md *** + namespace { + struct voidPlaceholder; // Just a hack to avoid `void&` parameters in Async + template struct _ThenType { using type = T; using realType = T; }; + template <> struct _ThenType { using type = voidPlaceholder; }; + } + #pragma mark - ASYNCPROVIDER: @@ -48,20 +55,6 @@ namespace litecore::actor { public: bool ready() const {return _ready;} - template T& result(); - template T extractResult(); - - /// Returns the error/exception result, else nullptr. - litecore::error* error() const {return _error.get();} - C4Error c4Error() const; - - /// Sets an error as the result. This will wake up observers. - void setError(const std::exception&); - void setError(const C4Error&); - - /// If the result is an error, throws it as an exception. Else does nothing. - void throwIfError() const; - using Observer = std::function; void setObserver(Actor*, Observer); @@ -79,7 +72,6 @@ namespace litecore::actor { private: Observer _observer; Retained _observerActor; // Actor the observer was running on - std::unique_ptr _error; // Error if provider failed std::atomic _ready {false}; // True when result is ready }; @@ -88,14 +80,15 @@ namespace litecore::actor { template class AsyncProvider final : public AsyncProviderBase { public: - using ResultType = T; + using ResultType = Result; + using ThenType = typename _ThenType::type; /// Creates a new empty AsyncProvider. static Retained create() {return new AsyncProvider;} /// Creates a new AsyncProvider that already has a result. - static Retained createReady(T&& r) { - return new AsyncProvider(std::forward(r)); + static Retained createReady(ResultType r) { + return new AsyncProvider(std::move(r)); } /// Constructs a new empty AsyncProvider. @@ -104,31 +97,25 @@ namespace litecore::actor { /// Creates the client-side view of the result. Async asyncValue() {return Async(this);} - /// Resolves the value by storing the result and waking any waiting clients. - void setResult(const T &result) { + /// Resolves the value by storing a non-error result and waking any waiting clients. + template >> + void setResult(RESULT &&result) { std::unique_lock lock(_mutex); precondition(!_result); - _result = result; + _result.emplace(std::forward(result)); _gotResult(lock); } - /// Resolves the value by move-storing the result and waking any waiting clients. - void setResult(T &&result) { - std::unique_lock lock(_mutex); - precondition(!_result); - _result = std::forward(result); - _gotResult(lock); - } - - /// Equivalent to `setResult` but constructs the T value directly inside the provider. - template >> - void emplaceResult(Args&&... args) { - std::unique_lock lock(_mutex); - precondition(!_result); - _result.emplace(args...); - _gotResult(lock); - } +// /// Equivalent to `setResult` but constructs the T value directly inside the provider. +// template >> +// void emplaceResult(Args&&... args) { +// std::unique_lock lock(_mutex); +// precondition(!_result); +// _result.emplace(args...); +// _gotResult(lock); +// } template void setResultFromCallback(LAMBDA &&callback) { @@ -140,49 +127,60 @@ namespace litecore::actor { } catch (...) { if (!duringCallback) throw; - setError(std::current_exception()); + setResult(error::convertCurrentException()); } } - private: - friend class AsyncProviderBase; - friend class Async; - - explicit AsyncProvider(T&& result) - :AsyncProviderBase(true) - ,_result(std::move(result)) - { } - - T& result() & { + ResultType& result() & { std::unique_lock _lock(_mutex); - throwIfError(); precondition(_result); return *_result; } - T result() && { - return extractResult(); + const ResultType& result() const & { + return const_cast(this)->result(); + } + + ResultType result() && { + return moveResult(); } - T extractResult() { + ResultType moveResult() { std::unique_lock _lock(_mutex); - throwIfError(); precondition(_result); return *std::move(_result); } + bool hasError() const {return result().isError();} + + C4Error error() const { + if (auto x = result().errorPtr()) + return *x; + else + return {}; + } + + private: + friend class AsyncProviderBase; + friend class Async; + + explicit AsyncProvider(ResultType result) + :AsyncProviderBase(true) + ,_result(std::move(result)) + { } + template - Async _now(std::function(T&&)> &callback) { - if (auto x = error()) - return Async(*x); + Async _now(std::function(ThenType&&)> &callback) { + if (hasError()) + return Async(error()); try { - return callback(extractResult()); + return callback(moveResult().get()); } catch (const std::exception &x) { - return Async(error::convertException(x)); + return Async(C4Error::fromException(x)); } } - std::optional _result; // My result + std::optional _result; // My result }; @@ -198,11 +196,6 @@ namespace litecore::actor { /// Returns true once the result is available. bool ready() const {return _provider->ready();} - /// Returns the error result, else nullptr. - litecore::error* error() const {return _provider->error();} - - C4Error c4Error() const; - /// Blocks the current thread (i.e. doesn't return) until the result is available. /// \warning This is intended for use in unit tests. Please don't use it otherwise unless /// absolutely necessary; use `then()` or `AWAIT()` instead. @@ -221,6 +214,9 @@ namespace litecore::actor { template class Async : public AsyncBase { public: + using ResultType = Result; + using ThenType = typename _ThenType::type; + /// Returns a new AsyncProvider. static Retained> makeProvider() {return AsyncProvider::create();} @@ -228,33 +224,30 @@ namespace litecore::actor { Async(AsyncProvider *provider) :AsyncBase(provider) { } Async(Retained> &&provider) :AsyncBase(std::move(provider)) { } - /// Creates an already-resolved Async with a value. - Async(T&& t) - :AsyncBase(AsyncProvider::createReady(std::forward(t))) + /// Creates an already-resolved Async with a result (success or error.) + Async(ResultType t) + :AsyncBase(AsyncProvider::createReady(std::move(t))) { } + template >> + Async(R &&r) :Async(ResultType(std::forward(r))) { } - /// Creates an already-resolved Async with an error. - Async(const litecore::error &x) - :AsyncBase(makeProvider()) - { - _provider->setError(x); - } - - Async(const C4Error &err) - :AsyncBase(makeProvider()) - { - _provider->setError(err); - } /// Invokes the callback when the result is ready. /// The callback should take a single parameter of type `T`, `T&` or `T&&`. /// The callback's return type may be: - /// - `void` -- the `then` method will return `void`. /// - `X` -- the `then` method will return `Async`, which will resolve to the callback's /// return value after the callback returns. /// - `Async` -- the `then` method will return `Async`. After the callback /// returns, _and_ its returned async value becomes ready, the returned /// async value will resolve to that value. + /// - `void` -- the `then` method will return `Async`, because you've handled the + /// result value (`T`) but not a potential error, so what's left is just the error. + /// You can chain `onError()` to handle the error. + /// + /// If the async operation fails, i.e. the result is an error not a `T`, your callback will + /// not be called. Instead the error is passed on to the `Async` value that was returned by + /// `then()`. /// /// By default the callback will be invoked either on the thread that set the result, /// or if the result is already available, synchronously on the current thread @@ -267,68 +260,133 @@ namespace litecore::actor { /// - `a.then([](T) -> void { ... });` /// - `Async x = a.then([](T) -> X { ... });` /// - `Async x = a.then([](T) -> Async { ... });` - template + template , + typename = std::enable_if_t>> [[nodiscard]] auto then(LAMBDA &&callback) && { - using U = unwrap_async>; // return type w/o Async<> + // U is the return type of the lambda with any `Async<...>` removed + using U = unwrap_async; return _then(std::forward(callback)); } - void then(std::function callback, std::function errorCallback) && { - _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { - if (provider.error()) - errorCallback(provider.c4Error()); - else - callback(provider.extractResult()); + + /// `then()` with a callback that returns nothing (`void`). + /// The callback is called only if the async operation is successful. + /// The `then` method itself returns an `Async` which conveys the success/failure of + /// the operation -- you need to handle that value because you haven't handled the failure + /// case yet. Typically you'd chain an `onError(...)`. + /// + /// Also consider using the `then()` method that takes two parameters for success & failure. + [[nodiscard]] + Async then(std::function &&callback) { + auto result = Async::makeProvider(); + _provider->setObserver(_onActor, [=](AsyncProviderBase &providerBase) { + auto &provider = dynamic_cast&>(providerBase); + if (provider.hasError()) + result->setResult(provider.error()); + else { + callback(provider.moveResult().get()); + result->setResult(Result()); + } + }); + return result->asyncValue(); + } + + + /// Special `then()` for `Async` only. The callback takes a `C4Error` and returns + /// nothing. It's called on sucess or failure; on success, the C4Error will have `code` 0. + void then(std::function &&callback) { + static_assert(std::is_same_v, + "then(C4Error) is only allowed with Async; use onError()"); + _provider->setObserver(_onActor, [=](AsyncProviderBase &providerBase) { + auto &provider = dynamic_cast&>(providerBase); + callback(provider.error()); + }); + } + + + /// Version of `then` that takes _two_ callbacks, one for the result and one for an error. + /// There is a function `assertNoAsyncError` that can be passed as the second parameter if + /// you are certain there will be no error. + template + void then(LAMBDA &&callback, + std::function errorCallback) && + { + std::function fn = std::forward(callback); + _then(std::move(fn), std::move(errorCallback)); + } + + + /// Invokes the callback when the result is ready, but only if it's an error. + /// A successful result is ignored. Normally chained after a `then` call to handle the + /// remaining error condition. + void onError(std::function callback) { + _provider->setObserver(_onActor, [=](AsyncProviderBase &providerBase) { + auto &provider = dynamic_cast&>(providerBase); + if (provider.hasError()) + callback(provider.error()); }); } /// Returns the result. (Throws an exception if the result is not yet available.) /// If the result contains an error, throws that as an exception. - T& result() & {return _provider->result();} - T result() && {return _provider->result();} + ResultType& result() & {return provider()->result();} + ResultType result() && {return provider()->result();} /// Move-returns the result. (Throws an exception if the result is not yet available.) /// If the result contains an error, throws that as an exception. - T extractResult() {return _provider->extractResult();} + ResultType moveResult() {return provider()->moveResult();} /// Blocks the current thread until the result is available, then returns it. /// If the result contains an error, throws that as an exception. /// \warning This is intended for use in unit tests. Please don't use it otherwise unless /// absolutely necessary; use `then()` or `AWAIT()` instead. - T& blockingResult() { + ResultType& blockingResult() { blockUntilReady(); return result(); } - using ResultType = T; - using ThenReturnType = Async; + /// Returns the error result, else nullptr. + C4Error error() const {return provider()->error();} private: - AsyncProvider* provider() { - return (AsyncProvider*)_provider.get(); + friend class Actor; + template friend class Async; + + Async() :AsyncBase(makeProvider()) { } + + AsyncProvider* provider() {return (AsyncProvider*)_provider.get();} + AsyncProvider const* provider() const {return (const AsyncProvider*)_provider.get();} + + + void thenProvider(std::function&)> callback) && { + _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { + callback(dynamic_cast&>(provider)); + }); } + // Implements `then` where the lambda returns a regular type `U`. Returns `Async`. template [[nodiscard]] - typename Async::ThenReturnType - _then(std::function &&callback) { + Async _then(std::function &&callback) { auto uProvider = Async::makeProvider(); if (canCallNow()) { // Result is available now, so call the callback: - if (auto x = error()) - uProvider->setError(x); + if (C4Error x = error()) + uProvider->setResult(x); else - uProvider->setResultFromCallback([&]{return callback(extractResult());}); + uProvider->setResultFromCallback([&]{return callback(moveResult());}); } else { - _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { + _provider->setObserver(_onActor, [=](AsyncProviderBase &providerBase) { + auto provider = dynamic_cast&>(providerBase); if (auto x = provider.error()) - uProvider->setError(x); + uProvider->setError(*x); else { uProvider->setResultFromCallback([&]{ - return callback(provider.extractResult()); + return callback(provider.moveResult()); }); } }); @@ -336,22 +394,11 @@ namespace litecore::actor { return uProvider->asyncValue(); } - // Implements `then` where the lambda returns void. (Specialization of above method.) - template<> - void _then(std::function &&callback) { - if (canCallNow()) { - callback(extractResult()); - } else { - _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) { - callback(provider.extractResult()); - }); - } - } // Implements `then` where the lambda returns `Async`. template [[nodiscard]] - Async _then(std::function(T&&)> &&callback) { + Async _then(std::function(ThenType&&)> &&callback) { if (canCallNow()) { // If I'm ready, just call the callback and pass on the Async it returns: return provider()->_now(callback); @@ -362,55 +409,36 @@ namespace litecore::actor { // Invoke the callback, then wait to resolve the Async it returns: auto &tProvider = dynamic_cast&>(provider); auto asyncU = tProvider._now(callback); - std::move(asyncU).then([uProvider](U &&uresult) { + std::move(asyncU).thenProvider([uProvider](auto &provider) { // Then finally resolve the async I returned: - uProvider->setResult(std::forward(uresult)); - }, [uProvider](C4Error err) { - uProvider->setError(err); + uProvider->setResult(provider.result()); }); }); return uProvider->asyncValue(); } } - }; - - - //---- Implementation gunk... - - - template - T& AsyncProviderBase::result() { - return dynamic_cast*>(this)->result(); - } - - template - T AsyncProviderBase::extractResult() { - return dynamic_cast*>(this)->extractResult(); - } - - - // Specialization of AsyncProvider for use in functions with no return value (void). - template <> - class AsyncProvider : public AsyncProviderBase { - private: - friend class Async; - void setResult() { - std::unique_lock lock(_mutex); - _gotResult(lock); + // innards of the `then()` with two callbacks + template >> + void _then(std::function &&callback, + std::function &&errorCallback) + { + _provider->setObserver(_onActor, [=](AsyncProviderBase &providerBase) { + auto &provider = dynamic_cast&>(providerBase); + if (provider.hasError()) + errorCallback(provider.error()); + else + callback(provider.moveResult().get()); + }); } - }; + }; - // Specialization of Async<> for `void` type; not used directly. - template <> - class Async : public AsyncBase { - public: - using ThenReturnType = void; - Async(AsyncProvider *provider) :AsyncBase(provider) { } - Async(const Retained> &provider) :AsyncBase(provider) { } - }; + /// You can use this as the error-handling callback to `void Async::then(onResult,onError)`. + /// It throws an assertion-failed exception if the C4Error's code is nonzero. + void assertNoAsyncError(C4Error); } diff --git a/LiteCore/Support/AsyncActorCommon.hh b/LiteCore/Support/AsyncActorCommon.hh index f7febc45e..3dff63610 100644 --- a/LiteCore/Support/AsyncActorCommon.hh +++ b/LiteCore/Support/AsyncActorCommon.hh @@ -10,6 +10,7 @@ namespace litecore::actor { class Actor; template class Async; + template class AsyncProvider; /** Outside of an Actor method, `thisActor` evaluates to `nullptr`. diff --git a/LiteCore/Support/Error.cc b/LiteCore/Support/Error.cc index ffe907344..7feb0232f 100644 --- a/LiteCore/Support/Error.cc +++ b/LiteCore/Support/Error.cc @@ -466,6 +466,7 @@ namespace litecore { domain(d), code(getPrimaryCode(d, c)) { + DebugAssert(code != 0); if (sCaptureBacktraces) captureBacktrace(3); } @@ -578,6 +579,24 @@ namespace litecore { } + __cold + error error::convertCurrentException() { + // This rigamarole recovers the current exception being thrown... + auto xp = std::current_exception(); + if (xp) { + try { + std::rethrow_exception(xp); + } catch(const std::exception& x) { + // Now we have the exception, so we can record it in outError: + return convertException(x); + } catch (...) { } + } + auto e = error(error::LiteCore, error::UnexpectedError, "Unknown C++ exception"); + e.captureBacktrace(1); + return e; + } + + __cold bool error::isUnremarkable() const { if (code == 0) diff --git a/LiteCore/Support/Error.hh b/LiteCore/Support/Error.hh index 4090e7835..ab66effe0 100644 --- a/LiteCore/Support/Error.hh +++ b/LiteCore/Support/Error.hh @@ -113,6 +113,7 @@ namespace litecore { exception types like SQLite::Exception. */ static error convertRuntimeError(const std::runtime_error&); static error convertException(const std::exception&); + static error convertCurrentException(); /** Static version of the standard `what` method. */ static std::string _what(Domain, int code) noexcept; diff --git a/LiteCore/Support/Result.hh b/LiteCore/Support/Result.hh new file mode 100644 index 000000000..d1ff79bfc --- /dev/null +++ b/LiteCore/Support/Result.hh @@ -0,0 +1,70 @@ +// +// Result.hh +// +// Copyright © 2022 Couchbase. All rights reserved. +// + +#pragma once +#include +#include +#include + +struct C4Error; + +namespace litecore { + struct error; + + /// Represents the return value of a function that can fail. + /// It contains either a value, or an error. + /// The error type defaults to `liteore::error`. + template + class Result { + public: + /// A `Result` can be constructed from a value or an error. + Result(const VAL &val) noexcept :_result(val) { } + Result(const ERR &err) noexcept :_result(err) { } + Result(VAL &&val) noexcept :_result(std::move(val)) { } + Result(ERR &&err) noexcept :_result(std::move(err)) { } + + bool ok() const noexcept {return _result.index() == 0;} + bool isError() const noexcept {return _result.index() != 0;} + + /// Returns the value. You must test first, as this will fail if there is an error! + VAL& get() & {return *std::get_if<0>(&_result);} + VAL&& get() && {return std::move(*std::get_if<0>(&_result));} + + /// Returns the error. Throws an exception if there is none. + const ERR& error() const noexcept {return *std::get_if<1>(&_result);} + + /// Returns a pointer to the error, or nullptr if there is none. + const ERR* errorPtr() const noexcept {return std::get_if<1>(&_result);} + + private: + std::variant _result; + }; + + + // Specialization of `Result` when there is no value. + // Assumes `ERR` has a default no-error value and can be tested as a bool. + template + class Result { + public: + Result() noexcept :_error{} { } + Result(const ERR &err) noexcept :_error(err) { } + Result(ERR &&err) noexcept :_error(std::move(err)) { } + + bool ok() const noexcept {return !_error;} + bool isError() const noexcept {return !!_error;} + void get() const {precondition(!_error);} + const ERR& error() const noexcept {precondition(_error); return _error;} + const ERR* errorPtr() const noexcept {return _error ? &_error : nullptr;} + + private: + ERR _error; + }; + + + /// A `Result` whose error type is `C4Error`. + template using C4Result = Result; + +} diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 39c80916a..12c74b491 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -26,7 +26,7 @@ static Async downloader(string url) { provider->setResult("Contents of " + url); }); t.detach(); - return provider; + return provider->asyncValue(); } @@ -48,11 +48,11 @@ class AsyncTest { } Async provideA() { - return aProvider(); + return aProvider()->asyncValue(); } Async provideB() { - return bProvider(); + return bProvider()->asyncValue(); } Async provideOne() { @@ -100,11 +100,13 @@ class AsyncTest { string provideNothingResult; - void provideNothing() { - provideA().then([=](string a) { - Log("provideSum: awaiting B"); - provideB().then([=](string b) { + Async provideNothing() { + return provideA().then([=](string a) { + Log("provideNothing: awaiting B"); + return provideB().then([=](string b) -> Async { + Log("provideNothing: got B"); provideNothingResult = a + b; + return C4Error{}; }); }); } @@ -119,7 +121,7 @@ TEST_CASE_METHOD(AsyncTest, "Async", "[Async]") { REQUIRE(!sum.ready()); _bProvider->setResult(" there"); REQUIRE(sum.ready()); - REQUIRE(sum.result() == "hi there"); + REQUIRE(sum.result().get() == "hi there"); } @@ -130,7 +132,7 @@ TEST_CASE_METHOD(AsyncTest, "Async, other order", "[Async]") { REQUIRE(!sum.ready()); aProvider()->setResult("hi"); REQUIRE(sum.ready()); - REQUIRE(sum.result() == "hi there"); + REQUIRE(sum.result().get() == "hi there"); } @@ -138,18 +140,18 @@ TEST_CASE_METHOD(AsyncTest, "Async, emplaceResult") { auto p = Async::makeProvider(); auto v = p->asyncValue(); REQUIRE(!v.ready()); - p->emplaceResult('*', 6); + p->setResult("******"); REQUIRE(v.ready()); - CHECK(v.result() == "******"); + CHECK(v.result().get() == "******"); } TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { Async sum = provideSum(); string result; - move(sum).then([&](string &&s) { + move(sum).then([&](string s) { result = s; - }); + }, assertNoAsyncError); REQUIRE(!sum.ready()); REQUIRE(result == ""); _aProvider->setResult("hi"); @@ -168,14 +170,14 @@ TEST_CASE_METHOD(AsyncTest, "Async, 2 levels", "[Async]") { REQUIRE(!sum.ready()); _bProvider->setResult(" there"); REQUIRE(sum.ready()); - REQUIRE(sum.result() == "hi there!"); + REQUIRE(sum.result().get() == "hi there!"); } TEST_CASE_METHOD(AsyncTest, "Async, immediately", "[Async]") { Async im = provideImmediately(); REQUIRE(im.ready()); - REQUIRE(im.result() == "immediately"); + REQUIRE(im.result().get() == "immediately"); } @@ -194,7 +196,7 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning void", "[Async]") { provideSum().then([&](string &&s) { Log("--Inside then fn; s = \"%s\"", s.c_str()); result = s; - }); + }, assertNoAsyncError); Log("--Providing aProvider"); _aProvider->setResult("hi"); @@ -214,7 +216,7 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning T", "[Async]") { _aProvider->setResult("hi"); Log("--Providing bProvider"); _bProvider->setResult(" there"); - CHECK(size.blockingResult() == 8); + CHECK(size.blockingResult().get() == 8); } @@ -228,7 +230,7 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning async T", "[Async]") { _aProvider->setResult("hi"); Log("--Providing bProvider"); _bProvider->setResult(" there"); - CHECK(dl.blockingResult() == "Contents of hi there"); + CHECK(dl.blockingResult().get() == "Contents of hi there"); } @@ -239,18 +241,38 @@ TEST_CASE_METHOD(AsyncTest, "Async Error", "[Async]") { _aProvider->setResult("hi"); REQUIRE(r.ready()); CHECK(!r.error()); - CHECK(r.c4Error() == C4Error{}); - CHECK(r.result() == "hi"); + CHECK(r.result().get() == "hi"); } SECTION("error") { _aProvider->setResult(""); REQUIRE(r.ready()); - auto e = r.error(); - REQUIRE(e); - CHECK(e->domain == error::LiteCore); - CHECK(e->code == error::InvalidParameter); - C4Error c4e = r.c4Error(); - CHECK(c4e == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + + +TEST_CASE_METHOD(AsyncTest, "Async Error Then", "[Async]") { + optional theStr; + optional theError; + provideError().then([&](string str) -> void { + theStr = str; + }).onError([&](C4Error err) { + theError = err; + }); + REQUIRE(!theStr); + REQUIRE(!theError); + + SECTION("no error") { + _aProvider->setResult("hi"); + CHECK(!theError); + REQUIRE(theStr); + CHECK(*theStr == "hi"); + } + SECTION("error") { + _aProvider->setResult(""); + CHECK(!theStr); + REQUIRE(theError); + CHECK(*theError == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); } } @@ -291,7 +313,7 @@ class AsyncTestActor : public Actor { assert(currentActor() == this); testThenResult = move(s); testThenReady = true; - }); + }, assertNoAsyncError); }); } @@ -302,7 +324,7 @@ class AsyncTestActor : public Actor { TEST_CASE("Async on thread", "[Async]") { auto asyncContents = downloader("couchbase.com"); - string contents = asyncContents.blockingResult(); + string contents = asyncContents.blockingResult().get(); CHECK(contents == "Contents of couchbase.com"); } @@ -310,7 +332,7 @@ TEST_CASE("Async on thread", "[Async]") { TEST_CASE("Async Actor", "[Async]") { auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org"); - string contents = asyncContents.blockingResult(); + string contents = asyncContents.blockingResult().get(); CHECK(contents == "Contents of couchbase.org"); } @@ -318,7 +340,7 @@ TEST_CASE("Async Actor", "[Async]") { TEST_CASE("Async Actor Twice", "[Async]") { auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org", "couchbase.biz"); - string contents = asyncContents.blockingResult(); + string contents = asyncContents.blockingResult().get(); CHECK(contents == "Contents of couchbase.org and Contents of couchbase.biz"); } diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index c1e561a0a..e298f3ccd 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -181,7 +181,7 @@ namespace litecore::client { } - Async ConnectedClient::getDoc(slice docID_, + Async ConnectedClient::getDoc(slice docID_, slice collectionID_, slice unlessRevID_, bool asFleece) @@ -194,7 +194,7 @@ namespace litecore::client { req["ifNotRev"] = unlessRevID_; return sendAsyncRequest(req) - .then([=](Retained response) -> DocResponseOrError { + .then([=](Retained response) -> Async { logInfo("...getDoc got response"); if (C4Error err = responseError(response)) @@ -211,14 +211,14 @@ namespace litecore::client { FLError flErr; docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); if (!docResponse.body) - return C4Error::make(FleeceDomain, flErr, "Unparseable JSON response from server"); + C4Error::raise(FleeceDomain, flErr, "Unparseable JSON response from server"); } return docResponse; }); } - Async ConnectedClient::getBlob(C4BlobKey blobKey, + Async ConnectedClient::getBlob(C4BlobKey blobKey, bool compress) { // Not yet running on Actor thread... @@ -230,9 +230,8 @@ namespace litecore::client { req["compress"] = "true"; return sendAsyncRequest(req) - .then([=](Retained response) -> BlobOrError { + .then([=](Retained response) -> Async { logInfo("...getAttachment got response"); - if (C4Error err = responseError(response)) return err; return response->body(); @@ -240,7 +239,7 @@ namespace litecore::client { } - Async ConnectedClient::putDoc(slice docID_, + Async ConnectedClient::putDoc(slice docID_, slice collectionID_, slice revID_, slice parentRevID_, @@ -266,20 +265,20 @@ namespace litecore::client { } return sendAsyncRequest(req) - .then([=](Retained response) -> C4Error { + .then([=](Retained response) -> Async { logInfo("...putDoc got response"); - return responseError(response); + return Async(responseError(response)); }); } - Async ConnectedClient::observeCollection(slice collectionID_, + Async ConnectedClient::observeCollection(slice collectionID_, CollectionObserver callback_) { return asCurrentActor([this, collectionID = alloc_slice(collectionID_), observe = !!callback_, - callback = move(callback_)] () -> Async { + callback = move(callback_)] () -> Async { logInfo("observeCollection(%.*s)", FMTSLICE(collectionID)); bool sameSubState = (observe == !!_observer); @@ -301,12 +300,9 @@ namespace litecore::client { } return sendAsyncRequest(req) - .then([=](Retained response) -> C4Error { + .then([=](Retained response) { logInfo("...observeCollection got response"); - C4Error error = responseError(response); - if (!error) - _observing = observe; - return error; + return Async(responseError(response)); }); }); } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index e2523f5b2..fdb754531 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -22,13 +22,8 @@ namespace litecore::client { bool deleted; }; - /** Result type of `ConnectedClient::getDoc()` -- either a response or an error. */ - using DocResponseOrError = std::variant; - - /** Result type of `ConnectedClient::getBlob()` -- either blob contents or an error. */ - using BlobOrError = std::variant; - + /** A callback invoked when one or more documents change on the server. */ using CollectionObserver = std::function const&)>; @@ -77,16 +72,16 @@ namespace litecore::client { /// @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. /// @return An async value that, when resolved, contains either a `DocResponse` struct /// or a C4Error. - actor::Async getDoc(slice docID, - slice collectionID, - slice unlessRevID, - bool asFleece = true); + actor::Async getDoc(slice docID, + slice collectionID, + slice unlessRevID, + bool asFleece = true); /// Gets the contents of a blob given its digest. /// @param blobKey The binary digest of the blob. /// @param compress True if the blob should be downloaded in compressed form. /// @return An async value that, when resolved, contains either the blob body or a C4Error. - actor::Async getBlob(C4BlobKey blobKey, + actor::Async getBlob(C4BlobKey blobKey, bool compress); /// Pushes a new document revision to the server. @@ -98,20 +93,20 @@ namespace litecore::client { /// @param revisionFlags Flags of this revision. /// @param fleeceData The document body encoded as Fleece (without shared keys!) /// @return An async value that, when resolved, contains the status as a C4Error. - actor::Async putDoc(slice docID, - slice collectionID, - slice revID, - slice parentRevID, - C4RevisionFlags revisionFlags, - slice fleeceData); + actor::Async putDoc(slice docID, + slice collectionID, + slice revID, + slice parentRevID, + C4RevisionFlags revisionFlags, + slice fleeceData); /// Registers a listener function that will be called when any document is changed. /// @note To cancel, pass a null callback. /// @param collectionID The ID of the collection to observe. /// @param callback The function to call (on an arbitrary background thread!) /// @return An async value that, when resolved, contains the status as a C4Error. - actor::Async observeCollection(slice collectionID, - CollectionObserver callback); + actor::Async observeCollection(slice collectionID, + CollectionObserver callback); // exposed for unit tests: websocket::WebSocket* webSocket() const {return connection().webSocket();} diff --git a/Replicator/Puller.cc b/Replicator/Puller.cc index 775a94a34..3c69cf1c6 100644 --- a/Replicator/Puller.cc +++ b/Replicator/Puller.cc @@ -122,7 +122,7 @@ namespace litecore { namespace repl { _fatalError = true; } Signpost::end(Signpost::blipSent); - }); + }, actor::assertNoAsyncError); }); } diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index e4cd1b86e..111eab7bb 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -290,7 +290,7 @@ namespace litecore { namespace repl { increment(_changeListsInFlight); //---- SEND REQUEST AND WAIT FOR REPLY ---- - sendAsyncRequest(req).then([=](Retained reply) { + sendAsyncRequest(req).then([=](Retained reply) -> void { if (!reply) return; @@ -358,7 +358,7 @@ namespace litecore { namespace repl { ++iResponse; } maybeSendMoreRevs(); - }); + }, actor::assertNoAsyncError); } diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index 8bca3b86b..44c9be25c 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -583,7 +583,7 @@ namespace litecore { namespace repl { if (_checkpointJSONToSave) saveCheckpointNow(); // _saveCheckpoint() was waiting for _remoteCheckpointRevID - }); + }, actor::assertNoAsyncError); } @@ -618,7 +618,7 @@ namespace litecore { namespace repl { msg << json; Signpost::begin(Signpost::blipSent); - sendAsyncRequest(msg).then([this,json](Retained response) { + sendAsyncRequest(msg).then([this,json](Retained response) -> void { Signpost::end(Signpost::blipSent); if (!response) return; @@ -654,7 +654,7 @@ namespace litecore { namespace repl { } _checkpointer.saveCompleted(); } - }); + }, actor::assertNoAsyncError); } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 6a8a5267f..3fc0a38ed 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -82,27 +82,25 @@ class ConnectedClientLoopbackTest : public C4Test, template - T waitForResponse(actor::Async> &asyncResult) { + auto waitForResponse(actor::Async &asyncResult) { asyncResult.blockUntilReady(); Log("++++ Async response available!"); - auto &result = asyncResult.result(); - if (auto err = std::get_if(&result)) - FAIL("Response returned an error " << *err); - return * std::get_if(&result); + if (auto err = asyncResult.error()) + FAIL("Response returned an error " << err); + return asyncResult.result().get(); } template - C4Error waitForErrorResponse(actor::Async> &asyncResult) { + C4Error waitForErrorResponse(actor::Async &asyncResult) { asyncResult.blockUntilReady(); Log("++++ Async response available!"); - auto &result = asyncResult.result(); - const C4Error *err = std::get_if(&result); - if (!*err) - FAIL("Response unexpectedly didn't fail"); - return *err; + auto err = asyncResult.error(); + if (!err) + FAIL("Response did not return an error"); + return err; } @@ -271,13 +269,13 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putRev", "[ConnectedClient]") { C4RevisionFlags{}, docBody); rq1.blockUntilReady(); - REQUIRE(rq1.result() == C4Error()); + REQUIRE(rq1.error() == C4Error()); c4::ref doc1 = c4db_getDoc(db, "0000001"_sl, true, kDocGetCurrentRev, ERROR_INFO()); REQUIRE(doc1); CHECK(doc1->revID == "2-2222"_sl); rq2.blockUntilReady(); - REQUIRE(rq2.result() == C4Error()); + REQUIRE(rq2.error() == C4Error()); c4::ref doc2 = c4db_getDoc(db, "frob"_sl, true, kDocGetCurrentRev, ERROR_INFO()); REQUIRE(doc2); CHECK(doc2->revID == "1-1111"_sl); @@ -300,7 +298,7 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Failure", "[ConnectedClien C4RevisionFlags{}, docBody); rq1.blockUntilReady(); - REQUIRE(rq1.result() == C4Error{LiteCoreDomain, kC4ErrorConflict}); + REQUIRE(rq1.error() == C4Error{LiteCoreDomain, kC4ErrorConflict}); } diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index e8eb4c719..6859c47ae 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1636,6 +1636,7 @@ 27FDF1421DAC22230087B4E6 /* SQLiteFunctionsTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SQLiteFunctionsTest.cc; sourceTree = ""; }; 27FDF1A21DAD79450087B4E6 /* LiteCore-dylib_Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "LiteCore-dylib_Release.xcconfig"; sourceTree = ""; }; 27FF5CAF27C83A8F00CFFA43 /* AsyncActorCommon.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AsyncActorCommon.hh; sourceTree = ""; }; + 27FF5CB027C96EC500CFFA43 /* Result.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Result.hh; sourceTree = ""; }; 42B6B0DD25A6A9D9004B20A7 /* URLTransformer.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = URLTransformer.hh; sourceTree = ""; }; 42B6B0E125A6A9D9004B20A7 /* URLTransformer.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = URLTransformer.cc; sourceTree = ""; }; 720EA3F51BA7EAD9002B8416 /* libLiteCore.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libLiteCore.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2258,6 +2259,7 @@ 2762A01F22EF641900F9AB18 /* Defer.hh */, 27393A861C8A353A00829C9B /* Error.cc */, 277D19C9194E295B008E91EB /* Error.hh */, + 27FF5CB027C96EC500CFFA43 /* Result.hh */, 27E89BA41D679542002C32B3 /* FilePath.cc */, 27E89BA51D679542002C32B3 /* FilePath.hh */, 27700DFD1FB642B80005D48E /* Increment.hh */, From ce017e94aa2c3b539f6db6a3939fc500b45667c1 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 3 Mar 2022 09:53:14 -0800 Subject: [PATCH 28/78] API: Added constant `kC4NoError` --- C/include/c4Error.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/C/include/c4Error.h b/C/include/c4Error.h index 5871c35c6..4ead68590 100644 --- a/C/include/c4Error.h +++ b/C/include/c4Error.h @@ -186,6 +186,13 @@ typedef struct C4Error { } C4Error; +#ifdef _MSC_VER +static const C4Error kC4NoError = { }; +#else +#define kC4NoError ((C4Error){ }) +#endif + + // C4Error C API: From 06021096b65fed5a7e94fcf60574a03547df67c1 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 3 Mar 2022 09:54:22 -0800 Subject: [PATCH 29/78] Async: Cleanup, API tweaks, documentation --- LiteCore/Support/Async.cc | 2 +- LiteCore/Support/Async.hh | 50 ++++----- LiteCore/Support/Result.hh | 34 +++--- LiteCore/tests/AsyncTest.cc | 28 ++--- Networking/BLIP/docs/Async.md | 136 ++++++++++++++++++++++-- Replicator/Puller.cc | 2 +- Replicator/Pusher.cc | 2 +- Replicator/Replicator.cc | 4 +- Replicator/tests/ConnectedClientTest.cc | 2 +- 9 files changed, 195 insertions(+), 65 deletions(-) diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 1cbb8207b..714d987a7 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -142,7 +142,7 @@ namespace litecore::actor { } - void assertNoAsyncError(C4Error err) { + void assertNoError(C4Error err) { Assert(!err, "Unexpected error in Async value, %s", err.description().c_str()); } diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index a077c9d61..79473c699 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -97,26 +97,26 @@ namespace litecore::actor { /// Creates the client-side view of the result. Async asyncValue() {return Async(this);} - /// Resolves the value by storing a non-error result and waking any waiting clients. + /// Resolves the value by storing a result (value or error) and waking any waiting clients. template >> void setResult(RESULT &&result) { + emplaceResult(std::forward(result)); + } + + /// Equivalent to `setResult` but constructs the result directly inside the provider + /// from the constructor parameters given. + template >> + void emplaceResult(Args&&... args) { std::unique_lock lock(_mutex); precondition(!_result); - _result.emplace(std::forward(result)); + _result.emplace(args...); _gotResult(lock); } -// /// Equivalent to `setResult` but constructs the T value directly inside the provider. -// template >> -// void emplaceResult(Args&&... args) { -// std::unique_lock lock(_mutex); -// precondition(!_result); -// _result.emplace(args...); -// _gotResult(lock); -// } - + /// Sets the result to be the `T` return value of the callback. + /// If the callback throws an exception, it is caught and stored as an error result. template void setResultFromCallback(LAMBDA &&callback) { bool duringCallback = true; @@ -131,27 +131,29 @@ namespace litecore::actor { } } + /// Returns the result, a `Result` containing either a value or an error. + /// The result must be available, or an exception is thrown. ResultType& result() & { std::unique_lock _lock(_mutex); precondition(_result); return *_result; } - const ResultType& result() const & { - return const_cast(this)->result(); - } - - ResultType result() && { - return moveResult(); - } + const ResultType& result() const & {return const_cast(this)->result();} + ResultType result() && {return moveResult();} + /// Extracts the result and returns it as a moveable direct value. ResultType moveResult() { std::unique_lock _lock(_mutex); precondition(_result); return *std::move(_result); } - bool hasError() const {return result().isError();} + /// Sets the result to a C4Error. + void setError(const C4Error &error) {setResult(error);} + void setException(const std::exception &x) {setResult(x);} + + bool hasError() const {return result().isError();} C4Error error() const { if (auto x = result().errorPtr()) @@ -174,7 +176,7 @@ namespace litecore::actor { if (hasError()) return Async(error()); try { - return callback(moveResult().get()); + return callback(moveResult().value()); } catch (const std::exception &x) { return Async(C4Error::fromException(x)); } @@ -286,7 +288,7 @@ namespace litecore::actor { if (provider.hasError()) result->setResult(provider.error()); else { - callback(provider.moveResult().get()); + callback(provider.moveResult().value()); result->setResult(Result()); } }); @@ -430,7 +432,7 @@ namespace litecore::actor { if (provider.hasError()) errorCallback(provider.error()); else - callback(provider.moveResult().get()); + callback(provider.moveResult().value()); }); } @@ -439,6 +441,6 @@ namespace litecore::actor { /// You can use this as the error-handling callback to `void Async::then(onResult,onError)`. /// It throws an assertion-failed exception if the C4Error's code is nonzero. - void assertNoAsyncError(C4Error); + void assertNoError(C4Error); } diff --git a/LiteCore/Support/Result.hh b/LiteCore/Support/Result.hh index d1ff79bfc..9f5df82fb 100644 --- a/LiteCore/Support/Result.hh +++ b/LiteCore/Support/Result.hh @@ -16,27 +16,33 @@ namespace litecore { /// Represents the return value of a function that can fail. /// It contains either a value, or an error. - /// The error type defaults to `liteore::error`. + /// The error type defaults to `C4Error`. template class Result { public: - /// A `Result` can be constructed from a value or an error. + /// Constructs a successful Result from a value. Result(const VAL &val) noexcept :_result(val) { } - Result(const ERR &err) noexcept :_result(err) { } Result(VAL &&val) noexcept :_result(std::move(val)) { } - Result(ERR &&err) noexcept :_result(std::move(err)) { } + /// Constructs a failure Result from an error. + /// The error must not be the empty/null/0 value. + Result(const ERR &err) noexcept :_result(err) {precondition(err);} + Result(ERR &&err) noexcept :_result(std::move(err)) {precondition(err);} + + /// True if successful. bool ok() const noexcept {return _result.index() == 0;} + + /// True if not successful. bool isError() const noexcept {return _result.index() != 0;} /// Returns the value. You must test first, as this will fail if there is an error! - VAL& get() & {return *std::get_if<0>(&_result);} - VAL&& get() && {return std::move(*std::get_if<0>(&_result));} + VAL& value() & {return *std::get_if<0>(&_result);} + VAL&& value() && {return std::move(*std::get_if<0>(&_result));} - /// Returns the error. Throws an exception if there is none. - const ERR& error() const noexcept {return *std::get_if<1>(&_result);} + /// Returns the error, or the default empty/null/0 error if none. + ERR error() const noexcept {auto e = errorPtr(); return e ? *e : ERR{};} - /// Returns a pointer to the error, or nullptr if there is none. + /// Returns a pointer to the error, or `nullptr` if there is none. const ERR* errorPtr() const noexcept {return std::get_if<1>(&_result);} private: @@ -44,19 +50,23 @@ namespace litecore { }; - // Specialization of `Result` when there is no value. + // Specialization of `Result` when there is no value; just represents success or an error. + // This just stores the ERR value, which is empty/null/0 in the success case. // Assumes `ERR` has a default no-error value and can be tested as a bool. template class Result { public: + /// Constructs a successful Result. Result() noexcept :_error{} { } + + /// Constructs a failure Result from an error. + /// The error may be the empty/null/0 value, in which case it means success. Result(const ERR &err) noexcept :_error(err) { } Result(ERR &&err) noexcept :_error(std::move(err)) { } bool ok() const noexcept {return !_error;} bool isError() const noexcept {return !!_error;} - void get() const {precondition(!_error);} - const ERR& error() const noexcept {precondition(_error); return _error;} + const ERR& error() const noexcept {return _error;} const ERR* errorPtr() const noexcept {return _error ? &_error : nullptr;} private: diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 12c74b491..9ea94af8e 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -121,7 +121,7 @@ TEST_CASE_METHOD(AsyncTest, "Async", "[Async]") { REQUIRE(!sum.ready()); _bProvider->setResult(" there"); REQUIRE(sum.ready()); - REQUIRE(sum.result().get() == "hi there"); + REQUIRE(sum.result().value() == "hi there"); } @@ -132,7 +132,7 @@ TEST_CASE_METHOD(AsyncTest, "Async, other order", "[Async]") { REQUIRE(!sum.ready()); aProvider()->setResult("hi"); REQUIRE(sum.ready()); - REQUIRE(sum.result().get() == "hi there"); + REQUIRE(sum.result().value() == "hi there"); } @@ -142,7 +142,7 @@ TEST_CASE_METHOD(AsyncTest, "Async, emplaceResult") { REQUIRE(!v.ready()); p->setResult("******"); REQUIRE(v.ready()); - CHECK(v.result().get() == "******"); + CHECK(v.result().value() == "******"); } @@ -151,7 +151,7 @@ TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { string result; move(sum).then([&](string s) { result = s; - }, assertNoAsyncError); + }, assertNoError); REQUIRE(!sum.ready()); REQUIRE(result == ""); _aProvider->setResult("hi"); @@ -170,14 +170,14 @@ TEST_CASE_METHOD(AsyncTest, "Async, 2 levels", "[Async]") { REQUIRE(!sum.ready()); _bProvider->setResult(" there"); REQUIRE(sum.ready()); - REQUIRE(sum.result().get() == "hi there!"); + REQUIRE(sum.result().value() == "hi there!"); } TEST_CASE_METHOD(AsyncTest, "Async, immediately", "[Async]") { Async im = provideImmediately(); REQUIRE(im.ready()); - REQUIRE(im.result().get() == "immediately"); + REQUIRE(im.result().value() == "immediately"); } @@ -196,7 +196,7 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning void", "[Async]") { provideSum().then([&](string &&s) { Log("--Inside then fn; s = \"%s\"", s.c_str()); result = s; - }, assertNoAsyncError); + }, assertNoError); Log("--Providing aProvider"); _aProvider->setResult("hi"); @@ -216,7 +216,7 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning T", "[Async]") { _aProvider->setResult("hi"); Log("--Providing bProvider"); _bProvider->setResult(" there"); - CHECK(size.blockingResult().get() == 8); + CHECK(size.blockingResult().value() == 8); } @@ -230,7 +230,7 @@ TEST_CASE_METHOD(AsyncTest, "Async then returning async T", "[Async]") { _aProvider->setResult("hi"); Log("--Providing bProvider"); _bProvider->setResult(" there"); - CHECK(dl.blockingResult().get() == "Contents of hi there"); + CHECK(dl.blockingResult().value() == "Contents of hi there"); } @@ -241,7 +241,7 @@ TEST_CASE_METHOD(AsyncTest, "Async Error", "[Async]") { _aProvider->setResult("hi"); REQUIRE(r.ready()); CHECK(!r.error()); - CHECK(r.result().get() == "hi"); + CHECK(r.result().value() == "hi"); } SECTION("error") { _aProvider->setResult(""); @@ -313,7 +313,7 @@ class AsyncTestActor : public Actor { assert(currentActor() == this); testThenResult = move(s); testThenReady = true; - }, assertNoAsyncError); + }, assertNoError); }); } @@ -324,7 +324,7 @@ class AsyncTestActor : public Actor { TEST_CASE("Async on thread", "[Async]") { auto asyncContents = downloader("couchbase.com"); - string contents = asyncContents.blockingResult().get(); + string contents = asyncContents.blockingResult().value(); CHECK(contents == "Contents of couchbase.com"); } @@ -332,7 +332,7 @@ TEST_CASE("Async on thread", "[Async]") { TEST_CASE("Async Actor", "[Async]") { auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org"); - string contents = asyncContents.blockingResult().get(); + string contents = asyncContents.blockingResult().value(); CHECK(contents == "Contents of couchbase.org"); } @@ -340,7 +340,7 @@ TEST_CASE("Async Actor", "[Async]") { TEST_CASE("Async Actor Twice", "[Async]") { auto actor = make_retained(); auto asyncContents = actor->download("couchbase.org", "couchbase.biz"); - string contents = asyncContents.blockingResult().get(); + string contents = asyncContents.blockingResult().value(); CHECK(contents == "Contents of couchbase.org and Contents of couchbase.biz"); } diff --git a/Networking/BLIP/docs/Async.md b/Networking/BLIP/docs/Async.md index 8b948dcc0..9932c2f47 100644 --- a/Networking/BLIP/docs/Async.md +++ b/Networking/BLIP/docs/Async.md @@ -1,6 +1,6 @@ # The Async API -(Last updated Feb 24 2022 by Jens) +(Last updated March 2 2022 by Jens) **Async** is a major extension of LiteCore’s concurrency support, which should help us write clearer and safer multithreaded code in the future. It extends the functionality of Actors: so far, Actor methods have had to return `void` since they’re called asynchronously. Getting a value back from an Actor meant explicitly passing a callback function. @@ -26,7 +26,7 @@ static Retained> _curProvider; Async getIntFromServer() { _curProvider = Async::makeProvider(); sendServerRequest(); - return intProvider->asyncValue(); + return _curProvider->asyncValue(); } ``` @@ -64,9 +64,11 @@ So how do you wait for the result? **You don’t.** Instead you let the Async ca Async request = getIntFromServer(); request.then([=](int i) { std::cout << "The result is " << i << "!\n"; -}); +}, assertNoError); ``` +> (What’s that `assertNoError`? Ignore it for now; it’ll be explained in the error handling section.) + What if you need that lambda to return a value? That value won’t be available until later when the lambda runs, so it too is returned as an `Async`: ```c++ @@ -85,7 +87,7 @@ Async status = getIntFromServer().then([=](int i) { }); ``` -In this situation it can be useful to chain multiple `then` calls: +In this situation it can be useful to **chain multiple `then` calls:** ```c++ Async message = getIntFromServer().then([=](int i) { @@ -95,6 +97,33 @@ Async message = getIntFromServer().then([=](int i) { }); ``` +### Async with no value (`Async`) + +Sometimes an asynchronous operation doesn’t need to return a value, but you still want to use `Async` with it so callers can be notified when it finishes. For that, use `Async`. For example: + +```c++ +Async slowOperation() { + _curProvider = Async::makeProvider(); + startOperationInBackground(); + return _curProvider->asyncValue(); +} + +static void operationFinished() { + _curProvider.setResult(kC4NoError); + _curProvider = nullptr; +} +``` + +Since there’s no actual result, you store a no-error value in the provider to indicate that it’s done. + +Similarly with a `then` call — if your callback returns nothing (`void`), the result will be an `Async` that merely indicates the completion of the callback: + +```c++ +Async done = getIntFromServer().then([=](int i) { + _currentInt = i; +}); +``` + ### Be Careful With Captures! It’s worth repeating the usual warnings about lambdas that can be called after the enclosing scope returns: **don’t capture by reference** (don’t use `[&]`) and **don’t capture pointers or `slice`s**. Here C++14’s capture-initializer syntax can be helpful: @@ -116,8 +145,8 @@ By default, a `then()` callback is called immediately when the provider’s `set But Actors want everything to run on their thread. For that case, `Async` has an `on(Actor*)` method that lets you specify that a subsequent `then()` should schedule its callback on the Actor’s thread. ```c++ -void MyActor::downloadInt() { - getIntFromServer() .on(this) .then([=](int i) { +Async MyActor::downloadInt() { + return getIntFromServer() .on(this) .then([=](int i) { // This code runs on the Actor's thread _myInt = i; }); @@ -159,7 +188,7 @@ As a bonus, if `asCurrentActor` is called on the Actor’s thread, it just calls ### Providing an Error Result -Any Async value (regardless of its type parameter) can resolve to an error instead of a result. You can store one by calling `setError()` on the provider. The parameter can be either a `C4Error` or a `std::exception`. +Any Async value (regardless of its type parameter) can resolve to an error instead of a result. You can store one by calling `setError(C4Error)` on the provider. If the code producing the value throws an exception, you can catch it and set it as the result with `setError()`. @@ -174,8 +203,97 @@ try { > Note: `asCurrentActor()` catches exceptions thrown by its lambda and returns them as an error on the returned Async. +### The `Result` class + +By the way, as part of implementing this, I added a general purpose `Result` class (see `Result.hh`.) This simply holds either a value of type `T` or a `C4Error`. It’s similar to types found in Swift, Rust, etc. + +```c++ +Result squareRoot(double n) { + if (n >= 0) + return sqrt(n); + else + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; +} + +if (auto root = squareRoot(x); root.ok()) + cout << "√x = " << root.value() << endl; +else + cerr << "No square root: " << root.error().description() << endl; +``` + + + ### Handling An Error -On the receiving side, you can check the error in an Async by calling its `error()` or `c4Error()` methods. +The regular `then` methods described earlier can’t tell their callback about an error, because their callbacks take a parameter of type `T`. So what happens if the result is an error? Consider this example from earlier: + +```c++ +Async incrementIntOnServer() { + return getIntFromServer().then([=](int i) { + return storeIntOnServer(i + 1); + }); +} +``` + +What happens if the async result of `getIntFromServer()` is an error? **The callback lambda is not called.** Instead, the error value is propagated to the `Async` , basically “passing the buck” to the caller of `incrementIntOnServer`. This is usually what you want. + +If you want to handle the result whether or not it’s an error, you can set the callback’s parameter type to `Result`: + +```c++ +getIntFromServer().then([=](Result i) { + if (i.ok()) + _latestInt = i.value(); + else + cerr << "Couldn't get int: " << i.error().description() << endl; +}); +``` + +Note that this form of `then()` does not return any value, because the callback completely handles the operation. + +Another way to do this is to pass **two callbacks** to `then`: + +```c++ +getIntFromServer().then([=](int i) { + _latestInt = i.value(); +}, [=](C4Error error) { + cerr << "Couldn't get int: " << error.description() << endl; +}); +``` + +This finally explains the reason for mysterious `assertNoError` in the first example of section 2: that’s a function declared in `Async.hh` that simply takes a `C4Error` and throws an exception if it’s non-zero. That example was calling this two-callback version of `then` but idiomatically asserting that there would be no error. + +### Returning an error from a `then` callback + +There are two ways that a `then` method’s callback can signal an error. + +1. The callback can throw an exception. This will be caught and converted into a `C4Error` result. +2. The callback can return a `C4Error` directly, by explicitly declaring a return type of `Result`. This works because `Result` can be initialized with either a value or an error. + +Here’s an example of the second form: + +```c++ +Async squareRootFromServer() { + return getIntFromServer().then([=](int i) -> Result { // explicit result type! + if (i >= 0) + return sqrt(i); + else + return C4Error{LiteCoreDomain, kC4ErrorRemoteError}; + }); +} +``` + +## Appendix: Design + +First off, I’m aware that C++ already has a `std::future` class. However, it uses blocking control flow: + +> The `get` member function waits until the `future` has a valid result and (depending on which template is used) retrieves it. It effectively calls `wait()` in order to wait for the result. ([\*](https://en.cppreference.com/w/cpp/thread/future/get)) + +`std::future` has no mechanism to observe the result or register a callback. This makes it unusable in our async-oriented concurrency system. + +The “async/await” mechanism that’s now available in many languages was an inspiration, but unfortunately it’s not possible to implement something like `await` in C++17 — it changes control flow fundamentally, turning the enclosing function into a coroutine. I did try to implement this using some [weird C(++) tricks](https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html) and macros, but it was too inflexible and had too many broken edge cases. We’ll have to wait until we can move to [C++20](https://en.cppreference.com/w/cpp/language/coroutines). + +You *can* do async without `await`; it just needs a “`then({...})`” idiom of chaining callback handlers. JavaScript did this before the `await` syntax was added in 2017. + +Two C++ libraries I took design ideas from were [Folly](https://engineering.fb.com/2015/06/19/developer-tools/futures-for-c-11-at-facebook/) (by Facebook) and [Cap’n Proto](https://github.com/capnproto/capnproto/blob/master/kjdoc/tour.md#asynchronous-event-loop). I went with a different class name, though; Folly calls them `future` and Cap’n Proto calls them `Promise`. I just liked `Async` better. `¯\_(ツ)_/¯` -> **Warning:** If you call `result` and there’s an error, it will be thrown as an exception!) +I didn’t directly use either library because their async code is tied to their own implementations of event loops, while we need to tie in with our existing `Actor` and `Mailbox`. diff --git a/Replicator/Puller.cc b/Replicator/Puller.cc index 3c69cf1c6..982509968 100644 --- a/Replicator/Puller.cc +++ b/Replicator/Puller.cc @@ -122,7 +122,7 @@ namespace litecore { namespace repl { _fatalError = true; } Signpost::end(Signpost::blipSent); - }, actor::assertNoAsyncError); + }, actor::assertNoError); }); } diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index 111eab7bb..c4da558be 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -358,7 +358,7 @@ namespace litecore { namespace repl { ++iResponse; } maybeSendMoreRevs(); - }, actor::assertNoAsyncError); + }, actor::assertNoError); } diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index 44c9be25c..c509e8737 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -583,7 +583,7 @@ namespace litecore { namespace repl { if (_checkpointJSONToSave) saveCheckpointNow(); // _saveCheckpoint() was waiting for _remoteCheckpointRevID - }, actor::assertNoAsyncError); + }, actor::assertNoError); } @@ -654,7 +654,7 @@ namespace litecore { namespace repl { } _checkpointer.saveCompleted(); } - }, actor::assertNoAsyncError); + }, actor::assertNoError); } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 3fc0a38ed..f061ca24d 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -88,7 +88,7 @@ class ConnectedClientLoopbackTest : public C4Test, Log("++++ Async response available!"); if (auto err = asyncResult.error()) FAIL("Response returned an error " << err); - return asyncResult.result().get(); + return asyncResult.result().value(); } From 79cab42af484b154f35bb829bf92df285bb45d15 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Mar 2022 10:57:02 -0800 Subject: [PATCH 30/78] Fleshing out and documenting Result --- LiteCore/Support/Async.cc | 55 ++---- LiteCore/Support/Async.hh | 43 +---- LiteCore/Support/Result.hh | 193 ++++++++++++++++++-- LiteCore/tests/AsyncTest.cc | 219 ++++++++++++++++++++++- Networking/BLIP/docs/Result.md | 97 ++++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 2 + 6 files changed, 513 insertions(+), 96 deletions(-) create mode 100644 Networking/BLIP/docs/Result.md diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 714d987a7..477954624 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -55,14 +55,19 @@ namespace litecore::actor { void AsyncProviderBase::notifyObserver(Observer &observer, Actor *actor) { - if (actor && actor != Actor::currentActor()) { - // Schedule a call on my Actor: - actor->asCurrentActor([observer, provider=fleece::retained(this)] { - observer(*provider); - }); - } else { - // ... or call it synchronously: - observer(*this); + try { + if (actor && actor != Actor::currentActor()) { + // Schedule a call on my Actor: + actor->asCurrentActor([observer, provider=fleece::retained(this)] { + observer(*provider); + }); + } else { + // ... or call it synchronously: + observer(*this); + } + } catch (...) { + // we do not want an exception from the observer to propagate + C4Error::warnCurrentException("AsyncProviderBase::notifyObserver"); } } @@ -83,35 +88,6 @@ namespace litecore::actor { } -// C4Error AsyncProviderBase::c4Error() const { -// return _error ? C4Error::fromException(*_error) : C4Error{}; -// } -// -// -// void AsyncProviderBase::setError(const C4Error &c4err) { -// precondition(c4err.code != 0); -// unique_lock lock(_mutex); -// precondition(!_error); -// _error = make_unique(c4err); -// _gotResult(lock); -// } -// -// -// void AsyncProviderBase::setError(const std::exception &x) { -// auto e = litecore::error::convertException(x); -// unique_lock lock(_mutex); -// precondition(!_error); -// _error = make_unique(move(e)); -// _gotResult(lock); -// } -// -// -// void AsyncProviderBase::throwIfError() const { -// if (_error) -// throw *_error; -// } - - #pragma mark - ASYNC BASE: @@ -120,11 +96,6 @@ namespace litecore::actor { } -// C4Error AsyncBase::c4Error() const { -// return _provider->c4Error(); -// } - - void AsyncBase::blockUntilReady() { if (!ready()) { precondition(Actor::currentActor() == nullptr); // would deadlock if called by an Actor diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index 79473c699..bf4366ae8 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -81,7 +81,7 @@ namespace litecore::actor { class AsyncProvider final : public AsyncProviderBase { public: using ResultType = Result; - using ThenType = typename _ThenType::type; + using ThenType = typename _ThenType::type; // `ThenType` is `T` unless `T` is `void` /// Creates a new empty AsyncProvider. static Retained create() {return new AsyncProvider;} @@ -115,22 +115,6 @@ namespace litecore::actor { _gotResult(lock); } - /// Sets the result to be the `T` return value of the callback. - /// If the callback throws an exception, it is caught and stored as an error result. - template - void setResultFromCallback(LAMBDA &&callback) { - bool duringCallback = true; - try { - auto result = callback(); - duringCallback = false; - setResult(std::move(result)); - } catch (...) { - if (!duringCallback) - throw; - setResult(error::convertCurrentException()); - } - } - /// Returns the result, a `Result` containing either a value or an error. /// The result must be available, or an exception is thrown. ResultType& result() & { @@ -172,7 +156,7 @@ namespace litecore::actor { { } template - Async _now(std::function(ThenType&&)> &callback) { + Async _now(std::function(ThenType&&)> &&callback) { if (hasError()) return Async(error()); try { @@ -269,7 +253,7 @@ namespace litecore::actor { auto then(LAMBDA &&callback) && { // U is the return type of the lambda with any `Async<...>` removed using U = unwrap_async; - return _then(std::forward(callback)); + return _then(std::function(std::forward(callback))); } @@ -350,7 +334,7 @@ namespace litecore::actor { return result(); } - /// Returns the error result, else nullptr. + /// Returns the error. C4Error error() const {return provider()->error();} private: @@ -377,20 +361,11 @@ namespace litecore::actor { auto uProvider = Async::makeProvider(); if (canCallNow()) { // Result is available now, so call the callback: - if (C4Error x = error()) - uProvider->setResult(x); - else - uProvider->setResultFromCallback([&]{return callback(moveResult());}); + uProvider->setResult(moveResult().then(callback)); } else { _provider->setObserver(_onActor, [=](AsyncProviderBase &providerBase) { - auto provider = dynamic_cast&>(providerBase); - if (auto x = provider.error()) - uProvider->setError(*x); - else { - uProvider->setResultFromCallback([&]{ - return callback(provider.moveResult()); - }); - } + auto &provider = dynamic_cast&>(providerBase); + uProvider->setResult(provider.moveResult().then(callback)); }); } return uProvider->asyncValue(); @@ -403,14 +378,14 @@ namespace litecore::actor { Async _then(std::function(ThenType&&)> &&callback) { if (canCallNow()) { // If I'm ready, just call the callback and pass on the Async it returns: - return provider()->_now(callback); + return provider()->_now(std::move(callback)); } else { // Otherwise wait for my result... auto uProvider = Async::makeProvider(); _provider->setObserver(_onActor, [=](AsyncProviderBase &provider) mutable { // Invoke the callback, then wait to resolve the Async it returns: auto &tProvider = dynamic_cast&>(provider); - auto asyncU = tProvider._now(callback); + auto asyncU = tProvider._now(std::move(callback)); std::move(asyncU).thenProvider([uProvider](auto &provider) { // Then finally resolve the async I returned: uProvider->setResult(provider.result()); diff --git a/LiteCore/Support/Result.hh b/LiteCore/Support/Result.hh index 9f5df82fb..4910089ba 100644 --- a/LiteCore/Support/Result.hh +++ b/LiteCore/Support/Result.hh @@ -5,6 +5,8 @@ // #pragma once +#include "Defer.hh" // for CONCATENATE() +#include "function_ref.hh" #include #include #include @@ -13,16 +15,26 @@ struct C4Error; namespace litecore { struct error; + template class Result; + + namespace { + // Magic template gunk. `unwrap_Result` removes a layer of `Result<...>` from a type: + // - `unwrap_Result` is `string`. + // - `unwrap_Result> is `string`. + template T _unwrap_Result(T*); + template T _unwrap_Result(Result*); + template using unwrap_Result = decltype(_unwrap_Result((T*)nullptr)); + } + /// Represents the return value of a function that can fail. - /// It contains either a value, or an error. - /// The error type defaults to `C4Error`. - template + /// It contains either a value of type T, or an error of type ERR (defaulting to C4Error). + template class Result { public: /// Constructs a successful Result from a value. - Result(const VAL &val) noexcept :_result(val) { } - Result(VAL &&val) noexcept :_result(std::move(val)) { } + Result(const T &val) noexcept :_result(val) { } + Result(T &&val) noexcept :_result(std::move(val)) { } /// Constructs a failure Result from an error. /// The error must not be the empty/null/0 value. @@ -36,8 +48,8 @@ namespace litecore { bool isError() const noexcept {return _result.index() != 0;} /// Returns the value. You must test first, as this will fail if there is an error! - VAL& value() & {return *std::get_if<0>(&_result);} - VAL&& value() && {return std::move(*std::get_if<0>(&_result));} + T& value() & {return *std::get_if<0>(&_result);} + T value() && {return std::move(*std::get_if<0>(&_result));} /// Returns the error, or the default empty/null/0 error if none. ERR error() const noexcept {auto e = errorPtr(); return e ? *e : ERR{};} @@ -45,22 +57,49 @@ namespace litecore { /// Returns a pointer to the error, or `nullptr` if there is none. const ERR* errorPtr() const noexcept {return std::get_if<1>(&_result);} + /// Transforms a `Result` to a `Result` by passing the value through a function. + /// - If I have a value, I pass it to `fn` and return its result. + /// * If `fn` throws an exception, it's caught and returned (thanks to `TryResult()`.) + /// - If I have an error, `fn` is _not_ called, and I return my error. + /// @param fn A function/lambda that takes a `T&&` and returns `U` or `Result`. + /// @return The result of `fn`, or else my current error, as a `Result`. + template , + typename U = unwrap_Result> + [[nodiscard]] + Result then(LAMBDA fn) && noexcept { + return _then(fleece::function_ref(std::forward(fn))); + } + + /// Calls `fn` with the error, if there is one, else does nothing. + /// @param fn A function/lambda that takes a `C4Error` and returns `void`. + /// @return Always returns itself, `*this`. + template + [[nodiscard]] + Result& onError(LAMBDA fn) { + if (isError()) + fn(error()); + return *this; + } + private: - std::variant _result; + template + Result _then(fleece::function_ref const& fn) noexcept; + template + Result _then(fleece::function_ref(T&&)> const& fn) noexcept; + + std::variant _result; }; // Specialization of `Result` when there is no value; just represents success or an error. - // This just stores the ERR value, which is empty/null/0 in the success case. - // Assumes `ERR` has a default no-error value and can be tested as a bool. + // - The `success` constructor takes no arguments. Or you can construct with `kC4NoError`. + // - There is no `value` method. + // - The `then` callback takes no arguments. template class Result { public: - /// Constructs a successful Result. Result() noexcept :_error{} { } - - /// Constructs a failure Result from an error. - /// The error may be the empty/null/0 value, in which case it means success. Result(const ERR &err) noexcept :_error(err) { } Result(ERR &&err) noexcept :_error(std::move(err)) { } @@ -69,12 +108,134 @@ namespace litecore { const ERR& error() const noexcept {return _error;} const ERR* errorPtr() const noexcept {return _error ? &_error : nullptr;} + template , + typename U = unwrap_Result> + [[nodiscard]] + Result then(LAMBDA fn) && noexcept { + return _then(fleece::function_ref(std::forward(fn))); + } + + template + void onError(LAMBDA fn) { + if (isError()) + fn(error()); + } + private: + template + Result _then(fleece::function_ref const& fn) noexcept; + template + Result _then(fleece::function_ref()> const& fn) noexcept; + ERR _error; }; - /// A `Result` whose error type is `C4Error`. - template using C4Result = Result; + /// Runs a function returning `T` (or `Result`) in a try/catch block, + /// catching any exception and returning it as an error. Returns `Result`. + template + [[nodiscard]] + Result TryResult(fleece::function_ref fn) noexcept { + try { + return fn(); + } catch (std::exception &x) { + return C4Error::fromException(x); + } + } + + + /// Runs a function returning `Result` in a try/catch block, catching any exception and + /// returning it as an error. Returns `Result`. + template + [[nodiscard]] + Result TryResult(fleece::function_ref()> fn) noexcept { + try { + return fn(); + } catch (std::exception &x) { + return C4Error::fromException(x); + } + } + + + template <> + [[nodiscard]] + inline Result TryResult(fleece::function_ref fn) noexcept { + try { + fn(); + return {}; + } catch (std::exception &x) { + return C4Error::fromException(x); + } + } + + + // (this helps the compiler deduce T when TryResult() is called with a lambda) + template , + typename T = unwrap_Result> + [[nodiscard]] + inline Result TryResult(LAMBDA fn) noexcept { + return TryResult(fleece::function_ref(std::move(fn))); + } + + + /// An approximation of Swift's `try` syntax for clean error propagation without exceptions. + /// First `EXPR` is evaluated. + /// - If the result is ok, the value is assigned to `VAR`, which may be an existing variable + /// name (`foo`) or a declaration (`int foo`). + /// - If the result is an error, that error is returned from the current function, which should + /// have a return type of `Result<>` or `C4Error`. + #define TRY_RESULT(VAR, EXPR) \ + auto CONCATENATE(rslt, __LINE__) = (EXPR); \ + if (CONCATENATE(rslt, __LINE__).isError()) \ + return CONCATENATE(rslt, __LINE__).error(); \ + VAR = std::move(CONCATENATE(rslt, __LINE__)).value(); + // (`CONCATENATE(rslt, __LINE__)` is just a clumsy way to create a unique variable name.) + + + //---- Method implementations + + + template + template + [[nodiscard]] + Result Result::_then(fleece::function_ref const& fn) noexcept { + if (ok()) + return TryResult([&]{return fn(std::move(value()));}); + else + return error(); + } + + template + template + [[nodiscard]] + Result Result::_then(fleece::function_ref(T&&)> const& fn) noexcept { + if (ok()) + return TryResult([&]{return fn(std::move(value()));}); + else + return error(); + } + + + template + template + [[nodiscard]] + Result Result::_then(fleece::function_ref const& fn) noexcept { + if (ok()) + return TryResult(fn); + else + return error(); + } + + template + template + [[nodiscard]] + Result Result::_then(fleece::function_ref()> const& fn) noexcept { + if (ok()) + return TryResult(fn); + else + return error(); + } } diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 9ea94af8e..3984d8d56 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -19,6 +19,209 @@ using namespace std; using namespace litecore::actor; +#pragma mark - RESULT: + + +static Result rfunc(int x) { + if (x > 0) + return to_string(x); + else if (x < 0) + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + else + throw logic_error("I didn't expect a kind of Spanish Inquisition!"); +} + + +static Result rvfunc(int x) { + if (x > 0) + return {}; + else if (x < 0) + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + else + throw logic_error("I didn't expect a kind of Spanish Inquisition!"); +} + + +static string xfunc(int x) { + if (x >= 0) + return to_string(x); + else + C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}.raise(); +} + + +TEST_CASE("Result", "[Async]") { + auto r = rfunc(1); + CHECK(r.ok()); + CHECK(r.value() == "1"); + CHECK(r.error() == kC4NoError); + CHECK(r.errorPtr() == nullptr); + + r = rfunc(-1); + CHECK(!r.ok()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + CHECK(r.errorPtr() != nullptr); + CHECK(*r.errorPtr() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); +} + + +// Test Result::then +TEST_CASE("Result then", "[Async]") { + SECTION("Success") { + Result r = rfunc(11).then([](string &&str) { return str.size();}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error") { + Result r = rfunc(-1).then([](string &&str) { return str.size();}); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success, returning Result") { + Result r = rfunc(11).then([](string &&str) -> Result { return str.size();}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error, returning Result") { + Result r = rfunc(11).then([](string &&str) -> Result { + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + + +// Test Result::then() +TEST_CASE("Result void then", "[Async]") { + SECTION("Success") { + Result r = rvfunc(11).then([]() { return 2;}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error") { + Result r = rvfunc(-1).then([]() { return 1;}); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success, returning Result") { + Result r = rvfunc(11).then([]() -> Result { return 2;}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error, returning Result") { + Result r = rvfunc(11).then([]() -> Result { + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + + +// Test Result.then(), where the fn returns void +TEST_CASE("Result then void", "[Async]") { + SECTION("Success") { + optional calledWith; + Result r = rfunc(11).then([&](string &&str) { calledWith = str; }); + REQUIRE(r.ok()); + CHECK(calledWith == "11"); + } + SECTION("Error") { + optional calledWith; + Result r = rfunc(-1).then([&](string &&str) { calledWith = str; }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success, returning Result") { + optional calledWith; + Result r = rfunc(11).then([&](string &&str) -> Result { + calledWith = str; return {}; + }); + REQUIRE(r.ok()); + CHECK(calledWith == "11"); + } + SECTION("Error, returning Result") { + optional calledWith; + Result r = rfunc(11).then([&](string &&str) -> Result { + calledWith = str; return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + CHECK(calledWith == "11"); + } +} + + +TEST_CASE("Result onError", "[Async]") { + SECTION("Success") { + optional calledWithErr; + Result r = rfunc(11).onError([&](C4Error err) {calledWithErr = err;}); + REQUIRE(r.ok()); + CHECK(r.value() == "11"); + CHECK(!calledWithErr); + } + SECTION("Error") { + optional calledWithErr; + Result r = rfunc(-1).onError([&](C4Error err) {calledWithErr = err;}); + REQUIRE(r.isError()); + CHECK(calledWithErr == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + +TEST_CASE("TryResult", "[Async]") { + SECTION("Success") { + auto r = TryResult([]{ return xfunc(4);}); + CHECK(r.value() == "4"); + } + + SECTION("Exception") { + ExpectingExceptions x; + auto r = TryResult([]{ return xfunc(-1);}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success when lambda returns Result") { + auto r = TryResult([]{ return rfunc(4);}); + CHECK(r.value() == "4"); + } + + SECTION("Error when lambda returns Result") { + auto r = TryResult([]{ return rfunc(-1);}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Exception when lambda returns Result") { + ExpectingExceptions x; + auto r = TryResult([]{ return rfunc(0);}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorAssertionFailed}); + } +} + + +TEST_CASE("TRY", "[Async]") { + auto fn = [](int x) -> Result { + TRY_RESULT(string str, rfunc(x)); + TRY_RESULT(string str2, rfunc(x)); + return str.size(); + }; + + Result r = fn(1234); + REQUIRE(r.ok()); + CHECK(r.value() == 4); + + r = fn(-1); + REQUIRE(!r.ok()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); +} + + +#pragma mark - ASYNC: + + static Async downloader(string url) { auto provider = Async::makeProvider(); std::thread t([=] { @@ -55,11 +258,10 @@ class AsyncTest { return bProvider()->asyncValue(); } - Async provideOne() { + Async provideDouble() { Log("provideSum: awaiting A"); - return provideA().then([=](string a) { - Log("provideSum: awaiting B"); - return a; + return provideA().then([=](string a) -> string { + return a + a; }); } @@ -146,6 +348,15 @@ TEST_CASE_METHOD(AsyncTest, "Async, emplaceResult") { } +TEST_CASE_METHOD(AsyncTest, "Async then", "[Async]") { + Async s = provideDouble(); + REQUIRE(!s.ready()); + _aProvider->setResult("Twice"); + REQUIRE(s.ready()); + CHECK(s.result().value() == "TwiceTwice"); +} + + TEST_CASE_METHOD(AsyncTest, "AsyncWaiter", "[Async]") { Async sum = provideSum(); string result; diff --git a/Networking/BLIP/docs/Result.md b/Networking/BLIP/docs/Result.md new file mode 100644 index 000000000..efff36707 --- /dev/null +++ b/Networking/BLIP/docs/Result.md @@ -0,0 +1,97 @@ +# The Useful `Result` Type + +(Last updated March 3 2022 by Jens) + +**Result** is a utility class template for improving error handling without exceptions, inspired by languages like Swift and Rust. + +We still use exceptions inside LiteCore, but in some places it’s better to manage errors as `C4Error` values, usually when the error isn’t considered an “exceptional” situation where something’s gone unexpectedly wrong. An example of this is when saving a document — it’s entirely possible to get a kC4ErrorConflict in normal operation, so it shouldn’t be thrown. And in the case of asynchronous operations (`Async`), exceptions don’t make sense at all. + +In these situations we’ve been using the same calling convention we use in the C API: a special return value like `NULL` or `0` indicating failure, and a `C4Error*` parameter that the callee copies the error to. But that’s kind of awkward. We can do better. + +## 1. What’s a `Result`? + +`Result`, defined in the header `Result.hh`, is a container that can hold either a value of type `T` or a `C4Error`. + +- You can construct one from either a `T` or a `C4Error`. +- Boolean methods `ok()` and `isError()` tell you which it holds. +- `value()` returns the value, but if there’s an error it throws it instead. (So check first!) +- `error()` returns the error if there is one, or else a default error with `code==0`. + +Result’s main job is as the return value of a function that can fail: + +```c++ +Result squareRoot(double n) { + if (n >= 0) + return sqrt(n); + else + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; +} +``` + +Note that one branch returns a `double` and the other a `C4Error`. That’s fine since the actual return type can be constructed from either one. + +### `Result` + +`Result` is a special case where there isn’t any value to return, just “no error”. This subtype has no `value()` method, but you can otherwise treat it like other Results. + +## 3. What Do You Do With One? + +If you call a function that returns Result, you can check what it holds and do the appropriate thing: + +```c++ +if (auto r = squareRoot(n); r.ok()) { + cout << "√n = " << r.value() << endl; +} else { + cerr << "squareRoot failed: " << r.error().description() << endl; +} +``` + +If you’re doing this inside a function that itself returns a `Result`, you can just pass the buck: + +```c++ +Result showSquareRoot(double n) { + if (auto r = squareRoot(n); r.ok()) { + cout << "√n = " << r.value() << endl; + return {}; + } else { + return r.error(); + } +} +``` + +## 4. Useful Helpers + +### TryResult() + +`TryResult` lets you safely call a function that may throw an exception. Itakes a function/lambda that returns `T` (or `Result`), calls it, and returns the result as a `Result`. If the function throws an exception, it is caught and returned as the `error` in the result. + +```c++ +extern string read_line(stream*); // throws exception on I/O error + +Result input = TryResult( []{ return read_line(in); }); +``` + +### then() + +The `Result::then()` method lets you chain together operations that return Results. You can also look at it as a kind of functional “map” operation that transforms one type of Result into another. + +It takes a function/lambda with a parameter of type `T` (or optimally, `T&&`) that returns some type `U`. + +- If the receiver contains a value, it passes it to the function, then returns the function’s result wrapped in a `Result`. + - Bonus: if the function throws an exception, it’s caught and returned as the Result’s error. +- Otherwise, it just returns its error in a `Result`. + +Here `T` is `double`, `U` is `string`, and the function returns `U`: + +```c++ +Result str = squareRoot(n).then( [](double root) {return to_string(root);} ); +``` + +Here’s an example that goes the other direction, `string` to `double`, and the function returns a `Result`: + +```c++ +Result root = parseDouble(str).then( [](double n) {return sqareRoot(n);} ); +``` + + + diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 6859c47ae..eeeb7fa4f 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1171,6 +1171,7 @@ 276943881DCD4AAD00DB2555 /* c4Observer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = c4Observer.h; sourceTree = ""; }; 2769438B1DCD502A00DB2555 /* c4Observer.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4Observer.cc; sourceTree = ""; }; 2769438E1DD0ED3F00DB2555 /* c4ObserverTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4ObserverTest.cc; sourceTree = ""; }; + 27697DD727D19B25006F5BB5 /* Result.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Result.md; sourceTree = ""; }; 276993E125390C3300FDF699 /* VectorRecord.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = VectorRecord.hh; sourceTree = ""; }; 276993E525390C3300FDF699 /* VectorRecord.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = VectorRecord.cc; sourceTree = ""; }; 276CE67C2267991400B681AC /* n1ql.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = n1ql.cc; sourceTree = ""; }; @@ -2934,6 +2935,7 @@ children = ( 27DF3BC326867F2600A57A1E /* Actors.md */, 277C5C4827AC4E0E001BE212 /* Async.md */, + 27697DD727D19B25006F5BB5 /* Result.md */, 27DF3BC426867F2600A57A1E /* logo.png */, 27DF3BC526867F2600A57A1E /* BLIP Protocol.md */, ); From dcf0bb5b7bf631f49b89ab4ace398023df72c7d3 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Mar 2022 11:29:10 -0800 Subject: [PATCH 31/78] Result: Hardcode C4Error instead of it being a template param --- LiteCore/Support/Result.hh | 98 ++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/LiteCore/Support/Result.hh b/LiteCore/Support/Result.hh index 4910089ba..81a9bdb3e 100644 --- a/LiteCore/Support/Result.hh +++ b/LiteCore/Support/Result.hh @@ -1,26 +1,31 @@ // // Result.hh // -// Copyright © 2022 Couchbase. All rights reserved. +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. // #pragma once #include "Defer.hh" // for CONCATENATE() #include "function_ref.hh" -#include +#include #include #include struct C4Error; namespace litecore { - struct error; - template class Result; + template class Result; + + // !!! Documentation is at Replicator/docs/Result.md !!! namespace { - // Magic template gunk. `unwrap_Result` removes a layer of `Result<...>` from a type: - // - `unwrap_Result` is `string`. - // - `unwrap_Result> is `string`. + // Magic template gunk. `unwrap_Result` removes a layer of `Result<...>` from type T template T _unwrap_Result(T*); template T _unwrap_Result(Result*); template using unwrap_Result = decltype(_unwrap_Result((T*)nullptr)); @@ -28,18 +33,18 @@ namespace litecore { /// Represents the return value of a function that can fail. - /// It contains either a value of type T, or an error of type ERR (defaulting to C4Error). - template + /// It contains either a value of type T, or a C4Error. + template class Result { public: /// Constructs a successful Result from a value. - Result(const T &val) noexcept :_result(val) { } - Result(T &&val) noexcept :_result(std::move(val)) { } + Result(const T &val) noexcept :_result(val) { } + Result(T &&val) noexcept :_result(std::move(val)) { } /// Constructs a failure Result from an error. /// The error must not be the empty/null/0 value. - Result(const ERR &err) noexcept :_result(err) {precondition(err);} - Result(ERR &&err) noexcept :_result(std::move(err)) {precondition(err);} + Result(const C4Error &err) noexcept :_result(err) {precondition(err);} + Result(C4Error &&err) noexcept :_result(std::move(err)) {precondition(err);} /// True if successful. bool ok() const noexcept {return _result.index() == 0;} @@ -51,11 +56,11 @@ namespace litecore { T& value() & {return *std::get_if<0>(&_result);} T value() && {return std::move(*std::get_if<0>(&_result));} - /// Returns the error, or the default empty/null/0 error if none. - ERR error() const noexcept {auto e = errorPtr(); return e ? *e : ERR{};} + /// Returns the error, or an empty C4Error with code==0 if none. + C4Error error() const noexcept {auto e = errorPtr(); return e ? *e : C4Error{};} /// Returns a pointer to the error, or `nullptr` if there is none. - const ERR* errorPtr() const noexcept {return std::get_if<1>(&_result);} + const C4Error* errorPtr() const noexcept {return std::get_if<1>(&_result);} /// Transforms a `Result` to a `Result` by passing the value through a function. /// - If I have a value, I pass it to `fn` and return its result. @@ -67,7 +72,7 @@ namespace litecore { typename RV = std::invoke_result_t, typename U = unwrap_Result> [[nodiscard]] - Result then(LAMBDA fn) && noexcept { + Result then(LAMBDA fn) && noexcept { return _then(fleece::function_ref(std::forward(fn))); } @@ -84,35 +89,35 @@ namespace litecore { private: template - Result _then(fleece::function_ref const& fn) noexcept; + Result _then(fleece::function_ref const& fn) noexcept; template - Result _then(fleece::function_ref(T&&)> const& fn) noexcept; + Result _then(fleece::function_ref(T&&)> const& fn) noexcept; - std::variant _result; + std::variant _result; }; - // Specialization of `Result` when there is no value; just represents success or an error. + // Specialization of `Result` when there is no value; it just represents success or an error. // - The `success` constructor takes no arguments. Or you can construct with `kC4NoError`. // - There is no `value` method. // - The `then` callback takes no arguments. - template - class Result { + template<> + class Result { public: - Result() noexcept :_error{} { } - Result(const ERR &err) noexcept :_error(err) { } - Result(ERR &&err) noexcept :_error(std::move(err)) { } + Result() noexcept :_error{} { } + Result(const C4Error &err) noexcept :_error(err) { } + Result(C4Error &&err) noexcept :_error(std::move(err)) { } - bool ok() const noexcept {return !_error;} - bool isError() const noexcept {return !!_error;} - const ERR& error() const noexcept {return _error;} - const ERR* errorPtr() const noexcept {return _error ? &_error : nullptr;} + bool ok() const noexcept {return !_error;} + bool isError() const noexcept {return !!_error;} + const C4Error& error() const noexcept {return _error;} + const C4Error* errorPtr() const noexcept {return _error ? &_error : nullptr;} template , typename U = unwrap_Result> [[nodiscard]] - Result then(LAMBDA fn) && noexcept { + Result then(LAMBDA fn) && noexcept { return _then(fleece::function_ref(std::forward(fn))); } @@ -124,15 +129,15 @@ namespace litecore { private: template - Result _then(fleece::function_ref const& fn) noexcept; + Result _then(fleece::function_ref const& fn) noexcept; template - Result _then(fleece::function_ref()> const& fn) noexcept; + Result _then(fleece::function_ref()> const& fn) noexcept; - ERR _error; + C4Error _error; }; - /// Runs a function returning `T` (or `Result`) in a try/catch block, + /// Runs a function returning `T` in a try/catch block, /// catching any exception and returning it as an error. Returns `Result`. template [[nodiscard]] @@ -145,8 +150,8 @@ namespace litecore { } - /// Runs a function returning `Result` in a try/catch block, catching any exception and - /// returning it as an error. Returns `Result`. + /// Runs a function returning `Result` in a try/catch block, + /// catching any exception and returning it as an error. Returns `Result`. template [[nodiscard]] Result TryResult(fleece::function_ref()> fn) noexcept { @@ -158,6 +163,7 @@ namespace litecore { } + // (specialization needed for T=void) template <> [[nodiscard]] inline Result TryResult(fleece::function_ref fn) noexcept { @@ -172,8 +178,8 @@ namespace litecore { // (this helps the compiler deduce T when TryResult() is called with a lambda) template , - typename T = unwrap_Result> + typename RV = std::invoke_result_t, // return value + typename T = unwrap_Result> // RV with `Result<...>` stripped off [[nodiscard]] inline Result TryResult(LAMBDA fn) noexcept { return TryResult(fleece::function_ref(std::move(fn))); @@ -197,20 +203,20 @@ namespace litecore { //---- Method implementations - template + template template [[nodiscard]] - Result Result::_then(fleece::function_ref const& fn) noexcept { + Result Result::_then(fleece::function_ref const& fn) noexcept { if (ok()) return TryResult([&]{return fn(std::move(value()));}); else return error(); } - template + template template [[nodiscard]] - Result Result::_then(fleece::function_ref(T&&)> const& fn) noexcept { + Result Result::_then(fleece::function_ref(T&&)> const& fn) noexcept { if (ok()) return TryResult([&]{return fn(std::move(value()));}); else @@ -218,20 +224,18 @@ namespace litecore { } - template template [[nodiscard]] - Result Result::_then(fleece::function_ref const& fn) noexcept { + Result Result::_then(fleece::function_ref const& fn) noexcept { if (ok()) return TryResult(fn); else return error(); } - template template [[nodiscard]] - Result Result::_then(fleece::function_ref()> const& fn) noexcept { + Result Result::_then(fleece::function_ref()> const& fn) noexcept { if (ok()) return TryResult(fn); else From 09f42e274470141843d789071b2ea3071d6065d6 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Mar 2022 11:50:58 -0800 Subject: [PATCH 32/78] Fixed CMake build --- LiteCore/Support/ActorProperty.cc | 36 ----------------------- LiteCore/Support/Async.cc | 1 - Networking/BLIP/cmake/platform_base.cmake | 5 ++-- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 -- 4 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 LiteCore/Support/ActorProperty.cc diff --git a/LiteCore/Support/ActorProperty.cc b/LiteCore/Support/ActorProperty.cc deleted file mode 100644 index dd75005b9..000000000 --- a/LiteCore/Support/ActorProperty.cc +++ /dev/null @@ -1,36 +0,0 @@ -// -// ActorProperty.cc -// -// Copyright 2017-Present Couchbase, Inc. -// -// Use of this software is governed by the Business Source License included -// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified -// in that file, in accordance with the Business Source License, use of this -// software will be governed by the Apache License, Version 2.0, included in -// the file licenses/APL2.txt. -// - -#include "ActorProperty.hh" - -using namespace std; - -namespace litecore { namespace actor { - - template - PropertyImpl& PropertyImpl::operator= (const T &t) { - if (t != _value) { - _value = t; - for (auto &observer : _observers) { - observer(_value); - } - } - return *this; - } - - - template - void PropertyImpl::addObserver(Observer &observer) { - _observers.push_back(observer); - } - -} } diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 477954624..915372988 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -13,7 +13,6 @@ #include "Async.hh" #include "Actor.hh" #include "c4Error.h" -#include "c4Internal.hh" #include "Logging.hh" #include "betterassert.hh" diff --git a/Networking/BLIP/cmake/platform_base.cmake b/Networking/BLIP/cmake/platform_base.cmake index 533bc57ff..1226c1734 100644 --- a/Networking/BLIP/cmake/platform_base.cmake +++ b/Networking/BLIP/cmake/platform_base.cmake @@ -15,11 +15,10 @@ function(set_source_files_base) ${WEBSOCKETS_LOCATION}/WebSocketImpl.cc ${WEBSOCKETS_LOCATION}/WebSocketInterface.cc ${SUPPORT_LOCATION}/Actor.cc - ${SUPPORT_LOCATION}/ActorProperty.cc -# ${SUPPORT_LOCATION}/Async.cc + ${SUPPORT_LOCATION}/Async.cc ${SUPPORT_LOCATION}/Channel.cc ${SUPPORT_LOCATION}/Codec.cc ${SUPPORT_LOCATION}/Timer.cc PARENT_SCOPE ) -endfunction() \ No newline at end of file +endfunction() diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index eeeb7fa4f..859dea976 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1014,7 +1014,6 @@ 2744B33C241854F2005A194D /* Batcher.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Batcher.hh; sourceTree = ""; }; 2744B33D241854F2005A194D /* ThreadUtil.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ThreadUtil.hh; sourceTree = ""; }; 2744B33E241854F2005A194D /* Codec.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Codec.hh; sourceTree = ""; }; - 2744B33F241854F2005A194D /* ActorProperty.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ActorProperty.cc; sourceTree = ""; }; 2744B340241854F2005A194D /* Actor.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Actor.hh; sourceTree = ""; }; 2744B341241854F2005A194D /* Async.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Async.hh; sourceTree = ""; }; 2744B342241854F2005A194D /* Channel.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Channel.cc; sourceTree = ""; }; @@ -2076,7 +2075,6 @@ 2744B339241854F2005A194D /* GCDMailbox.hh */, 2744B33A241854F2005A194D /* ThreadedMailbox.cc */, 2744B33B241854F2005A194D /* GCDMailbox.cc */, - 2744B33F241854F2005A194D /* ActorProperty.cc */, 2744B342241854F2005A194D /* Channel.cc */, 2744B344241854F2005A194D /* Channel.hh */, 274C1DC325C4A79C00B0EEAC /* ChannelManifest.cc */, From ad517d382f039e4aef39bc658250b0d21ea7119c Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Mar 2022 12:24:00 -0800 Subject: [PATCH 33/78] Result::value() now throws the error as an exception It used to just throw AssertionFailed, but it makes much more sense to throw the error it holds, and that might sometimes even be useful if you use it in a function that throws exceptions. Also, `_value` is a faster version of `value` that doesn't check for errors, so it'll instead crash with a null-deref. Use it when you already checked there's no error, and need the speed. --- LiteCore/Support/Async.hh | 6 +++--- LiteCore/Support/Result.hh | 25 ++++++++++++++++++------- LiteCore/tests/AsyncTest.cc | 4 ++-- Networking/BLIP/docs/Result.md | 24 ++++++++++++++++++++++-- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index bf4366ae8..d5a5db2e5 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -160,7 +160,7 @@ namespace litecore::actor { if (hasError()) return Async(error()); try { - return callback(moveResult().value()); + return callback(moveResult()._value()); } catch (const std::exception &x) { return Async(C4Error::fromException(x)); } @@ -272,7 +272,7 @@ namespace litecore::actor { if (provider.hasError()) result->setResult(provider.error()); else { - callback(provider.moveResult().value()); + callback(provider.moveResult()._value()); result->setResult(Result()); } }); @@ -407,7 +407,7 @@ namespace litecore::actor { if (provider.hasError()) errorCallback(provider.error()); else - callback(provider.moveResult().value()); + callback(provider.moveResult()._value()); }); } diff --git a/LiteCore/Support/Result.hh b/LiteCore/Support/Result.hh index 81a9bdb3e..f9258ad73 100644 --- a/LiteCore/Support/Result.hh +++ b/LiteCore/Support/Result.hh @@ -52,9 +52,16 @@ namespace litecore { /// True if not successful. bool isError() const noexcept {return _result.index() != 0;} - /// Returns the value. You must test first, as this will fail if there is an error! - T& value() & {return *std::get_if<0>(&_result);} - T value() && {return std::move(*std::get_if<0>(&_result));} + /// Returns the value. Or if there's an error, throws it as an exception(!) + T& value() & { + if (auto e = errorPtr(); _usuallyFalse(e != nullptr)) e->raise(); + return *std::get_if<0>(&_result); + } + + T value() && { + if (auto e = errorPtr(); _usuallyFalse(e != nullptr)) e->raise(); + return std::move(*std::get_if<0>(&_result)); + } /// Returns the error, or an empty C4Error with code==0 if none. C4Error error() const noexcept {auto e = errorPtr(); return e ? *e : C4Error{};} @@ -87,6 +94,10 @@ namespace litecore { return *this; } + // `_value` is faster than `value`, but you MUST have preflighted or it'll deref NULL. + T& _value() & noexcept {return *std::get_if<0>(&_result);} + T _value() && noexcept {return std::move(*std::get_if<0>(&_result));} + private: template Result _then(fleece::function_ref const& fn) noexcept; @@ -192,11 +203,11 @@ namespace litecore { /// name (`foo`) or a declaration (`int foo`). /// - If the result is an error, that error is returned from the current function, which should /// have a return type of `Result<>` or `C4Error`. - #define TRY_RESULT(VAR, EXPR) \ + #define TRY(VAR, EXPR) \ auto CONCATENATE(rslt, __LINE__) = (EXPR); \ if (CONCATENATE(rslt, __LINE__).isError()) \ return CONCATENATE(rslt, __LINE__).error(); \ - VAR = std::move(CONCATENATE(rslt, __LINE__)).value(); + VAR = std::move(CONCATENATE(rslt, __LINE__))._value(); // (`CONCATENATE(rslt, __LINE__)` is just a clumsy way to create a unique variable name.) @@ -208,7 +219,7 @@ namespace litecore { [[nodiscard]] Result Result::_then(fleece::function_ref const& fn) noexcept { if (ok()) - return TryResult([&]{return fn(std::move(value()));}); + return TryResult([&]{return fn(std::move(_value()));}); else return error(); } @@ -218,7 +229,7 @@ namespace litecore { [[nodiscard]] Result Result::_then(fleece::function_ref(T&&)> const& fn) noexcept { if (ok()) - return TryResult([&]{return fn(std::move(value()));}); + return TryResult([&]{return fn(std::move(_value()));}); else return error(); } diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 3984d8d56..27905e086 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -204,8 +204,8 @@ TEST_CASE("TryResult", "[Async]") { TEST_CASE("TRY", "[Async]") { auto fn = [](int x) -> Result { - TRY_RESULT(string str, rfunc(x)); - TRY_RESULT(string str2, rfunc(x)); + TRY(string str, rfunc(x)); + TRY(string str2, rfunc(x)); return str.size(); }; diff --git a/Networking/BLIP/docs/Result.md b/Networking/BLIP/docs/Result.md index efff36707..224abc151 100644 --- a/Networking/BLIP/docs/Result.md +++ b/Networking/BLIP/docs/Result.md @@ -1,6 +1,6 @@ # The Useful `Result` Type -(Last updated March 3 2022 by Jens) +(Last updated March 4 2022 by Jens) **Result** is a utility class template for improving error handling without exceptions, inspired by languages like Swift and Rust. @@ -14,7 +14,7 @@ In these situations we’ve been using the same calling convention we use in the - You can construct one from either a `T` or a `C4Error`. - Boolean methods `ok()` and `isError()` tell you which it holds. -- `value()` returns the value, but if there’s an error it throws it instead. (So check first!) +- `value()` returns the value … but if there’s an error it throws it instead. (So check first!) - `error()` returns the error if there is one, or else a default error with `code==0`. Result’s main job is as the return value of a function that can fail: @@ -93,5 +93,25 @@ Here’s an example that goes the other direction, `string` to `double`, and the Result root = parseDouble(str).then( [](double n) {return sqareRoot(n);} ); ``` +### TRY() +This one’s sort of an experiment to emulate Swift’s `try` statement (`try mightFail()`). Here’s an example: +```c++ +Result rootStr(double n) { + TRY(double root, squareRoot(n)); + return std::to_string(root); +} +``` + +TRY calls `squareRoot(n)`; if it succeeds, it declares `root` and assigns the value to it. If it fails, though, it *returns the error from the enclosing function* (`rootStr`). + +The `TRY` expression in the example is equivalent to: + +```c++ +auto __ = squareRoot(n); +if (__.isError()) return __.error(); +double root = __.value(); +``` + +The syntax is ugly, but I think it’s the best that can be done in standard C++. (GCC and Clang have an extension that would make it a lot prettier, but MSVC doesn’t.) From a416ffb4e80057348c05d8d877f8c837e078b599 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Mar 2022 12:59:59 -0800 Subject: [PATCH 34/78] Result: Cleanup --- LiteCore/Support/Result.hh | 27 ++- LiteCore/tests/AsyncTest.cc | 203 --------------------- LiteCore/tests/CMakeLists.txt | 4 +- LiteCore/tests/ResultTest.cc | 220 +++++++++++++++++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 6 +- {Networking/BLIP/docs => docs}/Result.md | 40 +++-- 6 files changed, 269 insertions(+), 231 deletions(-) create mode 100644 LiteCore/tests/ResultTest.cc rename {Networking/BLIP/docs => docs}/Result.md (65%) diff --git a/LiteCore/Support/Result.hh b/LiteCore/Support/Result.hh index f9258ad73..39824c279 100644 --- a/LiteCore/Support/Result.hh +++ b/LiteCore/Support/Result.hh @@ -11,18 +11,17 @@ // #pragma once +#include "c4Error.h" #include "Defer.hh" // for CONCATENATE() #include "function_ref.hh" #include #include #include -struct C4Error; - namespace litecore { template class Result; - // !!! Documentation is at Replicator/docs/Result.md !!! + // !!! Documentation is at docs/Result.md !!! namespace { // Magic template gunk. `unwrap_Result` removes a layer of `Result<...>` from type T @@ -71,7 +70,7 @@ namespace litecore { /// Transforms a `Result` to a `Result` by passing the value through a function. /// - If I have a value, I pass it to `fn` and return its result. - /// * If `fn` throws an exception, it's caught and returned (thanks to `TryResult()`.) + /// * If `fn` throws an exception, it's caught and returned (thanks to `CatchResult()`.) /// - If I have an error, `fn` is _not_ called, and I return my error. /// @param fn A function/lambda that takes a `T&&` and returns `U` or `Result`. /// @return The result of `fn`, or else my current error, as a `Result`. @@ -152,7 +151,7 @@ namespace litecore { /// catching any exception and returning it as an error. Returns `Result`. template [[nodiscard]] - Result TryResult(fleece::function_ref fn) noexcept { + Result CatchResult(fleece::function_ref fn) noexcept { try { return fn(); } catch (std::exception &x) { @@ -165,7 +164,7 @@ namespace litecore { /// catching any exception and returning it as an error. Returns `Result`. template [[nodiscard]] - Result TryResult(fleece::function_ref()> fn) noexcept { + Result CatchResult(fleece::function_ref()> fn) noexcept { try { return fn(); } catch (std::exception &x) { @@ -177,7 +176,7 @@ namespace litecore { // (specialization needed for T=void) template <> [[nodiscard]] - inline Result TryResult(fleece::function_ref fn) noexcept { + inline Result CatchResult(fleece::function_ref fn) noexcept { try { fn(); return {}; @@ -187,13 +186,13 @@ namespace litecore { } - // (this helps the compiler deduce T when TryResult() is called with a lambda) + // (this helps the compiler deduce T when CatchResult() is called with a lambda) template , // return value typename T = unwrap_Result> // RV with `Result<...>` stripped off [[nodiscard]] - inline Result TryResult(LAMBDA fn) noexcept { - return TryResult(fleece::function_ref(std::move(fn))); + inline Result CatchResult(LAMBDA fn) noexcept { + return CatchResult(fleece::function_ref(std::move(fn))); } @@ -219,7 +218,7 @@ namespace litecore { [[nodiscard]] Result Result::_then(fleece::function_ref const& fn) noexcept { if (ok()) - return TryResult([&]{return fn(std::move(_value()));}); + return CatchResult([&]{return fn(std::move(_value()));}); else return error(); } @@ -229,7 +228,7 @@ namespace litecore { [[nodiscard]] Result Result::_then(fleece::function_ref(T&&)> const& fn) noexcept { if (ok()) - return TryResult([&]{return fn(std::move(_value()));}); + return CatchResult([&]{return fn(std::move(_value()));}); else return error(); } @@ -239,7 +238,7 @@ namespace litecore { [[nodiscard]] Result Result::_then(fleece::function_ref const& fn) noexcept { if (ok()) - return TryResult(fn); + return CatchResult(fn); else return error(); } @@ -248,7 +247,7 @@ namespace litecore { [[nodiscard]] Result Result::_then(fleece::function_ref()> const& fn) noexcept { if (ok()) - return TryResult(fn); + return CatchResult(fn); else return error(); } diff --git a/LiteCore/tests/AsyncTest.cc b/LiteCore/tests/AsyncTest.cc index 27905e086..ba265e6ea 100644 --- a/LiteCore/tests/AsyncTest.cc +++ b/LiteCore/tests/AsyncTest.cc @@ -19,209 +19,6 @@ using namespace std; using namespace litecore::actor; -#pragma mark - RESULT: - - -static Result rfunc(int x) { - if (x > 0) - return to_string(x); - else if (x < 0) - return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; - else - throw logic_error("I didn't expect a kind of Spanish Inquisition!"); -} - - -static Result rvfunc(int x) { - if (x > 0) - return {}; - else if (x < 0) - return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; - else - throw logic_error("I didn't expect a kind of Spanish Inquisition!"); -} - - -static string xfunc(int x) { - if (x >= 0) - return to_string(x); - else - C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}.raise(); -} - - -TEST_CASE("Result", "[Async]") { - auto r = rfunc(1); - CHECK(r.ok()); - CHECK(r.value() == "1"); - CHECK(r.error() == kC4NoError); - CHECK(r.errorPtr() == nullptr); - - r = rfunc(-1); - CHECK(!r.ok()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - CHECK(r.errorPtr() != nullptr); - CHECK(*r.errorPtr() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); -} - - -// Test Result::then -TEST_CASE("Result then", "[Async]") { - SECTION("Success") { - Result r = rfunc(11).then([](string &&str) { return str.size();}); - REQUIRE(r.ok()); - CHECK(r.value() == 2); - } - SECTION("Error") { - Result r = rfunc(-1).then([](string &&str) { return str.size();}); - REQUIRE(r.isError()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } - - SECTION("Success, returning Result") { - Result r = rfunc(11).then([](string &&str) -> Result { return str.size();}); - REQUIRE(r.ok()); - CHECK(r.value() == 2); - } - SECTION("Error, returning Result") { - Result r = rfunc(11).then([](string &&str) -> Result { - return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; - }); - REQUIRE(r.isError()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } -} - - -// Test Result::then() -TEST_CASE("Result void then", "[Async]") { - SECTION("Success") { - Result r = rvfunc(11).then([]() { return 2;}); - REQUIRE(r.ok()); - CHECK(r.value() == 2); - } - SECTION("Error") { - Result r = rvfunc(-1).then([]() { return 1;}); - REQUIRE(r.isError()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } - - SECTION("Success, returning Result") { - Result r = rvfunc(11).then([]() -> Result { return 2;}); - REQUIRE(r.ok()); - CHECK(r.value() == 2); - } - SECTION("Error, returning Result") { - Result r = rvfunc(11).then([]() -> Result { - return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; - }); - REQUIRE(r.isError()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } -} - - -// Test Result.then(), where the fn returns void -TEST_CASE("Result then void", "[Async]") { - SECTION("Success") { - optional calledWith; - Result r = rfunc(11).then([&](string &&str) { calledWith = str; }); - REQUIRE(r.ok()); - CHECK(calledWith == "11"); - } - SECTION("Error") { - optional calledWith; - Result r = rfunc(-1).then([&](string &&str) { calledWith = str; }); - REQUIRE(r.isError()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } - - SECTION("Success, returning Result") { - optional calledWith; - Result r = rfunc(11).then([&](string &&str) -> Result { - calledWith = str; return {}; - }); - REQUIRE(r.ok()); - CHECK(calledWith == "11"); - } - SECTION("Error, returning Result") { - optional calledWith; - Result r = rfunc(11).then([&](string &&str) -> Result { - calledWith = str; return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; - }); - REQUIRE(r.isError()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - CHECK(calledWith == "11"); - } -} - - -TEST_CASE("Result onError", "[Async]") { - SECTION("Success") { - optional calledWithErr; - Result r = rfunc(11).onError([&](C4Error err) {calledWithErr = err;}); - REQUIRE(r.ok()); - CHECK(r.value() == "11"); - CHECK(!calledWithErr); - } - SECTION("Error") { - optional calledWithErr; - Result r = rfunc(-1).onError([&](C4Error err) {calledWithErr = err;}); - REQUIRE(r.isError()); - CHECK(calledWithErr == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } -} - -TEST_CASE("TryResult", "[Async]") { - SECTION("Success") { - auto r = TryResult([]{ return xfunc(4);}); - CHECK(r.value() == "4"); - } - - SECTION("Exception") { - ExpectingExceptions x; - auto r = TryResult([]{ return xfunc(-1);}); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } - - SECTION("Success when lambda returns Result") { - auto r = TryResult([]{ return rfunc(4);}); - CHECK(r.value() == "4"); - } - - SECTION("Error when lambda returns Result") { - auto r = TryResult([]{ return rfunc(-1);}); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); - } - - SECTION("Exception when lambda returns Result") { - ExpectingExceptions x; - auto r = TryResult([]{ return rfunc(0);}); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorAssertionFailed}); - } -} - - -TEST_CASE("TRY", "[Async]") { - auto fn = [](int x) -> Result { - TRY(string str, rfunc(x)); - TRY(string str2, rfunc(x)); - return str.size(); - }; - - Result r = fn(1234); - REQUIRE(r.ok()); - CHECK(r.value() == 4); - - r = fn(-1); - REQUIRE(!r.ok()); - CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); -} - - -#pragma mark - ASYNC: - - static Async downloader(string url) { auto provider = Async::makeProvider(); std::thread t([=] { diff --git a/LiteCore/tests/CMakeLists.txt b/LiteCore/tests/CMakeLists.txt index ea8853827..0abf02f11 100644 --- a/LiteCore/tests/CMakeLists.txt +++ b/LiteCore/tests/CMakeLists.txt @@ -51,13 +51,15 @@ file(GLOB FLEECE_FILES "../../vendor/fleece/Tests/*.json" "../../vendor/fleece/T file(COPY ${FLEECE_FILES} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/vendor/fleece/Tests) add_executable( CppTests + LogEncoderTest.cc + ResultTest.cc c4BaseTest.cc c4DocumentTest_Internal.cc + AsyncTest.cc DataFileTest.cc DocumentKeysTest.cc FTSTest.cc LiteCoreTest.cc - LogEncoderTest.cc N1QLParserTest.cc PredictiveQueryTest.cc QueryParserTest.cc diff --git a/LiteCore/tests/ResultTest.cc b/LiteCore/tests/ResultTest.cc new file mode 100644 index 000000000..bd3cd08c9 --- /dev/null +++ b/LiteCore/tests/ResultTest.cc @@ -0,0 +1,220 @@ +// +// ResultTest.cc +// +// Copyright © 2022 Couchbase. All rights reserved. +// +// 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. +// + +#include "Result.hh" +#include "LiteCoreTest.hh" + +using namespace std; +using namespace litecore; + + +static Result rfunc(int x) { + if (x > 0) + return to_string(x); + else if (x < 0) + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + else + throw logic_error("I didn't expect a kind of Spanish Inquisition!"); +} + + +static Result rvfunc(int x) { + if (x > 0) + return {}; + else if (x < 0) + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + else + throw logic_error("I didn't expect a kind of Spanish Inquisition!"); +} + + +static string xfunc(int x) { + if (x >= 0) + return to_string(x); + else + C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}.raise(); +} + + +TEST_CASE("Result", "[Result]") { + auto r = rfunc(1); + CHECK(r.ok()); + CHECK(r.value() == "1"); + CHECK(r.error() == kC4NoError); + CHECK(r.errorPtr() == nullptr); + + r = rfunc(-1); + CHECK(!r.ok()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + CHECK(r.errorPtr() != nullptr); + CHECK(*r.errorPtr() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); +} + + +// Test Result::then +TEST_CASE("Result then", "[Result]") { + SECTION("Success") { + Result r = rfunc(11).then([](string &&str) { return str.size();}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error") { + Result r = rfunc(-1).then([](string &&str) { return str.size();}); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success, returning Result") { + Result r = rfunc(11).then([](string &&str) -> Result { return str.size();}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error, returning Result") { + Result r = rfunc(11).then([](string &&str) -> Result { + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + + +// Test Result::then() +TEST_CASE("Result void then", "[Result]") { + SECTION("Success") { + Result r = rvfunc(11).then([]() { return 2;}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error") { + Result r = rvfunc(-1).then([]() { return 1;}); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success, returning Result") { + Result r = rvfunc(11).then([]() -> Result { return 2;}); + REQUIRE(r.ok()); + CHECK(r.value() == 2); + } + SECTION("Error, returning Result") { + Result r = rvfunc(11).then([]() -> Result { + return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + + +// Test Result.then(), where the fn returns void +TEST_CASE("Result then void", "[Result]") { + SECTION("Success") { + optional calledWith; + Result r = rfunc(11).then([&](string &&str) { calledWith = str; }); + REQUIRE(r.ok()); + CHECK(calledWith == "11"); + } + SECTION("Error") { + optional calledWith; + Result r = rfunc(-1).then([&](string &&str) { calledWith = str; }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success, returning Result") { + optional calledWith; + Result r = rfunc(11).then([&](string &&str) -> Result { + calledWith = str; return {}; + }); + REQUIRE(r.ok()); + CHECK(calledWith == "11"); + } + SECTION("Error, returning Result") { + optional calledWith; + Result r = rfunc(11).then([&](string &&str) -> Result { + calledWith = str; return C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}; + }); + REQUIRE(r.isError()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + CHECK(calledWith == "11"); + } +} + + +TEST_CASE("Result onError", "[Result]") { + SECTION("Success") { + optional calledWithErr; + Result r = rfunc(11).onError([&](C4Error err) {calledWithErr = err;}); + REQUIRE(r.ok()); + CHECK(r.value() == "11"); + CHECK(!calledWithErr); + } + SECTION("Error") { + optional calledWithErr; + Result r = rfunc(-1).onError([&](C4Error err) {calledWithErr = err;}); + REQUIRE(r.isError()); + CHECK(calledWithErr == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } +} + +TEST_CASE("CatchResult", "[Result]") { + SECTION("Success") { + auto r = CatchResult([]{ return xfunc(4);}); + CHECK(r.value() == "4"); + } + + SECTION("Exception") { + ExpectingExceptions x; + auto r = CatchResult([]{ return xfunc(-1);}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Success when lambda returns Result") { + auto r = CatchResult([]{ return rfunc(4);}); + CHECK(r.value() == "4"); + } + + SECTION("Error when lambda returns Result") { + auto r = CatchResult([]{ return rfunc(-1);}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); + } + + SECTION("Exception when lambda returns Result") { + ExpectingExceptions x; + auto r = CatchResult([]{ return rfunc(0);}); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorAssertionFailed}); + } +} + + +TEST_CASE("TRY", "[Result]") { + auto fn = [](int x) -> Result { + TRY(string str, rfunc(x)); + TRY(string str2, rfunc(x)); + return str.size(); + }; + + Result r = fn(1234); + REQUIRE(r.ok()); + CHECK(r.value() == 4); + + r = fn(-1); + REQUIRE(!r.ok()); + CHECK(r.error() == C4Error{LiteCoreDomain, kC4ErrorInvalidParameter}); +} diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 859dea976..0d4977687 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -206,6 +206,7 @@ 276683B81DC7DD2E00E3F187 /* SequenceTracker.hh in Headers */ = {isa = PBXBuildFile; fileRef = 276683B51DC7DD2E00E3F187 /* SequenceTracker.hh */; }; 2769438C1DCD502A00DB2555 /* c4Observer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2769438B1DCD502A00DB2555 /* c4Observer.cc */; }; 2769438F1DD0ED3F00DB2555 /* c4ObserverTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2769438E1DD0ED3F00DB2555 /* c4ObserverTest.cc */; }; + 27697DDC27D2B32D006F5BB5 /* ResultTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27697DDB27D2B32D006F5BB5 /* ResultTest.cc */; }; 276993E625390C3300FDF699 /* VectorRecord.cc in Sources */ = {isa = PBXBuildFile; fileRef = 276993E525390C3300FDF699 /* VectorRecord.cc */; }; 276CE6832267991500B681AC /* n1ql.cc in Sources */ = {isa = PBXBuildFile; fileRef = 276CE67C2267991400B681AC /* n1ql.cc */; settings = {COMPILER_FLAGS = "-Wno-unreachable-code"; }; }; 276D152B1DFB878800543B1B /* c4DocumentTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27E0CA9D1DBEAA130089A9C0 /* c4DocumentTest.cc */; }; @@ -1171,6 +1172,7 @@ 2769438B1DCD502A00DB2555 /* c4Observer.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4Observer.cc; sourceTree = ""; }; 2769438E1DD0ED3F00DB2555 /* c4ObserverTest.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4ObserverTest.cc; sourceTree = ""; }; 27697DD727D19B25006F5BB5 /* Result.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Result.md; sourceTree = ""; }; + 27697DDB27D2B32D006F5BB5 /* ResultTest.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ResultTest.cc; sourceTree = ""; }; 276993E125390C3300FDF699 /* VectorRecord.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = VectorRecord.hh; sourceTree = ""; }; 276993E525390C3300FDF699 /* VectorRecord.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = VectorRecord.cc; sourceTree = ""; }; 276CE67C2267991400B681AC /* n1ql.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = n1ql.cc; sourceTree = ""; }; @@ -1837,6 +1839,7 @@ 2771991B2272498300B18E0A /* QueryParserTest.hh */, 27E6737C1EC78144008F50C4 /* QueryTest.cc */, 2723410F211B5FC400DA9437 /* QueryTest.hh */, + 27697DDB27D2B32D006F5BB5 /* ResultTest.cc */, 27456AFC1DC9507D00A38B20 /* SequenceTrackerTest.cc */, 27FDF1421DAC22230087B4E6 /* SQLiteFunctionsTest.cc */, 272850B41E9BE361009CA22F /* UpgraderTest.cc */, @@ -2105,6 +2108,7 @@ isa = PBXGroup; children = ( 2745B7CB26825F970012A17A /* index.html */, + 27697DD727D19B25006F5BB5 /* Result.md */, 2745B7CC26825F970012A17A /* overview */, ); name = docs; @@ -2933,7 +2937,6 @@ children = ( 27DF3BC326867F2600A57A1E /* Actors.md */, 277C5C4827AC4E0E001BE212 /* Async.md */, - 27697DD727D19B25006F5BB5 /* Result.md */, 27DF3BC426867F2600A57A1E /* logo.png */, 27DF3BC526867F2600A57A1E /* BLIP Protocol.md */, ); @@ -3953,6 +3956,7 @@ 275067DC230B6AD500FA23B2 /* c4Listener.cc in Sources */, 274D17C22615445B0018D39C /* DBAccessTestWrapper.cc in Sources */, 27FA09A01D6FA380005888AA /* DataFileTest.cc in Sources */, + 27697DDC27D2B32D006F5BB5 /* ResultTest.cc in Sources */, 27E0CAA01DBEB0BA0089A9C0 /* DocumentKeysTest.cc in Sources */, 277C5C4627AB1EB4001BE212 /* AsyncTest.cc in Sources */, 27505DDD256335B000123115 /* VersionVectorTest.cc in Sources */, diff --git a/Networking/BLIP/docs/Result.md b/docs/Result.md similarity index 65% rename from Networking/BLIP/docs/Result.md rename to docs/Result.md index 224abc151..0eaebaaa6 100644 --- a/Networking/BLIP/docs/Result.md +++ b/docs/Result.md @@ -2,22 +2,24 @@ (Last updated March 4 2022 by Jens) -**Result** is a utility class template for improving error handling without exceptions, inspired by languages like Swift and Rust. +**Result is a utility class template for improving error handling without exceptions, inspired by languages like Swift, Kotlin and Rust.** + +## 1. Why? We still use exceptions inside LiteCore, but in some places it’s better to manage errors as `C4Error` values, usually when the error isn’t considered an “exceptional” situation where something’s gone unexpectedly wrong. An example of this is when saving a document — it’s entirely possible to get a kC4ErrorConflict in normal operation, so it shouldn’t be thrown. And in the case of asynchronous operations (`Async`), exceptions don’t make sense at all. In these situations we’ve been using the same calling convention we use in the C API: a special return value like `NULL` or `0` indicating failure, and a `C4Error*` parameter that the callee copies the error to. But that’s kind of awkward. We can do better. -## 1. What’s a `Result`? +## 2. What’s a `Result`? -`Result`, defined in the header `Result.hh`, is a container that can hold either a value of type `T` or a `C4Error`. +`Result`, defined in the header `Result.hh`, is **a container that can hold either a value of type `T` or a `C4Error`.** (It’s implemented using `std::variant`.) - You can construct one from either a `T` or a `C4Error`. - Boolean methods `ok()` and `isError()` tell you which it holds. - `value()` returns the value … but if there’s an error it throws it instead. (So check first!) - `error()` returns the error if there is one, or else a default error with `code==0`. -Result’s main job is as the return value of a function that can fail: +**Result’s main job is as the return value of a function that can fail:** ```c++ Result squareRoot(double n) { @@ -28,7 +30,7 @@ Result squareRoot(double n) { } ``` -Note that one branch returns a `double` and the other a `C4Error`. That’s fine since the actual return type can be constructed from either one. +> Note that one branch returns a `double` and the other a `C4Error`. That’s fine since they both convert implicitly to the actual return type, `Result`. ### `Result` @@ -36,7 +38,7 @@ Note that one branch returns a `double` and the other a `C4Error`. That’s fine ## 3. What Do You Do With One? -If you call a function that returns Result, you can check what it holds and do the appropriate thing: +If you called a function that returned a Result, you can check what it holds and do the appropriate thing: ```c++ if (auto r = squareRoot(n); r.ok()) { @@ -59,16 +61,14 @@ Result showSquareRoot(double n) { } ``` -## 4. Useful Helpers - -### TryResult() +### CatchResult() -`TryResult` lets you safely call a function that may throw an exception. Itakes a function/lambda that returns `T` (or `Result`), calls it, and returns the result as a `Result`. If the function throws an exception, it is caught and returned as the `error` in the result. +`CatchResult` lets you safely call a function that may throw an exception. Itakes a function/lambda that returns `T` (or `Result`), calls it, and returns the result as a `Result`. If the function throws an exception, it is caught and returned as the `error` in the result. ```c++ extern string read_line(stream*); // throws exception on I/O error -Result input = TryResult( []{ return read_line(in); }); +Result input = CatchResult( []{ return read_line(in); }); ``` ### then() @@ -93,6 +93,22 @@ Here’s an example that goes the other direction, `string` to `double`, and the Result root = parseDouble(str).then( [](double n) {return sqareRoot(n);} ); ``` +### onError() + +The `onError` method is sort of the opposite of `then`: it takes a function/lambda and calls it with the _error_ value, if there is one; otherwise it does nothing. + +`onError` returns itself (`*this`) since it hasn’t dealt with the non-error value and you need to do so. However, `Result::onError` returns `void` because there’s no non-error value. + +You can chain `then` and `onError` to handle both cases: + +```c++ +squareRoot(n).then( [](double root) { + cout << "√n = " << root.value() << endl; +}).onError( [](C4Error error) { + cerr << "squareRoot failed: " << error.description() << endl; +}); +``` + ### TRY() This one’s sort of an experiment to emulate Swift’s `try` statement (`try mightFail()`). Here’s an example: @@ -114,4 +130,4 @@ if (__.isError()) return __.error(); double root = __.value(); ``` -The syntax is ugly, but I think it’s the best that can be done in standard C++. (GCC and Clang have an extension that would make it a lot prettier, but MSVC doesn’t.) +> The syntax is ugly, but I think it’s the best that can be done in standard C++. (GCC and Clang have a “statement expression” extension that would let us say `double root = TRY(squareRoot(n));`, but MSVC doesn’t support it.) From 65c5b0c35c888e711d7a04c05355f07d43e0c7ae Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 4 Mar 2022 13:21:54 -0800 Subject: [PATCH 35/78] GCC & MSVC fixes --- LiteCore/Support/Actor.hh | 19 ++++++++++--------- LiteCore/Support/Async.cc | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/LiteCore/Support/Actor.hh b/LiteCore/Support/Actor.hh index 581e5b4ef..9a12cdac8 100644 --- a/LiteCore/Support/Actor.hh +++ b/LiteCore/Support/Actor.hh @@ -188,15 +188,6 @@ namespace litecore::actor { return provider->asyncValue(); } - // Specialization of `asCurrentActor` where `fn` returns void. - template <> - auto _asCurrentActor(std::function fn) { - if (currentActor() == this) - fn(); - else - _mailbox.enqueue("asCurrentActor", ACTOR_BIND_FN0(fn)); - } - // Implementation of `asCurrentActor` where `fn` itself returns an `Async`. template Async _asCurrentActor(std::function()> fn) { @@ -212,6 +203,16 @@ namespace litecore::actor { Mailbox _mailbox; }; + + // Specialization of `asCurrentActor` where `fn` returns void. + template <> + inline auto Actor::_asCurrentActor(std::function fn) { + if (currentActor() == this) + fn(); + else + _mailbox.enqueue("asCurrentActor", ACTOR_BIND_FN0(fn)); + } + #undef ACTOR_BIND_METHOD #undef ACTOR_BIND_METHOD0 #undef ACTOR_BIND_FN diff --git a/LiteCore/Support/Async.cc b/LiteCore/Support/Async.cc index 915372988..1cf35eb54 100644 --- a/LiteCore/Support/Async.cc +++ b/LiteCore/Support/Async.cc @@ -102,11 +102,11 @@ namespace litecore::actor { condition_variable _cond; _provider->setObserver(nullptr, [&](AsyncProviderBase &provider) { - unique_lock lock(_mutex); + unique_lock lock(_mutex); _cond.notify_one(); }); - unique_lock lock(_mutex); + unique_lock lock(_mutex); _cond.wait(lock, [&]{return ready();}); } } From e5f79c5e389315308dd448acf191b520764e030b Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 7 Mar 2022 10:07:17 -0800 Subject: [PATCH 36/78] Xcode: Disable `-fsanitize-ignorelist` flag until Jenkins is upgraded See comment in commit. --- Xcode/xcconfigs/Project_Debug.xcconfig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Xcode/xcconfigs/Project_Debug.xcconfig b/Xcode/xcconfigs/Project_Debug.xcconfig index 2fe570a9b..9d6b6c46b 100644 --- a/Xcode/xcconfigs/Project_Debug.xcconfig +++ b/Xcode/xcconfigs/Project_Debug.xcconfig @@ -14,4 +14,11 @@ GCC_PREPROCESSOR_DEFINITIONS = $(inherited) DEBUG=1 _LIBCPP_DEBUG=0 ENABLE_TESTABILITY = YES MTL_ENABLE_DEBUG_INFO = YES -OTHER_CFLAGS = $(inherited) -fsanitize-ignorelist=$(SRCROOT)/sanitizer-ignore-list.txt +// This flag turns off the Address Sanitizer for the files or functions listed in the .txt file. +// I added this to speed up logging, so it doesn't introduce as much of a slowdown which can +// perturb test results and prevent some timing-sensitive race conditions from showing up. +// +// However, this compiler flag wasn't added until Xcode 13(?); at least, the version of Xcode +// currently on Jenkins doesn't support it, so I'm commenting this out for now. --Jens +// TODO: Re-enable this when Jenkins is upgraded. +//OTHER_CFLAGS = $(inherited) -fsanitize-ignorelist=$(SRCROOT)/sanitizer-ignore-list.txt From de5316df37fe7cab59a3e8af03f3ddd2595af0de Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Thu, 10 Mar 2022 22:39:10 -0800 Subject: [PATCH 37/78] c4connectedclient API * expose c4client_new and c4client_getDoc APIs --- C/Cpp_include/c4.hh | 1 + C/Cpp_include/c4ConnectedClient.hh | 29 ++++++++++ C/c4.def | 3 ++ C/c4.exp | 3 ++ C/c4.gnu | 3 ++ C/c4_ee.def | 3 ++ C/c4_ee.exp | 3 ++ C/c4_ee.gnu | 3 ++ C/include/c4.h | 1 + C/include/c4Base.h | 2 + C/include/c4ConnectedClient.h | 67 +++++++++++++++++++++++ C/scripts/c4.txt | 3 ++ Replicator/c4ConnectedClient.cc | 28 ++++++++++ Replicator/c4ConnectedClientImpl.hh | 69 ++++++++++++++++++++++++ Replicator/c4ConnectedClient_CAPI.cc | 46 ++++++++++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 16 ++++++ 16 files changed, 280 insertions(+) create mode 100644 C/Cpp_include/c4ConnectedClient.hh create mode 100644 C/include/c4ConnectedClient.h create mode 100644 Replicator/c4ConnectedClient.cc create mode 100644 Replicator/c4ConnectedClientImpl.hh create mode 100644 Replicator/c4ConnectedClient_CAPI.cc diff --git a/C/Cpp_include/c4.hh b/C/Cpp_include/c4.hh index dea2b0e7d..5820bd90a 100644 --- a/C/Cpp_include/c4.hh +++ b/C/Cpp_include/c4.hh @@ -33,3 +33,4 @@ #include "c4Query.hh" #include "c4Replicator.hh" #include "c4Socket.hh" +#include "c4ConnectedClient.hh" diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh new file mode 100644 index 000000000..debd1b34b --- /dev/null +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -0,0 +1,29 @@ +// +// c4ConnectedClient.hh +// +// Copyright 2021-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#pragma once +#include "c4Base.hh" +#include "Async.hh" +#include "C4ConnectedClient.h" + +C4_ASSUME_NONNULL_BEGIN + +struct C4ConnectedClient : public fleece::RefCounted, + public fleece::InstanceCountedIn, + C4Base { + + static Retained newClient(litecore::websocket::WebSocket* NONNULL, C4Slice options); + + virtual litecore::actor::Async getDoc(C4Slice, C4Slice, C4Slice, bool) noexcept=0; +}; + +C4_ASSUME_NONNULL_END diff --git a/C/c4.def b/C/c4.def index fd898596d..e20e0cad8 100644 --- a/C/c4.def +++ b/C/c4.def @@ -416,3 +416,6 @@ FLDictIterator_End FLValue_IsEqual FLValue_ToJSON5 +c4client_new +c4client_getDoc + diff --git a/C/c4.exp b/C/c4.exp index 268a1d0cb..807d3ae6f 100644 --- a/C/c4.exp +++ b/C/c4.exp @@ -414,5 +414,8 @@ _FLDictIterator_End _FLValue_IsEqual _FLValue_ToJSON5 +_c4client_new +_c4client_getDoc + # Apple specific _FLEncoder_WriteNSObject diff --git a/C/c4.gnu b/C/c4.gnu index 922b744c8..0ac9e8793 100644 --- a/C/c4.gnu +++ b/C/c4.gnu @@ -413,6 +413,9 @@ CBL { FLValue_IsEqual; FLValue_ToJSON5; + + c4client_new; + c4client_getDoc; local: *; }; \ No newline at end of file diff --git a/C/c4_ee.def b/C/c4_ee.def index ebcdb09d9..d0e00c987 100644 --- a/C/c4_ee.def +++ b/C/c4_ee.def @@ -455,6 +455,9 @@ FLDictIterator_End FLValue_IsEqual FLValue_ToJSON5 +c4client_new +c4client_getDoc + c4db_URINameFromPath diff --git a/C/c4_ee.exp b/C/c4_ee.exp index 4a8efdb45..639ff6502 100644 --- a/C/c4_ee.exp +++ b/C/c4_ee.exp @@ -453,6 +453,9 @@ _FLDictIterator_End _FLValue_IsEqual _FLValue_ToJSON5 +_c4client_new +_c4client_getDoc + _c4db_URINameFromPath diff --git a/C/c4_ee.gnu b/C/c4_ee.gnu index 3b040791c..9d8edb73d 100644 --- a/C/c4_ee.gnu +++ b/C/c4_ee.gnu @@ -453,6 +453,9 @@ CBL { FLValue_IsEqual; FLValue_ToJSON5; + c4client_new; + c4client_getDoc; + c4db_URINameFromPath; diff --git a/C/include/c4.h b/C/include/c4.h index b8e262d2c..2c3b517e5 100644 --- a/C/include/c4.h +++ b/C/include/c4.h @@ -25,3 +25,4 @@ #include "c4Query.h" #include "c4Replicator.h" #include "c4Socket.h" +#include "c4ConnectedClient.h" diff --git a/C/include/c4Base.h b/C/include/c4Base.h index 3b9e66911..07721f01f 100644 --- a/C/include/c4Base.h +++ b/C/include/c4Base.h @@ -169,6 +169,8 @@ typedef struct C4SocketFactory C4SocketFactory; /** An open stream for writing data to a blob. */ typedef struct C4WriteStream C4WriteStream; +/** Opaque reference to a Connected Client. */ +typedef struct C4ConnectedClient C4ConnectedClient; #pragma mark - REFERENCE COUNTING: diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h new file mode 100644 index 000000000..3a82bcca0 --- /dev/null +++ b/C/include/c4ConnectedClient.h @@ -0,0 +1,67 @@ +// +// c4ConnectedClient.h +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#pragma once +#include "c4Base.h" + +C4_ASSUME_NONNULL_BEGIN +C4API_BEGIN_DECLS + +/** Result of a successful `c4client_getDoc` call. */ +typedef struct { + C4Slice docID; + C4Slice revID; + C4Slice body; + bool deleted; +} C4DocResponse; + +/** Callback for getting the document result. + + @param client The client that initiated the callback. + @param doc Resuting document response. + @param context user-defined parameter given when registering the callback. */ +typedef void (*C4ConnectedClientDocumentResultCallback)(C4ConnectedClient* client, + C4DocResponse doc, + void * C4NULLABLE context); +/** Creates a new connected client + @param socket The web socket through which it is connected + @param options Fleece-encoded dictionary of optional parameters. + @param error Error will be written here if the function fails. + @result A new C4ConnectedClient, or NULL on failure. */ +C4ConnectedClient* c4client_new(C4Socket* socket, + C4Slice options, + C4Error* error) C4API; + +/** Gets the current revision of a document from the server. + + You can set the `unlessRevID` parameter to avoid getting a redundant copy of a + revision you already have. + + @param docID The document ID. + @param collectionID The name of the document's collection, or `nullslice` for default. + @param unlessRevID If non-null, and equal to the current server-side revision ID, + the server will return error {WebSocketDomain, 304}. + @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. + @param callback Callback for getting document. + @param context Client value passed to getDocument callback + @param error On failure, the error info will be stored here. */ +void c4client_getDoc(C4ConnectedClient*, + C4Slice docID, + C4Slice collectionID, + C4Slice unlessRevID, + bool asFleece, + C4ConnectedClientDocumentResultCallback C4NULLABLE callback, + void * C4NULLABLE context, + C4Error* error) C4API; + +C4API_END_DECLS +C4_ASSUME_NONNULL_END diff --git a/C/scripts/c4.txt b/C/scripts/c4.txt index ed36e1e7b..f7ab3e9d4 100644 --- a/C/scripts/c4.txt +++ b/C/scripts/c4.txt @@ -421,3 +421,6 @@ FLDictIterator_End FLValue_IsEqual FLValue_ToJSON5 + +c4client_new +c4client_getDoc diff --git a/Replicator/c4ConnectedClient.cc b/Replicator/c4ConnectedClient.cc new file mode 100644 index 000000000..679bf4024 --- /dev/null +++ b/Replicator/c4ConnectedClient.cc @@ -0,0 +1,28 @@ +// +// c4ConnectedClient.cc +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "c4Base.hh" +#include "c4ConnectedClient.hh" +#include "c4ConnectedClientImpl.hh" +#include "ConnectedClient.hh" + +using namespace litecore::client; +using namespace fleece; + +/*static*/ Retained C4ConnectedClient::newClient(litecore::websocket::WebSocket* socket, C4Slice options) { + try { + return new C4ConnectedClientImpl(socket, options); + } catch (...) { + throw; + } +} + diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh new file mode 100644 index 000000000..71a3886b0 --- /dev/null +++ b/Replicator/c4ConnectedClientImpl.hh @@ -0,0 +1,69 @@ +// +// ConnectedClientImpl.hh +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "c4Base.hh" +#include "ConnectedClient.hh" +#include "C4ConnectedClient.hh" + +namespace litecore::client { + + using namespace litecore::client; + using namespace litecore::websocket; + using namespace litecore::actor; + + struct C4ConnectedClientImpl: public C4ConnectedClient, public ConnectedClient::Delegate { + public: + C4ConnectedClientImpl(WebSocket* NONNULL, C4Slice options) { }; + + protected: +#pragma mark - ConnectedClient Delegate + + virtual void clientGotHTTPResponse(ConnectedClient* NONNULL client, + int status, + const websocket::Headers &headers) { + // TODO: implement + } + virtual void clientGotTLSCertificate(ConnectedClient* NONNULL client, + slice certData) { + // TODO: implement + } + virtual void clientStatusChanged(ConnectedClient* NONNULL client, + ConnectedClient::ActivityLevel level) { + // TODO: implement + } + virtual void clientConnectionClosed(ConnectedClient* NONNULL client, const CloseStatus& status) { + // TODO: implement + } + +#pragma mark - + virtual Async getDoc(C4Slice docID, + C4Slice collectionID, + C4Slice unlessRevID, + bool asFleece) noexcept { + return _client->getDoc(docID, + collectionID, + unlessRevID, + asFleece).then([](DocResponse a) -> C4DocResponse { + return C4DocResponse { + .docID = a.docID, + .body = a.body, + .revID = a.revID, + .deleted = a.deleted, + }; + }); + } + + private: + Retained _client; + }; + +} diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc new file mode 100644 index 000000000..ec796ef14 --- /dev/null +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -0,0 +1,46 @@ +// +// c4ConnectedClient.cpp +// +// Copyright 2017-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + +#include "c4ConnectedClient.h" +#include "c4ConnectedClient.hh" +#include "c4Socket+Internal.hh" +#include "c4ExceptionUtils.hh" +#include "Async.hh" + +using namespace litecore::repl; + +C4ConnectedClient* c4client_new(C4Socket *openSocket, C4Slice options, C4Error *outError) noexcept { + try { + return C4ConnectedClient::newClient(WebSocketFrom(openSocket), options).detach(); + } catchError(outError); + return nullptr; +} + +void c4client_getDoc(C4ConnectedClient* client, + C4Slice docID, + C4Slice collectionID, + C4Slice unlessRevID, + bool asFleece, + C4ConnectedClientDocumentResultCallback callback, + void *context, + C4Error* outError) noexcept { + try { + auto res = client->getDoc(docID, collectionID, unlessRevID, asFleece); + res.then([=](C4DocResponse response) { + return callback(client, response, context); + }).onError([&](C4Error err) { + if (outError) + *outError = err; + }); + } catchError(outError); + return; +} diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 0d4977687..2f7e3d634 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 1A5726B227D74A8900A9B412 /* c4ConnectedClient.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */; }; + 1AE26CC727D9C432003C3043 /* c4ConnectedClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1AE26CC927D9C488003C3043 /* c4ConnectedClient_CAPI.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */; }; 2700BB53216FF2DB00797537 /* CoreML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2700BB4D216FF2DA00797537 /* CoreML.framework */; }; 2700BB5B217005A900797537 /* CoreMLPredictiveModel.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2700BB5A217005A900797537 /* CoreMLPredictiveModel.mm */; }; 2700BB75217905FE00797537 /* libLiteCore-static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27EF81121917EEC600A327B9 /* libLiteCore-static.a */; }; @@ -831,6 +834,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1A5726AF27D7474900A9B412 /* c4ConnectedClient.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4ConnectedClient.hh; sourceTree = ""; }; + 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClient.cc; sourceTree = ""; }; + 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = c4ConnectedClient.h; sourceTree = ""; }; + 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClient_CAPI.cc; sourceTree = ""; }; + 1AE26CCA27D9D456003C3043 /* c4ConnectedClientImpl.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4ConnectedClientImpl.hh; sourceTree = ""; }; 2700BB4D216FF2DA00797537 /* CoreML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreML.framework; path = System/Library/Frameworks/CoreML.framework; sourceTree = SDKROOT; }; 2700BB59217005A900797537 /* CoreMLPredictiveModel.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CoreMLPredictiveModel.hh; sourceTree = ""; }; 2700BB5A217005A900797537 /* CoreMLPredictiveModel.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CoreMLPredictiveModel.mm; sourceTree = ""; }; @@ -2022,6 +2030,7 @@ 272BA50A23F61591000EB6E8 /* c4Query.hh */, 2758E0A725F30A37007C7487 /* c4Replicator.hh */, 274D18862617B4300018D39C /* c4Socket.hh */, + 1A5726AF27D7474900A9B412 /* c4ConnectedClient.hh */, ); path = Cpp_include; sourceTree = ""; @@ -2633,6 +2642,9 @@ 27D9652B23303B0C00F4A51C /* c4IncomingReplicator.hh */, 278BD6891EEB6756000DBF41 /* DatabaseCookies.cc */, 278BD68A1EEB6756000DBF41 /* DatabaseCookies.hh */, + 1AE26CCA27D9D456003C3043 /* c4ConnectedClientImpl.hh */, + 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */, + 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */, ); name = "API implementation"; sourceTree = ""; @@ -3125,6 +3137,7 @@ 2743E33525F8554F006F696D /* c4QueryTypes.h */, 2743E34625F8583D006F696D /* c4ReplicatorTypes.h */, 274D18852617B3660018D39C /* c4SocketTypes.h */, + 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */, 27B64936206971FC00FC12F7 /* LiteCore.h */, ); path = include; @@ -3264,6 +3277,7 @@ 27B649542069723900FC12F7 /* c4Query.h in Headers */, 27B649552069723900FC12F7 /* c4Replicator.h in Headers */, 27B649562069723900FC12F7 /* c4Socket.h in Headers */, + 1AE26CC727D9C432003C3043 /* c4ConnectedClient.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4218,6 +4232,7 @@ buildActionMask = 2147483647; files = ( 2771B01A1FB2817800C6B794 /* SQLiteKeyStore+Indexes.cc in Sources */, + 1AE26CC927D9C488003C3043 /* c4ConnectedClient_CAPI.cc in Sources */, 27393A871C8A353A00829C9B /* Error.cc in Sources */, 27B699DB1F27B50000782145 /* SQLiteN1QLFunctions.cc in Sources */, 2744B36224186142005A194D /* BuiltInWebSocket.cc in Sources */, @@ -4298,6 +4313,7 @@ 274EDDEC1DA2F488003AD158 /* SQLiteKeyStore.cc in Sources */, 27FC8DBD22135BDA0083B033 /* RevFinder.cc in Sources */, 27D74A7C1D4D3F2300D806E0 /* Column.cpp in Sources */, + 1A5726B227D74A8900A9B412 /* c4ConnectedClient.cc in Sources */, 2763011B1F32A7FD004A1592 /* UnicodeCollator_Stub.cc in Sources */, 93CD010D1E933BE100AFB3FA /* Replicator.cc in Sources */, 273855AF25B790B1009D746E /* DatabaseImpl+Upgrade.cc in Sources */, From 4a26a19a9e4f440359ec63832f522e1e70200d09 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Wed, 16 Mar 2022 03:47:56 -0700 Subject: [PATCH 38/78] Update the API to pass only a Configuration and will create the c4ocket and pass it back to platform also start the connected-client class, once its created. --- C/Cpp_include/c4ConnectedClient.hh | 2 +- C/include/c4ConnectedClient.h | 14 +++++++++---- Replicator/c4ConnectedClient.cc | 4 ++-- Replicator/c4ConnectedClientImpl.hh | 31 +++++++++++++++++++++------- Replicator/c4ConnectedClient_CAPI.cc | 9 ++++---- 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index debd1b34b..dced3c922 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -21,7 +21,7 @@ struct C4ConnectedClient : public fleece::RefCounted, public fleece::InstanceCountedIn, C4Base { - static Retained newClient(litecore::websocket::WebSocket* NONNULL, C4Slice options); + static Retained newClient(const C4ConnectedClientParameters ¶ms); virtual litecore::actor::Async getDoc(C4Slice, C4Slice, C4Slice, bool) noexcept=0; }; diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index 3a82bcca0..18aa4e37c 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -24,6 +24,14 @@ typedef struct { bool deleted; } C4DocResponse; +/** Parameters describing a connected client, used when creating a C4ConnectedClient. */ +typedef struct C4ConnectedClientParameters { + C4Slice url; /// C4ConnectedClient::newClient(litecore::websocket::WebSocket* socket, C4Slice options) { +/*static*/ Retained C4ConnectedClient::newClient(const C4ConnectedClientParameters ¶ms) { try { - return new C4ConnectedClientImpl(socket, options); + return new C4ConnectedClientImpl(params); } catch (...) { throw; } diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 71a3886b0..db864f025 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -10,9 +10,10 @@ // the file licenses/APL2.txt. // -#include "c4Base.hh" +#include "c4Base.h" #include "ConnectedClient.hh" #include "C4ConnectedClient.hh" +#include "c4Socket+Internal.hh" namespace litecore::client { @@ -22,25 +23,38 @@ namespace litecore::client { struct C4ConnectedClientImpl: public C4ConnectedClient, public ConnectedClient::Delegate { public: - C4ConnectedClientImpl(WebSocket* NONNULL, C4Slice options) { }; + C4ConnectedClientImpl(const C4ConnectedClientParameters ¶ms) { + if (params.socketFactory) { + // Keep a copy of the C4SocketFactory struct in case original is invalidated: + _customSocketFactory = *params.socketFactory; + _socketFactory = &_customSocketFactory; + } + + auto webSocket = new repl::C4SocketImpl(alloc_slice(params.url), + Role::Client, + alloc_slice(params.options), + _socketFactory); + _client = new ConnectedClient(webSocket, *this, fleece::AllocedDict(params.options)); + _client->start(); + } protected: #pragma mark - ConnectedClient Delegate - virtual void clientGotHTTPResponse(ConnectedClient* NONNULL client, + virtual void clientGotHTTPResponse(ConnectedClient* C4NONNULL client, int status, const websocket::Headers &headers) { // TODO: implement } - virtual void clientGotTLSCertificate(ConnectedClient* NONNULL client, + virtual void clientGotTLSCertificate(ConnectedClient* C4NONNULL client, slice certData) { // TODO: implement } - virtual void clientStatusChanged(ConnectedClient* NONNULL client, + virtual void clientStatusChanged(ConnectedClient* C4NONNULL client, ConnectedClient::ActivityLevel level) { // TODO: implement } - virtual void clientConnectionClosed(ConnectedClient* NONNULL client, const CloseStatus& status) { + virtual void clientConnectionClosed(ConnectedClient* C4NONNULL client, const CloseStatus& status) { // TODO: implement } @@ -63,7 +77,10 @@ namespace litecore::client { } private: - Retained _client; + Retained _client; + const C4SocketFactory* C4NULLABLE _socketFactory {nullptr}; + C4SocketFactory _customSocketFactory {}; // Storage for *_socketFactory if non-null + void* C4NULLABLE _nativeHandle; }; } diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index ec796ef14..0c5aa67fa 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -18,9 +18,9 @@ using namespace litecore::repl; -C4ConnectedClient* c4client_new(C4Socket *openSocket, C4Slice options, C4Error *outError) noexcept { +C4ConnectedClient* c4client_new(C4ConnectedClientParameters params, C4Error *outError) noexcept { try { - return C4ConnectedClient::newClient(WebSocketFrom(openSocket), options).detach(); + return C4ConnectedClient::newClient(params).detach(); } catchError(outError); return nullptr; } @@ -38,8 +38,9 @@ void c4client_getDoc(C4ConnectedClient* client, res.then([=](C4DocResponse response) { return callback(client, response, context); }).onError([&](C4Error err) { - if (outError) - *outError = err; + // TODO: Handle the error here: + + printf("\n Error occured: %s\n", err.description().c_str()); }); } catchError(outError); return; From 0f7e16ccc546faf90a6412478e829eb4ac15018b Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Wed, 16 Mar 2022 23:20:18 -0700 Subject: [PATCH 39/78] add socket options as well as append the blipsync in the URL --- Replicator/c4ConnectedClientImpl.cc | 37 ++++++++++++++++++++++++ Replicator/c4ConnectedClientImpl.hh | 17 +++++++---- Xcode/LiteCore.xcodeproj/project.pbxproj | 4 +++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 Replicator/c4ConnectedClientImpl.cc diff --git a/Replicator/c4ConnectedClientImpl.cc b/Replicator/c4ConnectedClientImpl.cc new file mode 100644 index 000000000..b0597f231 --- /dev/null +++ b/Replicator/c4ConnectedClientImpl.cc @@ -0,0 +1,37 @@ +// +// c4ConnectedClientImpl.cc +// +// Copyright 2022-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. +// + + +#include "c4ConnectedClientImpl.hh" +#include "Replicator.hh" + +using namespace fleece; + +namespace litecore::client { + + alloc_slice C4ConnectedClientImpl::effectiveURL(slice url) { + string newPath = string(url); + if (!url.hasSuffix("/"_sl)) + newPath += "/"; + newPath += "_blipsync"; + return alloc_slice(newPath); + } + + alloc_slice C4ConnectedClientImpl::socketOptions() { + fleece::Encoder enc; + enc.beginDict(); + enc.writeKey(C4STR(kC4SocketOptionWSProtocols)); + enc.writeString(repl::Replicator::ProtocolName().c_str()); + enc.endDict(); + return enc.finish(); + } +} diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index db864f025..47f48573c 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -17,11 +17,11 @@ namespace litecore::client { - using namespace litecore::client; using namespace litecore::websocket; using namespace litecore::actor; struct C4ConnectedClientImpl: public C4ConnectedClient, public ConnectedClient::Delegate { + public: C4ConnectedClientImpl(const C4ConnectedClientParameters ¶ms) { if (params.socketFactory) { @@ -30,9 +30,9 @@ namespace litecore::client { _socketFactory = &_customSocketFactory; } - auto webSocket = new repl::C4SocketImpl(alloc_slice(params.url), + auto webSocket = new repl::C4SocketImpl(effectiveURL(params.url), Role::Client, - alloc_slice(params.options), + socketOptions(), _socketFactory); _client = new ConnectedClient(webSocket, *this, fleece::AllocedDict(params.options)); _client->start(); @@ -75,12 +75,19 @@ namespace litecore::client { }; }); } - + private: + ~C4ConnectedClientImpl() { + if (_client) + _client->stop(); + } + + alloc_slice effectiveURL(slice); + alloc_slice socketOptions(); + Retained _client; const C4SocketFactory* C4NULLABLE _socketFactory {nullptr}; C4SocketFactory _customSocketFactory {}; // Storage for *_socketFactory if non-null - void* C4NULLABLE _nativeHandle; }; } diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 2f7e3d634..0393901db 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ /* Begin PBXBuildFile section */ 1A5726B227D74A8900A9B412 /* c4ConnectedClient.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */; }; + 1A6F835127E305710055C9F8 /* c4ConnectedClientImpl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A6F835027E305710055C9F8 /* c4ConnectedClientImpl.cc */; }; 1AE26CC727D9C432003C3043 /* c4ConnectedClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1AE26CC927D9C488003C3043 /* c4ConnectedClient_CAPI.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */; }; 2700BB53216FF2DB00797537 /* CoreML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2700BB4D216FF2DA00797537 /* CoreML.framework */; }; @@ -836,6 +837,7 @@ /* Begin PBXFileReference section */ 1A5726AF27D7474900A9B412 /* c4ConnectedClient.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4ConnectedClient.hh; sourceTree = ""; }; 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClient.cc; sourceTree = ""; }; + 1A6F835027E305710055C9F8 /* c4ConnectedClientImpl.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClientImpl.cc; sourceTree = ""; }; 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = c4ConnectedClient.h; sourceTree = ""; }; 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClient_CAPI.cc; sourceTree = ""; }; 1AE26CCA27D9D456003C3043 /* c4ConnectedClientImpl.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4ConnectedClientImpl.hh; sourceTree = ""; }; @@ -2643,6 +2645,7 @@ 278BD6891EEB6756000DBF41 /* DatabaseCookies.cc */, 278BD68A1EEB6756000DBF41 /* DatabaseCookies.hh */, 1AE26CCA27D9D456003C3043 /* c4ConnectedClientImpl.hh */, + 1A6F835027E305710055C9F8 /* c4ConnectedClientImpl.cc */, 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */, 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */, ); @@ -4346,6 +4349,7 @@ 2734F61A206ABEB000C982FF /* ReplicatorTypes.cc in Sources */, 2753AF721EBD190600C12E98 /* LogDecoder.cc in Sources */, 275CED451D3ECE9B001DE46C /* TreeDocument.cc in Sources */, + 1A6F835127E305710055C9F8 /* c4ConnectedClientImpl.cc in Sources */, 2708FE5E1CF6197D0022F721 /* RawRevTree.cc in Sources */, 27D9655C23355DC900F4A51C /* SecureSymmetricCrypto.cc in Sources */, 27469D05233D58D900A1EE1A /* c4Certificate.cc in Sources */, From 4d3ce4a62766b0772d13c5bb7d88b77c28b354fe Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Thu, 17 Mar 2022 02:08:38 -0700 Subject: [PATCH 40/78] release the connected client once its done, exposed the start, stop and release API --- C/Cpp_include/c4ConnectedClient.hh | 7 +++++- C/include/c4ConnectedClient.h | 6 +++++ Replicator/ConnectedClient/ConnectedClient.cc | 6 ++++- Replicator/ConnectedClient/ConnectedClient.hh | 3 +++ Replicator/c4ConnectedClient.cc | 1 - Replicator/c4ConnectedClientImpl.hh | 24 +++++++++++++------ Replicator/c4ConnectedClient_CAPI.cc | 15 ++++++++++++ 7 files changed, 52 insertions(+), 10 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index dced3c922..2e32128ae 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -19,11 +19,16 @@ C4_ASSUME_NONNULL_BEGIN struct C4ConnectedClient : public fleece::RefCounted, public fleece::InstanceCountedIn, - C4Base { + C4Base +{ static Retained newClient(const C4ConnectedClientParameters ¶ms); virtual litecore::actor::Async getDoc(C4Slice, C4Slice, C4Slice, bool) noexcept=0; + + virtual void start() noexcept=0; + + virtual void stop() noexcept=0; }; C4_ASSUME_NONNULL_END diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index 18aa4e37c..f5b65a521 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -69,5 +69,11 @@ void c4client_getDoc(C4ConnectedClient*, void * C4NULLABLE context, C4Error* error) C4API; +void c4client_start(C4ConnectedClient*) C4API; + +void c4client_stop(C4ConnectedClient*) C4API; + +void c4client_free(C4ConnectedClient*) C4API; + C4API_END_DECLS C4_ASSUME_NONNULL_END diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index e298f3ccd..3c510eee6 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -45,7 +45,7 @@ namespace litecore::client { void ConnectedClient::setStatus(ActivityLevel status) { - if (status != _status) { + if (status != _status && _delegate) { _status = status; _delegate->clientStatusChanged(this, status); } @@ -77,6 +77,10 @@ namespace litecore::client { } } + void ConnectedClient::terminate() { + _delegate = nullptr; + } + #pragma mark - BLIP DELEGATE: diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index fdb754531..061cf3af5 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -58,7 +58,10 @@ namespace litecore::client { }; void start(); + void stop(); + + void terminate(); //---- CRUD! diff --git a/Replicator/c4ConnectedClient.cc b/Replicator/c4ConnectedClient.cc index dbef491c0..450aa4ba3 100644 --- a/Replicator/c4ConnectedClient.cc +++ b/Replicator/c4ConnectedClient.cc @@ -25,4 +25,3 @@ using namespace fleece; throw; } } - diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 47f48573c..cb4ba6153 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -43,26 +43,26 @@ namespace litecore::client { virtual void clientGotHTTPResponse(ConnectedClient* C4NONNULL client, int status, - const websocket::Headers &headers) { + const websocket::Headers &headers) override { // TODO: implement } virtual void clientGotTLSCertificate(ConnectedClient* C4NONNULL client, - slice certData) { + slice certData) override { // TODO: implement } virtual void clientStatusChanged(ConnectedClient* C4NONNULL client, - ConnectedClient::ActivityLevel level) { + ConnectedClient::ActivityLevel level) override { // TODO: implement } - virtual void clientConnectionClosed(ConnectedClient* C4NONNULL client, const CloseStatus& status) { + virtual void clientConnectionClosed(ConnectedClient* C4NONNULL client, const CloseStatus& status) override { // TODO: implement } #pragma mark - - virtual Async getDoc(C4Slice docID, + Async getDoc(C4Slice docID, C4Slice collectionID, C4Slice unlessRevID, - bool asFleece) noexcept { + bool asFleece) noexcept override { return _client->getDoc(docID, collectionID, unlessRevID, @@ -75,11 +75,21 @@ namespace litecore::client { }; }); } + + virtual void start() noexcept override { + _client->start(); + } + + virtual void stop() noexcept override { + _client->stop(); + } private: ~C4ConnectedClientImpl() { if (_client) - _client->stop(); + _client->terminate(); + + _client = nullptr; } alloc_slice effectiveURL(slice); diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index 0c5aa67fa..721da8232 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -45,3 +45,18 @@ void c4client_getDoc(C4ConnectedClient* client, } catchError(outError); return; } + +void c4client_start(C4ConnectedClient* client) noexcept { + client->start(); +} + +void c4client_stop(C4ConnectedClient* client) noexcept { + client->stop(); +} + +void c4client_free(C4ConnectedClient* client) noexcept { + if (!client) + return; + release(client); +} + From dd90f288a9f753c5f87e4518c43e654d5b3a1043 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Fri, 18 Mar 2022 01:00:36 -0700 Subject: [PATCH 41/78] update docs use mutex for start & stop error returned in the callback --- C/include/c4ConnectedClient.h | 17 ++++++++++++++--- Replicator/c4ConnectedClientImpl.hh | 4 ++++ Replicator/c4ConnectedClient_CAPI.cc | 8 +++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index f5b65a521..8fc9b0da9 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -36,11 +36,15 @@ typedef struct C4ConnectedClientParameters { @param client The client that initiated the callback. @param doc Resuting document response. + @param err Error will be written here if the get-document fails. @param context user-defined parameter given when registering the callback. */ typedef void (*C4ConnectedClientDocumentResultCallback)(C4ConnectedClient* client, - C4DocResponse doc, - void * C4NULLABLE context); -/** Creates a new connected client + C4DocResponse doc, + C4Error err, + void * C4NULLABLE context); +/** Creates a new connected client and starts it automatically. + \note No need to call the c4client_start(). + @param params Connected Client parameters (see above.) @param error Error will be written here if the function fails. @result A new C4ConnectedClient, or NULL on failure. */ @@ -69,10 +73,17 @@ void c4client_getDoc(C4ConnectedClient*, void * C4NULLABLE context, C4Error* error) C4API; +/** Tells a connected client to start. + \note This function is thread-safe.*/ void c4client_start(C4ConnectedClient*) C4API; +/** Tells a replicator to stop. + \note This function is thread-safe. */ void c4client_stop(C4ConnectedClient*) C4API; +/** Frees a connected client reference. + Does not stop the connected client -- if the client still has other internal references, + it will keep going. If you need the client to stop, call \ref c4client_stop first. */ void c4client_free(C4ConnectedClient*) C4API; C4API_END_DECLS diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index cb4ba6153..426c7dba0 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -14,6 +14,7 @@ #include "ConnectedClient.hh" #include "C4ConnectedClient.hh" #include "c4Socket+Internal.hh" +#include "c4Internal.hh" namespace litecore::client { @@ -77,10 +78,12 @@ namespace litecore::client { } virtual void start() noexcept override { + LOCK(_mutex); _client->start(); } virtual void stop() noexcept override { + LOCK(_mutex); _client->stop(); } @@ -95,6 +98,7 @@ namespace litecore::client { alloc_slice effectiveURL(slice); alloc_slice socketOptions(); + mutable std::mutex _mutex; Retained _client; const C4SocketFactory* C4NULLABLE _socketFactory {nullptr}; C4SocketFactory _customSocketFactory {}; // Storage for *_socketFactory if non-null diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index 721da8232..b5e3c867b 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -36,11 +36,9 @@ void c4client_getDoc(C4ConnectedClient* client, try { auto res = client->getDoc(docID, collectionID, unlessRevID, asFleece); res.then([=](C4DocResponse response) { - return callback(client, response, context); - }).onError([&](C4Error err) { - // TODO: Handle the error here: - - printf("\n Error occured: %s\n", err.description().c_str()); + return callback(client, response, {}, context); + }).onError([=](C4Error err) { + return callback(client, {}, err, context); }); } catchError(outError); return; From 68a9b59a1371a816b383ac05ceef3ec04267807a Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Fri, 18 Mar 2022 01:33:02 -0700 Subject: [PATCH 42/78] remove empty line add start,stop, free functions --- C/Cpp_include/c4ConnectedClient.hh | 7 +++++-- C/c4.def | 3 +++ C/c4.exp | 3 +++ C/c4.gnu | 3 +++ C/c4_ee.def | 3 +++ C/c4_ee.exp | 3 +++ C/c4_ee.gnu | 3 +++ C/scripts/c4.txt | 3 +++ Replicator/c4ConnectedClientImpl.cc | 1 - Replicator/c4ConnectedClientImpl.hh | 1 - 10 files changed, 26 insertions(+), 4 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index 2e32128ae..1b005c9d3 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -21,13 +21,16 @@ struct C4ConnectedClient : public fleece::RefCounted, public fleece::InstanceCountedIn, C4Base { - + /// Creates a new ConnectedClient static Retained newClient(const C4ConnectedClientParameters ¶ms); - + + /// Gets the current revision of a document from the server. virtual litecore::actor::Async getDoc(C4Slice, C4Slice, C4Slice, bool) noexcept=0; + /// Tells a connected client to start. virtual void start() noexcept=0; + /// Tells a replicator to stop. virtual void stop() noexcept=0; }; diff --git a/C/c4.def b/C/c4.def index e20e0cad8..5cf99919d 100644 --- a/C/c4.def +++ b/C/c4.def @@ -418,4 +418,7 @@ FLValue_ToJSON5 c4client_new c4client_getDoc +c4client_start +c4client_stop +c4client_free diff --git a/C/c4.exp b/C/c4.exp index 807d3ae6f..51b5445b8 100644 --- a/C/c4.exp +++ b/C/c4.exp @@ -416,6 +416,9 @@ _FLValue_ToJSON5 _c4client_new _c4client_getDoc +_c4client_start +_c4client_stop +_c4client_free # Apple specific _FLEncoder_WriteNSObject diff --git a/C/c4.gnu b/C/c4.gnu index 0ac9e8793..923a928ad 100644 --- a/C/c4.gnu +++ b/C/c4.gnu @@ -416,6 +416,9 @@ CBL { c4client_new; c4client_getDoc; + c4client_start; + c4client_stop; + c4client_free; local: *; }; \ No newline at end of file diff --git a/C/c4_ee.def b/C/c4_ee.def index d0e00c987..4ce1701ea 100644 --- a/C/c4_ee.def +++ b/C/c4_ee.def @@ -457,6 +457,9 @@ FLValue_ToJSON5 c4client_new c4client_getDoc +c4client_start +c4client_stop +c4client_free c4db_URINameFromPath diff --git a/C/c4_ee.exp b/C/c4_ee.exp index 639ff6502..7c6ed2729 100644 --- a/C/c4_ee.exp +++ b/C/c4_ee.exp @@ -455,6 +455,9 @@ _FLValue_ToJSON5 _c4client_new _c4client_getDoc +_c4client_start +_c4client_stop +_c4client_free _c4db_URINameFromPath diff --git a/C/c4_ee.gnu b/C/c4_ee.gnu index 9d8edb73d..4c56fcbd3 100644 --- a/C/c4_ee.gnu +++ b/C/c4_ee.gnu @@ -455,6 +455,9 @@ CBL { c4client_new; c4client_getDoc; + c4client_start; + c4client_stop; + c4client_free; c4db_URINameFromPath; diff --git a/C/scripts/c4.txt b/C/scripts/c4.txt index f7ab3e9d4..d7880e120 100644 --- a/C/scripts/c4.txt +++ b/C/scripts/c4.txt @@ -424,3 +424,6 @@ FLValue_ToJSON5 c4client_new c4client_getDoc +c4client_start +c4client_stop +c4client_free diff --git a/Replicator/c4ConnectedClientImpl.cc b/Replicator/c4ConnectedClientImpl.cc index b0597f231..0af5c6441 100644 --- a/Replicator/c4ConnectedClientImpl.cc +++ b/Replicator/c4ConnectedClientImpl.cc @@ -10,7 +10,6 @@ // the file licenses/APL2.txt. // - #include "c4ConnectedClientImpl.hh" #include "Replicator.hh" diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 426c7dba0..a5b35d457 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -103,5 +103,4 @@ namespace litecore::client { const C4SocketFactory* C4NULLABLE _socketFactory {nullptr}; C4SocketFactory _customSocketFactory {}; // Storage for *_socketFactory if non-null }; - } From cd7631fe65c1d9c1558425f08fc2c013ba620214 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Tue, 22 Mar 2022 00:28:39 -0700 Subject: [PATCH 43/78] review changes --- C/include/c4ConnectedClient.h | 21 +++++++++++-------- Replicator/ConnectedClient/ConnectedClient.cc | 15 ++++++++++--- Replicator/ConnectedClient/ConnectedClient.hh | 1 + Replicator/c4ConnectedClient.cc | 6 +----- Replicator/c4ConnectedClient_CAPI.cc | 12 +++++------ 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index 8fc9b0da9..e5d7f0397 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -17,10 +17,10 @@ C4_ASSUME_NONNULL_BEGIN C4API_BEGIN_DECLS /** Result of a successful `c4client_getDoc` call. */ -typedef struct { - C4Slice docID; - C4Slice revID; - C4Slice body; +typedef struct C4DocResponse { + C4HeapSlice docID; + C4HeapSlice revID; + C4HeapSlice body; bool deleted; } C4DocResponse; @@ -35,20 +35,23 @@ typedef struct C4ConnectedClientParameters { /** Callback for getting the document result. @param client The client that initiated the callback. - @param doc Resuting document response. + @param doc Resuting document response, NULL on failure. @param err Error will be written here if the get-document fails. @param context user-defined parameter given when registering the callback. */ typedef void (*C4ConnectedClientDocumentResultCallback)(C4ConnectedClient* client, - C4DocResponse doc, - C4Error err, + const C4DocResponse* C4NULLABLE doc, + C4Error* C4NULLABLE err, void * C4NULLABLE context); + +typedef C4ConnectedClientDocumentResultCallback C4ConnectedClientDocumentResultCallback; + /** Creates a new connected client and starts it automatically. \note No need to call the c4client_start(). @param params Connected Client parameters (see above.) @param error Error will be written here if the function fails. @result A new C4ConnectedClient, or NULL on failure. */ -C4ConnectedClient* c4client_new(C4ConnectedClientParameters params, +C4ConnectedClient* c4client_new(const C4ConnectedClientParameters* params, C4Error* error) C4API; /** Gets the current revision of a document from the server. @@ -69,7 +72,7 @@ void c4client_getDoc(C4ConnectedClient*, C4Slice collectionID, C4Slice unlessRevID, bool asFleece, - C4ConnectedClientDocumentResultCallback C4NULLABLE callback, + C4ConnectedClientDocumentResultCallback callback, void * C4NULLABLE context, C4Error* error) C4API; diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 3c510eee6..1cabddcb2 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -24,6 +24,7 @@ #include "MessageBuilder.hh" #include "NumConversion.hh" #include "WebSocketInterface.hh" +#include "c4Internal.hh" namespace litecore::client { using namespace std; @@ -45,9 +46,12 @@ namespace litecore::client { void ConnectedClient::setStatus(ActivityLevel status) { - if (status != _status && _delegate) { + if (status != _status) { _status = status; - _delegate->clientStatusChanged(this, status); + + LOCK(_mutex); + if (_delegate) + _delegate->clientStatusChanged(this, status); } } @@ -78,6 +82,7 @@ namespace litecore::client { } void ConnectedClient::terminate() { + LOCK(_mutex); _delegate = nullptr; } @@ -86,6 +91,7 @@ namespace litecore::client { void ConnectedClient::onTLSCertificate(slice certData) { + LOCK(_mutex); if (_delegate) _delegate->clientGotTLSCertificate(this, certData); } @@ -94,8 +100,10 @@ namespace litecore::client { void ConnectedClient::onHTTPResponse(int status, const websocket::Headers &headers) { asCurrentActor([=] { logVerbose("Got HTTP response from server, status %d", status); + LOCK(_mutex); if (_delegate) _delegate->clientGotHTTPResponse(this, status, headers); + if (status == 101 && !headers["Sec-WebSocket-Protocol"_sl]) { gotError(C4Error::make(WebSocketDomain, kWebSocketCloseProtocolError, "Incompatible replication protocol " @@ -145,7 +153,8 @@ namespace litecore::client { } gotError(C4Error::make(domain, code, status.message)); } - + + LOCK(_mutex); if (_delegate) _delegate->clientConnectionClosed(this, status); diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 061cf3af5..706146a65 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -137,6 +137,7 @@ namespace litecore::client { bool _observing = false; bool _registeredChangesHandler = false; bool _remoteUsesVersionVectors = false; + mutable std::mutex _mutex; }; } diff --git a/Replicator/c4ConnectedClient.cc b/Replicator/c4ConnectedClient.cc index 450aa4ba3..7d9d772c0 100644 --- a/Replicator/c4ConnectedClient.cc +++ b/Replicator/c4ConnectedClient.cc @@ -19,9 +19,5 @@ using namespace litecore::client; using namespace fleece; /*static*/ Retained C4ConnectedClient::newClient(const C4ConnectedClientParameters ¶ms) { - try { - return new C4ConnectedClientImpl(params); - } catch (...) { - throw; - } + return new C4ConnectedClientImpl(params); } diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index b5e3c867b..aa585fd72 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -1,7 +1,7 @@ // -// c4ConnectedClient.cpp +// c4ConnectedClient_CAPI.cpp // -// Copyright 2017-Present Couchbase, Inc. +// Copyright 2022-Present Couchbase, Inc. // // Use of this software is governed by the Business Source License included // in the file licenses/BSL-Couchbase.txt. As of the Change Date specified @@ -18,9 +18,9 @@ using namespace litecore::repl; -C4ConnectedClient* c4client_new(C4ConnectedClientParameters params, C4Error *outError) noexcept { +C4ConnectedClient* c4client_new(const C4ConnectedClientParameters* params, C4Error *outError) noexcept { try { - return C4ConnectedClient::newClient(params).detach(); + return C4ConnectedClient::newClient(*params).detach(); } catchError(outError); return nullptr; } @@ -36,9 +36,9 @@ void c4client_getDoc(C4ConnectedClient* client, try { auto res = client->getDoc(docID, collectionID, unlessRevID, asFleece); res.then([=](C4DocResponse response) { - return callback(client, response, {}, context); + return callback(client, &response, nullptr, context); }).onError([=](C4Error err) { - return callback(client, {}, err, context); + return callback(client, nullptr, &err, context); }); } catchError(outError); return; From 50203fb8dca531c9735c29490b1688c3f8457356 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Wed, 23 Mar 2022 19:26:03 -0700 Subject: [PATCH 44/78] add the API definitions in c4_ee txt file too and generated the export list --- C/c4_ee.def | 6 ++++++ C/c4_ee.exp | 6 ++++++ C/c4_ee.gnu | 6 ++++++ C/scripts/c4_ee.txt | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/C/c4_ee.def b/C/c4_ee.def index 4ce1701ea..242dae129 100644 --- a/C/c4_ee.def +++ b/C/c4_ee.def @@ -475,3 +475,9 @@ c4keypair_privateKeyData c4keypair_publicKeyData c4keypair_publicKeyDigest +c4client_new +c4client_getDoc +c4client_start +c4client_stop +c4client_free + diff --git a/C/c4_ee.exp b/C/c4_ee.exp index 7c6ed2729..0a87506ef 100644 --- a/C/c4_ee.exp +++ b/C/c4_ee.exp @@ -473,5 +473,11 @@ _c4keypair_privateKeyData _c4keypair_publicKeyData _c4keypair_publicKeyDigest +_c4client_new +_c4client_getDoc +_c4client_start +_c4client_stop +_c4client_free + # Apple specific _FLEncoder_WriteNSObject diff --git a/C/c4_ee.gnu b/C/c4_ee.gnu index 4c56fcbd3..83d68a371 100644 --- a/C/c4_ee.gnu +++ b/C/c4_ee.gnu @@ -472,6 +472,12 @@ CBL { c4keypair_privateKeyData; c4keypair_publicKeyData; c4keypair_publicKeyDigest; + + c4client_new; + c4client_getDoc; + c4client_start; + c4client_stop; + c4client_free; local: *; }; \ No newline at end of file diff --git a/C/scripts/c4_ee.txt b/C/scripts/c4_ee.txt index 674186769..20f851f20 100644 --- a/C/scripts/c4_ee.txt +++ b/C/scripts/c4_ee.txt @@ -54,3 +54,9 @@ c4keypair_isPersistent c4keypair_privateKeyData c4keypair_publicKeyData c4keypair_publicKeyDigest + +c4client_new +c4client_getDoc +c4client_start +c4client_stop +c4client_free From 96e0be74a17cb88773ef9b142d0035b06bc5c4a0 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Wed, 23 Mar 2022 22:41:19 -0700 Subject: [PATCH 45/78] include the implementation files to cmake source list --- C/Cpp_include/c4ConnectedClient.hh | 2 +- CMakeLists.txt | 1 + Replicator/c4ConnectedClient.cc | 1 - Replicator/c4ConnectedClientImpl.hh | 4 +++- Replicator/c4ConnectedClient_CAPI.cc | 2 +- cmake/platform_base.cmake | 4 ++++ 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index 1b005c9d3..de7b6390e 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -13,7 +13,7 @@ #pragma once #include "c4Base.hh" #include "Async.hh" -#include "C4ConnectedClient.h" +#include "c4ConnectedClient.h" C4_ASSUME_NONNULL_BEGIN diff --git a/CMakeLists.txt b/CMakeLists.txt index 57da20561..104de3357 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -281,6 +281,7 @@ target_include_directories( Networking/HTTP Networking/WebSockets Replicator + Replicator/ConnectedClient REST vendor/SQLiteCpp/include vendor/sqlite3-unicodesn diff --git a/Replicator/c4ConnectedClient.cc b/Replicator/c4ConnectedClient.cc index 7d9d772c0..b8e0d958c 100644 --- a/Replicator/c4ConnectedClient.cc +++ b/Replicator/c4ConnectedClient.cc @@ -13,7 +13,6 @@ #include "c4Base.hh" #include "c4ConnectedClient.hh" #include "c4ConnectedClientImpl.hh" -#include "ConnectedClient.hh" using namespace litecore::client; using namespace fleece; diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index a5b35d457..2db3a9465 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -10,9 +10,11 @@ // the file licenses/APL2.txt. // +#pragma once + #include "c4Base.h" #include "ConnectedClient.hh" -#include "C4ConnectedClient.hh" +#include "c4ConnectedClient.hh" #include "c4Socket+Internal.hh" #include "c4Internal.hh" diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index aa585fd72..f8059b9f2 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -1,5 +1,5 @@ // -// c4ConnectedClient_CAPI.cpp +// c4ConnectedClient_CAPI.cc // // Copyright 2022-Present Couchbase, Inc. // diff --git a/cmake/platform_base.cmake b/cmake/platform_base.cmake index 55724b5c9..913cbd5fe 100644 --- a/cmake/platform_base.cmake +++ b/cmake/platform_base.cmake @@ -109,6 +109,10 @@ function(set_litecore_source_base) Replicator/RevFinder.cc Replicator/URLTransformer.cc Replicator/Worker.cc + Replicator/c4ConnectedClient_CAPI.cc + Replicator/c4ConnectedClient.cc + Replicator/c4ConnectedClientImpl.cc + Replicator/ConnectedClient/ConnectedClient.cc LiteCore/Support/Logging.cc LiteCore/Support/DefaultLogger.cc LiteCore/Support/Error.cc From cf85c8d8e764abcfe36b927c44e8775f76207149 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Wed, 23 Mar 2022 23:00:51 -0700 Subject: [PATCH 46/78] avoid designated initializer --- Replicator/c4ConnectedClientImpl.hh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 2db3a9465..22299b66c 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -70,12 +70,7 @@ namespace litecore::client { collectionID, unlessRevID, asFleece).then([](DocResponse a) -> C4DocResponse { - return C4DocResponse { - .docID = a.docID, - .body = a.body, - .revID = a.revID, - .deleted = a.deleted, - }; + return { a.docID, a.revID, a.body, a.deleted }; }); } From f4bd0817bdc51ae75dc5c7e75769d526a370a221 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:07:53 -0700 Subject: [PATCH 47/78] keep the lock only for delegate: review changes --- Replicator/ConnectedClient/ConnectedClient.cc | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 1cabddcb2..aa9447388 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -100,9 +100,11 @@ namespace litecore::client { void ConnectedClient::onHTTPResponse(int status, const websocket::Headers &headers) { asCurrentActor([=] { logVerbose("Got HTTP response from server, status %d", status); - LOCK(_mutex); - if (_delegate) - _delegate->clientGotHTTPResponse(this, status, headers); + { + LOCK(_mutex); + if (_delegate) + _delegate->clientGotHTTPResponse(this, status, headers); + } if (status == 101 && !headers["Sec-WebSocket-Protocol"_sl]) { gotError(C4Error::make(WebSocketDomain, kWebSocketCloseProtocolError, @@ -153,10 +155,11 @@ namespace litecore::client { } gotError(C4Error::make(domain, code, status.message)); } - - LOCK(_mutex); - if (_delegate) - _delegate->clientConnectionClosed(this, status); + { + LOCK(_mutex); + if (_delegate) + _delegate->clientConnectionClosed(this, status); + } _selfRetain = nullptr; // balances the self-retain in start() }); From bd24d79153fd17f30ff4f6591bd658341f3ca536 Mon Sep 17 00:00:00 2001 From: jayahariv <10448770+jayahariv@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:15:59 -0700 Subject: [PATCH 48/78] rename the callback to GetDocument --- C/include/c4ConnectedClient.h | 6 +++--- Replicator/c4ConnectedClient_CAPI.cc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index e5d7f0397..421e9e674 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -38,12 +38,12 @@ typedef struct C4ConnectedClientParameters { @param doc Resuting document response, NULL on failure. @param err Error will be written here if the get-document fails. @param context user-defined parameter given when registering the callback. */ -typedef void (*C4ConnectedClientDocumentResultCallback)(C4ConnectedClient* client, +typedef void (*C4ConnectedClientGetDocumentCallback)(C4ConnectedClient* client, const C4DocResponse* C4NULLABLE doc, C4Error* C4NULLABLE err, void * C4NULLABLE context); -typedef C4ConnectedClientDocumentResultCallback C4ConnectedClientDocumentResultCallback; +typedef C4ConnectedClientGetDocumentCallback C4ConnectedClientGetDocumentCallback; /** Creates a new connected client and starts it automatically. \note No need to call the c4client_start(). @@ -72,7 +72,7 @@ void c4client_getDoc(C4ConnectedClient*, C4Slice collectionID, C4Slice unlessRevID, bool asFleece, - C4ConnectedClientDocumentResultCallback callback, + C4ConnectedClientGetDocumentCallback callback, void * C4NULLABLE context, C4Error* error) C4API; diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index f8059b9f2..b86d4870f 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -30,7 +30,7 @@ void c4client_getDoc(C4ConnectedClient* client, C4Slice collectionID, C4Slice unlessRevID, bool asFleece, - C4ConnectedClientDocumentResultCallback callback, + C4ConnectedClientGetDocumentCallback callback, void *context, C4Error* outError) noexcept { try { From f30757fd4957a8512eb826c987a165632653ca19 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 6 Apr 2022 13:49:15 -0700 Subject: [PATCH 49/78] Removed unnecessary #include from Async.hh --- LiteCore/Support/Async.hh | 1 - 1 file changed, 1 deletion(-) diff --git a/LiteCore/Support/Async.hh b/LiteCore/Support/Async.hh index d5a5db2e5..cf60ff680 100644 --- a/LiteCore/Support/Async.hh +++ b/LiteCore/Support/Async.hh @@ -12,7 +12,6 @@ #pragma once #include "AsyncActorCommon.hh" -#include "Error.hh" #include "RefCounted.hh" #include "Result.hh" #include "InstanceCounted.hh" From 8d250bcfabbab6369fde1fc4b34f7b6e874a1d11 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 7 Apr 2022 13:49:23 -0700 Subject: [PATCH 50/78] ConnectedClient: Encryption/decryption/_attachments --- C/Cpp_include/c4Document.hh | 6 - C/c4CAPI.cc | 7 +- C/c4Document.cc | 16 -- C/include/c4ConnectedClient.h | 31 +--- C/include/c4ConnectedClientTypes.h | 50 ++++++ LiteCore/Database/LegacyAttachments.cc | 10 +- LiteCore/Database/LegacyAttachments.hh | 44 +++-- LiteCore/Database/LegacyAttachments2.cc | 134 ++++++++++++++ REST/RESTListener+Handlers.cc | 4 +- Replicator/ConnectedClient/ConnectedClient.cc | 170 +++++++++++++++--- Replicator/ConnectedClient/ConnectedClient.hh | 18 +- Replicator/DBAccess.cc | 92 +--------- Replicator/IncomingRev.cc | 5 +- Replicator/Inserter.cc | 5 +- Replicator/c4ConnectedClientImpl.hh | 2 +- Replicator/tests/ConnectedClientTest.cc | 94 +++++++++- Xcode/LiteCore.xcodeproj/project.pbxproj | 10 +- cmake/platform_base.cmake | 1 + 18 files changed, 499 insertions(+), 200 deletions(-) create mode 100644 C/include/c4ConnectedClientTypes.h create mode 100644 LiteCore/Database/LegacyAttachments2.cc diff --git a/C/Cpp_include/c4Document.hh b/C/Cpp_include/c4Document.hh index 73c1e523e..930cd6bd4 100644 --- a/C/Cpp_include/c4Document.hh +++ b/C/Cpp_include/c4Document.hh @@ -146,12 +146,6 @@ struct C4Document : public fleece::RefCounted, /// Returns the Document instance, if any, that contains the given Fleece value. static C4Document* C4NULLABLE containingValue(FLValue) noexcept; - static bool isOldMetaProperty(slice propertyName) noexcept; - static bool hasOldMetaProperties(FLDict) noexcept; - - static alloc_slice encodeStrippingOldMetaProperties(FLDict properties, - FLSharedKeys); - // Special property names & values: /** The Dict property that identifies it as a special type of object. diff --git a/C/c4CAPI.cc b/C/c4CAPI.cc index ffe0cd172..53b36d212 100644 --- a/C/c4CAPI.cc +++ b/C/c4CAPI.cc @@ -21,6 +21,7 @@ #include "c4Query.hh" #include "c4QueryImpl.hh" #include "c4Replicator.hh" +#include "LegacyAttachments.hh" #include "c4.h" #include "c4Private.h" @@ -1085,12 +1086,12 @@ FLSharedKeys c4db_getFLSharedKeys(C4Database *db) noexcept { bool c4doc_isOldMetaProperty(C4String prop) noexcept { - return C4Document::isOldMetaProperty(prop); + return legacy_attachments::isOldMetaProperty(prop); } bool c4doc_hasOldMetaProperties(FLDict doc) noexcept { - return C4Document::hasOldMetaProperties(doc); + return legacy_attachments::hasOldMetaProperties(doc); } @@ -1135,7 +1136,7 @@ bool c4doc_blobIsCompressible(FLDict blobDict) { C4SliceResult c4doc_encodeStrippingOldMetaProperties(FLDict doc, FLSharedKeys sk, C4Error *outError) noexcept { return tryCatch(outError, [&]{ - return C4SliceResult(C4Document::encodeStrippingOldMetaProperties(doc, sk)); + return C4SliceResult(legacy_attachments::encodeStrippingOldMetaProperties(doc, sk)); }); } diff --git a/C/c4Document.cc b/C/c4Document.cc index 3d0c28572..b121d00a0 100644 --- a/C/c4Document.cc +++ b/C/c4Document.cc @@ -360,19 +360,3 @@ C4RevisionFlags C4Document::revisionFlagsFromDocFlags(C4DocumentFlags docFlags) C4Document* C4Document::containingValue(FLValue value) noexcept { return C4Collection::documentContainingValue(value); } - - -bool C4Document::isOldMetaProperty(slice propertyName) noexcept { - return legacy_attachments::isOldMetaProperty(propertyName); -} - - -bool C4Document::hasOldMetaProperties(FLDict dict) noexcept { - return legacy_attachments::hasOldMetaProperties((const fleece::impl::Dict*)dict); -} - - -alloc_slice C4Document::encodeStrippingOldMetaProperties(FLDict properties, FLSharedKeys sk) { - return legacy_attachments::encodeStrippingOldMetaProperties((const fleece::impl::Dict*)properties, - (fleece::impl::SharedKeys*)sk); -} diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index 421e9e674..f1eef2952 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -11,40 +11,11 @@ // #pragma once -#include "c4Base.h" +#include "c4ConnectedClientTypes.h" C4_ASSUME_NONNULL_BEGIN C4API_BEGIN_DECLS -/** Result of a successful `c4client_getDoc` call. */ -typedef struct C4DocResponse { - C4HeapSlice docID; - C4HeapSlice revID; - C4HeapSlice body; - bool deleted; -} C4DocResponse; - -/** Parameters describing a connected client, used when creating a C4ConnectedClient. */ -typedef struct C4ConnectedClientParameters { - C4Slice url; /// #include +// Note: Some of the functions in LegacyAttachments.hh are implemented in LegacyAttachments2.cc. + namespace litecore { namespace legacy_attachments { using namespace std; using namespace fleece; @@ -29,8 +31,8 @@ namespace litecore { namespace legacy_attachments { // Returns true if a Fleece Dict contains any top-level keys that begin with an underscore. - bool hasOldMetaProperties(const Dict* root) { - for (Dict::iterator i(root); i; ++i) { + bool hasOldMetaProperties(FLDict root) { + for (Dict::iterator i((const Dict*)root); i; ++i) { if (isOldMetaProperty(i.keyString())) return true; } @@ -38,7 +40,9 @@ namespace litecore { namespace legacy_attachments { } - alloc_slice encodeStrippingOldMetaProperties(const Dict *root, SharedKeys *sk) { + alloc_slice encodeStrippingOldMetaProperties(FLDict fl_root, FLSharedKeys fl_sk) { + auto root = (const Dict*)fl_root; + auto sk = (SharedKeys*)fl_sk; if (!root) return {}; diff --git a/LiteCore/Database/LegacyAttachments.hh b/LiteCore/Database/LegacyAttachments.hh index 1b305065f..e1f5ac7ea 100644 --- a/LiteCore/Database/LegacyAttachments.hh +++ b/LiteCore/Database/LegacyAttachments.hh @@ -11,34 +11,42 @@ // #pragma once -#include "c4Compat.h" +#include "c4Base.h" +#include "function_ref.hh" #include "fleece/slice.hh" +#include "fleece/Fleece.h" C4_ASSUME_NONNULL_BEGIN -namespace fleece::impl { - class Dict; - class SharedKeys; -} +/** Utilities for dealing with 'legacy' properties like _id, _rev, _deleted, _attachments. */ +namespace litecore::legacy_attachments { -namespace litecore { + /** Returns true if this is the name of a 1.x metadata property ("_id", "_rev", etc.) */ + bool isOldMetaProperty(fleece::slice key); - /** Utilities for dealing with 'legacy' properties like _id, _rev, _deleted, _attachments. */ - namespace legacy_attachments { + /** Returns true if the document contains 1.x metadata properties (at top level). */ + bool hasOldMetaProperties(FLDict root); - /** Returns true if this is the name of a 1.x metadata property ("_id", "_rev", etc.) */ - bool isOldMetaProperty(fleece::slice key); + /** Encodes to Fleece, without any 1.x metadata properties. + The _attachments property is treated specially, in that any entries in it that don't + appear elsewhere in the dictionary as blobs are preserved. */ + fleece::alloc_slice encodeStrippingOldMetaProperties(FLDict root, + FLSharedKeys C4NULLABLE); - /** Returns true if the document contains 1.x metadata properties (at top level). */ - bool hasOldMetaProperties(const fleece::impl::Dict* root); + using FindBlobCallback = fleece::function_ref; - /** Encodes to Fleece, without any 1.x metadata properties. - The _attachments property is treated specially, in that any entries in it that don't - appear elsewhere in the dictionary as blobs are preserved. */ - fleece::alloc_slice encodeStrippingOldMetaProperties(const fleece::impl::Dict* root, - fleece::impl::SharedKeys* C4NULLABLE); - } + /** Finds all blob references in the dict, at any depth. */ + void findBlobReferences(FLDict root, + bool unique, + const FindBlobCallback &callback, + bool attachmentsOnly =false); + /** Writes `root` to the encoder, transforming blobs into old-school `_attachments` dict */ + void encodeRevWithLegacyAttachments(FLEncoder enc, + FLDict root, + unsigned revpos); } C4_ASSUME_NONNULL_END diff --git a/LiteCore/Database/LegacyAttachments2.cc b/LiteCore/Database/LegacyAttachments2.cc new file mode 100644 index 000000000..401760f15 --- /dev/null +++ b/LiteCore/Database/LegacyAttachments2.cc @@ -0,0 +1,134 @@ +// +// LegacyAttachments2.cc +// +// Copyright © 2022 Couchbase. All rights reserved. +// +// 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. +// + +#include "LegacyAttachments.hh" +#include "c4BlobStore.hh" +#include "c4Document.hh" +#include "fleece/Fleece.hh" +#include + +// This source file is separate from LegacyAttachments.cc because, for historical reasons, those +// functions are implemented with the fleece::impl API while the ones here use the public Fleece +// API, and the two APIs don't mix well in a single source file. + +namespace litecore::legacy_attachments { + using namespace std; + using namespace fleece; + + + static bool isBlobOrAttachment(FLDeepIterator i, C4BlobKey *blobKey, bool attachmentsOnly) { + auto dict = FLValue_AsDict(FLDeepIterator_GetValue(i)); + if (!dict) + return false; + + // Get the digest: + if (auto key = C4Blob::keyFromDigestProperty(dict); key) + *blobKey = *key; + else + return false; + + // Check if it's a blob: + if (!attachmentsOnly && C4Blob::isBlob(dict)) { + return true; + } else { + // Check if it's an old-school attachment, i.e. in a top level "_attachments" dict: + FLPathComponent* path; + size_t depth; + FLDeepIterator_GetPath(i, &path, &depth); + return depth == 2 && path[0].key == C4Blob::kLegacyAttachmentsProperty; + } + } + + + void findBlobReferences(FLDict root, + bool unique, + const FindBlobCallback &callback, + bool attachmentsOnly) + { + unordered_set found; + FLDeepIterator i = FLDeepIterator_New(FLValue(root)); + for (; FLDeepIterator_GetValue(i); FLDeepIterator_Next(i)) { + C4BlobKey blobKey; + if (isBlobOrAttachment(i, &blobKey, attachmentsOnly)) { + if (!unique || found.emplace((const char*)&blobKey, sizeof(blobKey)).second) { + auto blob = Value(FLDeepIterator_GetValue(i)).asDict(); + callback(i, blob, blobKey); + } + FLDeepIterator_SkipChildren(i); + } + } + FLDeepIterator_Free(i); + } + + + void encodeRevWithLegacyAttachments(FLEncoder fl_enc, FLDict root, unsigned revpos) { + SharedEncoder enc(fl_enc); + enc.beginDict(); + + // Write existing properties except for _attachments: + Dict oldAttachments; + for (Dict::iterator i(root); i; ++i) { + slice key = i.keyString(); + if (key == C4Blob::kLegacyAttachmentsProperty) { + oldAttachments = i.value().asDict(); // remember _attachments dict for later + } else { + enc.writeKey(key); + enc.writeValue(i.value()); + } + } + + // Now write _attachments: + enc.writeKey(C4Blob::kLegacyAttachmentsProperty); + enc.beginDict(); + // First pre-existing legacy attachments, if any: + for (Dict::iterator i(oldAttachments); i; ++i) { + slice key = i.keyString(); + if (!key.hasPrefix("blob_"_sl)) { + // TODO: Should skip this entry if a blob with the same digest exists + enc.writeKey(key); + enc.writeValue(i.value()); + } + } + + // Then entries for blobs found in the document: + findBlobReferences(root, false, [&](FLDeepIterator di, FLDict blob, C4BlobKey blobKey) { + alloc_slice path(FLDeepIterator_GetJSONPointer(di)); + if (path.hasPrefix("/_attachments/"_sl)) + return; + string attName = string("blob_") + string(path); + enc.writeKey(slice(attName)); + enc.beginDict(); + for (Dict::iterator i(blob); i; ++i) { + slice key = i.keyString(); + if (key != C4Document::kObjectTypeProperty && key != "stub"_sl) { + enc.writeKey(key); + enc.writeValue(i.value()); + } + } + enc.writeKey("stub"_sl); + enc.writeBool(true); + enc.writeKey("revpos"_sl); + enc.writeInt(revpos); + enc.endDict(); + }); + enc.endDict(); + + enc.endDict(); + } + +} diff --git a/REST/RESTListener+Handlers.cc b/REST/RESTListener+Handlers.cc index 17a921960..8a105d235 100644 --- a/REST/RESTListener+Handlers.cc +++ b/REST/RESTListener+Handlers.cc @@ -20,6 +20,7 @@ #include "Server.hh" #include "StringUtil.hh" #include "c4ExceptionUtils.hh" +#include "LegacyAttachments.hh" #include using namespace std; @@ -279,7 +280,8 @@ namespace litecore { namespace REST { // Encode body as Fleece (and strip _id and _rev): alloc_slice encodedBody; if (body) - encodedBody = doc->encodeStrippingOldMetaProperties(body, db->getFleeceSharedKeys()); + encodedBody = legacy_attachments::encodeStrippingOldMetaProperties(body, + db->getFleeceSharedKeys()); // Save the revision: C4Slice history[1] = {revID}; diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index aa9447388..ed3eb3199 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -17,14 +17,20 @@ // #include "ConnectedClient.hh" -#include "c4BlobStoreTypes.h" +#include "c4BlobStore.hh" #include "c4Document.hh" #include "c4SocketTypes.h" #include "Headers.hh" +#include "LegacyAttachments.hh" #include "MessageBuilder.hh" #include "NumConversion.hh" +#include "PropertyEncryption.hh" #include "WebSocketInterface.hh" #include "c4Internal.hh" +#include "fleece/Mutable.hh" + + +#define _options DONT_USE_OPTIONS // inherited from Worker, but replicator-specific, not used here namespace litecore::client { using namespace std; @@ -33,12 +39,21 @@ namespace litecore::client { using namespace blip; + alloc_slice ConnectedClient::Delegate::getBlobContents(slice hexDigest, C4Error *error) { + Warn("ConnectedClient's delegate needs to override getBlobContents!"); + *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); + return nullslice; + } + + + ConnectedClient::ConnectedClient(websocket::WebSocket* webSocket, Delegate& delegate, - fleece::AllocedDict options) - :Worker(new Connection(webSocket, options, *this), + const C4ConnectedClientParameters ¶ms) + :Worker(new Connection(webSocket, AllocedDict(params.optionsDictFleece), *this), nullptr, nullptr, nullptr, "Client") ,_delegate(&delegate) + ,_params(params) ,_status(kC4Stopped) { _importance = 2; @@ -62,6 +77,7 @@ namespace litecore::client { Assert(_status == kC4Stopped); setStatus(kC4Connecting); connection().start(); + registerHandler("getAttachment", &ConnectedClient::handleGetAttachment); _selfRetain = this; // retain myself while the connection is open }); } @@ -198,9 +214,9 @@ namespace litecore::client { Async ConnectedClient::getDoc(slice docID_, - slice collectionID_, - slice unlessRevID_, - bool asFleece) + slice collectionID_, + slice unlessRevID_, + bool asFleece) { // Not yet running on Actor thread... logInfo("getDoc(\"%.*s\")", FMTSLICE(docID_)); @@ -216,24 +232,69 @@ namespace litecore::client { if (C4Error err = responseError(response)) return err; - DocResponse docResponse { + return DocResponse { docID, alloc_slice(response->property("rev")), - response->body(), + processIncomingDoc(docID, response->body(), asFleece), response->boolProperty("deleted") }; - - if (asFleece && docResponse.body) { - FLError flErr; - docResponse.body = FLData_ConvertJSON(docResponse.body, &flErr); - if (!docResponse.body) - C4Error::raise(FleeceDomain, flErr, "Unparseable JSON response from server"); - } - return docResponse; }); } + // (This method's code is adapted from IncomingRev::parseAndInsert) + alloc_slice ConnectedClient::processIncomingDoc(slice docID, + alloc_slice jsonData, + bool asFleece) + { + if (!jsonData) + return jsonData; + + bool modified = false; + bool tryDecrypt = _params.propertyDecryptor && repl::MayContainPropertiesToDecrypt(jsonData); + + // Convert JSON to Fleece: + FLError flErr; + Doc fleeceDoc = Doc::fromJSON(jsonData, &flErr); + if (!fleeceDoc) + C4Error::raise(FleeceDomain, flErr, "Unparseable JSON response from server"); + alloc_slice fleeceData = fleeceDoc.allocedData(); + Dict root = fleeceDoc.asDict(); + + // Decrypt properties: + MutableDict decryptedRoot; + if (tryDecrypt) { + C4Error error; + decryptedRoot = repl::DecryptDocumentProperties(docID, + root, + _params.propertyDecryptor, + _params.callbackContext, + &error); + if (decryptedRoot) { + root = decryptedRoot; + modified = true; + } else if (error) { + error.raise(); + } + } + + // Strip out any "_"-prefixed properties like _id, just in case, and also any + // attachments in _attachments that are redundant with blobs elsewhere in the doc. + // This also re-encodes, updating fleeceData, if `root` was modified by the decryptor. + if (modified || legacy_attachments::hasOldMetaProperties(root)) { + fleeceData = legacy_attachments::encodeStrippingOldMetaProperties(root, nullptr); + if (!fleeceData) + C4Error::raise(LiteCoreDomain, kC4ErrorRemoteError, + "Invalid legacy attachments received from server"); + //modified = true; + if (!asFleece) + jsonData = Doc(fleeceData, kFLTrusted).root().toJSON(); + } + + return asFleece ? fleeceData : jsonData; + } + + Async ConnectedClient::getBlob(C4BlobKey blobKey, bool compress) { @@ -256,11 +317,11 @@ namespace litecore::client { Async ConnectedClient::putDoc(slice docID_, - slice collectionID_, - slice revID_, - slice parentRevID_, - C4RevisionFlags revisionFlags, - slice fleeceData_) + slice collectionID_, + slice revID_, + slice parentRevID_, + C4RevisionFlags revisionFlags, + slice fleeceData_) { // Not yet running on Actor thread... logInfo("putDoc(\"%.*s\", \"%.*s\")", FMTSLICE(docID_), FMTSLICE(revID_)); @@ -273,9 +334,7 @@ namespace litecore::client { req["deleted"] = "1"; if (fleeceData_.size > 0) { - // TODO: Encryption!! - // TODO: Convert blobs to legacy attachments - req.jsonBody().writeValue(FLValue_FromData(fleeceData_, kFLTrusted)); + processOutgoingDoc(docID_, revID_, fleeceData_, req.jsonBody()); } else { req.write("{}"); } @@ -288,6 +347,69 @@ namespace litecore::client { } + static inline bool MayContainBlobs(fleece::slice documentData) noexcept { + return documentData.find(C4Document::kObjectTypeProperty) + && documentData.find(C4Blob::kObjectType_Blob); + } + + + void ConnectedClient::processOutgoingDoc(slice docID, slice revID, + slice fleeceData, + JSONEncoder &enc) + { + Dict root = Value(FLValue_FromData(fleeceData, kFLUntrusted)).asDict(); + if (!root) + C4Error::raise(LiteCoreDomain, kC4ErrorCorruptRevisionData, + "Invalid Fleece data passed to ConnectedClient::putDoc"); + + // Encrypt any encryptable properties + MutableDict encryptedRoot; + if (repl::MayContainPropertiesToEncrypt(fleeceData)) { + logVerbose("Encrypting properties in doc '%.*s'", FMTSLICE(docID)); + C4Error c4err; + encryptedRoot = repl::EncryptDocumentProperties(docID, root, + _params.propertyEncryptor, + _params.callbackContext, + &c4err); + if (encryptedRoot) + root = encryptedRoot; + else if (c4err) + c4err.raise(); + } + + if (_remoteNeedsLegacyAttachments && MayContainBlobs(fleeceData)) { + // Create shadow copies of blobs, in `_attachments`: + int revpos = C4Document::getRevIDGeneration(revID); + legacy_attachments::encodeRevWithLegacyAttachments(enc, root, revpos); + } else { + enc.writeValue(root); + } + } + + + void ConnectedClient::handleGetAttachment(Retained req) { + // Pass the buck to the delegate: + alloc_slice contents; + C4Error error = {}; + try { + contents = _delegate->getBlobContents(req->property("digest"_sl), &error); + } catch (...) { + error = C4Error::fromCurrentException(); + } + if (!contents) { + if (!error) + error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); + req->respondWithError(c4ToBLIPError(error)); + return; + } + + MessageBuilder reply(req); + reply.compressed = req->boolProperty("compress"_sl); + reply.write(contents); + req->respond(reply); + } + + Async ConnectedClient::observeCollection(slice collectionID_, CollectionObserver callback_) { diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 706146a65..426981f15 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -8,6 +8,7 @@ #include "Worker.hh" #include "Async.hh" #include "BLIPConnection.hh" +#include "c4ConnectedClientTypes.h" #include "c4Observer.hh" #include "c4ReplicatorTypes.h" #include @@ -40,7 +41,7 @@ namespace litecore::client { ConnectedClient(websocket::WebSocket* NONNULL, Delegate&, - fleece::AllocedDict options); + const C4ConnectedClientParameters&); /** ConnectedClient Delegate API. Almost identical to `Replicator::Delegate` */ class Delegate { @@ -54,6 +55,14 @@ namespace litecore::client { ActivityLevel) =0; virtual void clientConnectionClosed(ConnectedClient* NONNULL, const CloseStatus&) { } + + /** You must override this if you upload documents containing blobs. + The default implementation always returns a Not Found error. + @param hexDigest The value of the blob's `digest` property, a hex SHA-1 digest. + @param error If you can't return the contents, store an error here. + @return The blob's contents, or `nullslice` if an error occurred. */ + virtual alloc_slice getBlobContents(slice hexDigest, C4Error *error); + virtual ~Delegate() =default; }; @@ -123,21 +132,26 @@ namespace litecore::client { void onRequestReceived(blip::MessageIn* request) override; void handleChanges(Retained); + void handleGetAttachment(Retained); private: void setStatus(ActivityLevel); C4Error responseError(blip::MessageIn *response); void _disconnect(websocket::CloseCode closeCode, slice message); bool validateDocAndRevID(slice docID, slice revID); + alloc_slice processIncomingDoc(slice docID, alloc_slice body, bool asFleece); + void processOutgoingDoc(slice docID, slice revID, slice fleeceData, fleece::JSONEncoder &enc); Delegate* _delegate; // Delegate whom I report progress/errors to + C4ConnectedClientParameters _params; ActivityLevel _status; Retained _selfRetain; CollectionObserver _observer; + mutable std::mutex _mutex; bool _observing = false; bool _registeredChangesHandler = false; bool _remoteUsesVersionVectors = false; - mutable std::mutex _mutex; + bool _remoteNeedsLegacyAttachments = true; }; } diff --git a/Replicator/DBAccess.cc b/Replicator/DBAccess.cc index b7e7c9dc5..5497ad799 100644 --- a/Replicator/DBAccess.cc +++ b/Replicator/DBAccess.cc @@ -20,6 +20,7 @@ #include "c4Document.hh" #include "c4DocEnumerator.hh" #include "c4Private.h" +#include "LegacyAttachments.hh" #include #include #include @@ -164,103 +165,18 @@ namespace litecore { namespace repl { } - static inline bool isBlobOrAttachment(FLDeepIterator i, C4BlobKey *blobKey, bool noBlobs) { - auto dict = FLValue_AsDict(FLDeepIterator_GetValue(i)); - if (!dict) - return false; - - // Get the digest: - if (auto key = C4Blob::keyFromDigestProperty(dict); key) - *blobKey = *key; - else - return false; - - // Check if it's a blob: - if (!noBlobs && C4Blob::isBlob(dict)) { - return true; - } else { - // Check if it's an old-school attachment, i.e. in a top level "_attachments" dict: - FLPathComponent* path; - size_t depth; - FLDeepIterator_GetPath(i, &path, &depth); - return depth == 2 && path[0].key == C4Blob::kLegacyAttachmentsProperty; - } - } - - void DBAccess::findBlobReferences(Dict root, bool unique, const FindBlobCallback &callback) { // This method is non-static because it references _disableBlobSupport, but it's // thread-safe. - set found; - FLDeepIterator i = FLDeepIterator_New(root); - for (; FLDeepIterator_GetValue(i); FLDeepIterator_Next(i)) { - C4BlobKey blobKey; - if (isBlobOrAttachment(i, &blobKey, _disableBlobSupport)) { - if (!unique || found.emplace((const char*)&blobKey, sizeof(blobKey)).second) { - auto blob = Value(FLDeepIterator_GetValue(i)).asDict(); - callback(i, blob, blobKey); - } - FLDeepIterator_SkipChildren(i); - } - } - FLDeepIterator_Free(i); + return legacy_attachments::findBlobReferences(root, unique, callback, _disableBlobSupport); } void DBAccess::encodeRevWithLegacyAttachments(fleece::Encoder& enc, Dict root, unsigned revpos) { - enc.beginDict(); - - // Write existing properties except for _attachments: - Dict oldAttachments; - for (Dict::iterator i(root); i; ++i) { - slice key = i.keyString(); - if (key == C4Blob::kLegacyAttachmentsProperty) { - oldAttachments = i.value().asDict(); // remember _attachments dict for later - } else { - enc.writeKey(key); - enc.writeValue(i.value()); - } - } - - // Now write _attachments: - enc.writeKey(C4Blob::kLegacyAttachmentsProperty); - enc.beginDict(); - // First pre-existing legacy attachments, if any: - for (Dict::iterator i(oldAttachments); i; ++i) { - slice key = i.keyString(); - if (!key.hasPrefix("blob_"_sl)) { - // TODO: Should skip this entry if a blob with the same digest exists - enc.writeKey(key); - enc.writeValue(i.value()); - } - } - - // Then entries for blobs found in the document: - findBlobReferences(root, false, [&](FLDeepIterator di, FLDict blob, C4BlobKey blobKey) { - alloc_slice path(FLDeepIterator_GetJSONPointer(di)); - if (path.hasPrefix("/_attachments/"_sl)) - return; - string attName = string("blob_") + string(path); - enc.writeKey(slice(attName)); - enc.beginDict(); - for (Dict::iterator i(blob); i; ++i) { - slice key = i.keyString(); - if (key != C4Document::kObjectTypeProperty && key != "stub"_sl) { - enc.writeKey(key); - enc.writeValue(i.value()); - } - } - enc.writeKey("stub"_sl); - enc.writeBool(true); - enc.writeKey("revpos"_sl); - enc.writeInt(revpos); - enc.endDict(); - }); - enc.endDict(); - - enc.endDict(); + Assert(!_disableBlobSupport); + legacy_attachments::encodeRevWithLegacyAttachments(enc, root, revpos); } diff --git a/Replicator/IncomingRev.cc b/Replicator/IncomingRev.cc index 31ea83be4..093ef9e8c 100644 --- a/Replicator/IncomingRev.cc +++ b/Replicator/IncomingRev.cc @@ -18,6 +18,7 @@ #include "StringUtil.hh" #include "c4BlobStore.hh" #include "c4Document.hh" +#include "LegacyAttachments.hh" #include "Instrumentation.hh" #include "BLIP.hh" #include "fleece/Mutable.hh" @@ -233,10 +234,10 @@ namespace litecore { namespace repl { // Strip out any "_"-prefixed properties like _id, just in case, and also any attachments // in _attachments that are redundant with blobs elsewhere in the doc. // This also re-encodes the document if it was modified by the decryptor. - if ((C4Document::hasOldMetaProperties(root) && !_db->disableBlobSupport()) + if ((legacy_attachments::hasOldMetaProperties(root) && !_db->disableBlobSupport()) || decryptedRoot) { auto sk = fleeceDoc.sharedKeys(); - alloc_slice body = C4Document::encodeStrippingOldMetaProperties(root, sk); + alloc_slice body = legacy_attachments::encodeStrippingOldMetaProperties(root, sk); if (!body) { failWithError(WebSocketDomain, 500, "invalid legacy attachments"_sl); return; diff --git a/Replicator/Inserter.cc b/Replicator/Inserter.cc index 652876332..45b260580 100644 --- a/Replicator/Inserter.cc +++ b/Replicator/Inserter.cc @@ -22,6 +22,7 @@ #include "c4Private.h" #include "c4Document.hh" #include "c4ReplicatorTypes.h" +#include "LegacyAttachments.hh" #include "BLIP.hh" using namespace std; @@ -196,11 +197,11 @@ namespace litecore { namespace repl { // After applying the delta, remove legacy attachment properties and any other // "_"-prefixed top level properties: Dict root = doc.root().asDict(); - if (C4Document::hasOldMetaProperties(root)) { + if (legacy_attachments::hasOldMetaProperties(root)) { body = nullslice; try { FLSharedKeys sk = _db->insertionDB().useLocked()->getFleeceSharedKeys(); - body = C4Document::encodeStrippingOldMetaProperties(root, sk); + body = legacy_attachments::encodeStrippingOldMetaProperties(root, sk); } catchAndWarn(); if (!body) *outError = C4Error::make(WebSocketDomain, 500, "invalid legacy attachments"); diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 22299b66c..bf0ec7b47 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -37,7 +37,7 @@ namespace litecore::client { Role::Client, socketOptions(), _socketFactory); - _client = new ConnectedClient(webSocket, *this, fleece::AllocedDict(params.options)); + _client = new ConnectedClient(webSocket, *this, params); _client->start(); } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index f061ca24d..892fe2874 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -35,6 +35,10 @@ class ConnectedClientLoopbackTest : public C4Test, { public: + virtual C4ConnectedClientParameters params() { + return {}; + } + void start() { std::unique_lock lock(_mutex); Assert(!_serverRunning && !_clientRunning); @@ -49,11 +53,11 @@ class ConnectedClientLoopbackTest : public C4Test, new LoopbackWebSocket(alloc_slice("ws://srv/"), Role::Server, {}), *this, serverOpts); - AllocedDict clientOpts; + _client = new client::ConnectedClient(new LoopbackWebSocket(alloc_slice("ws://cli/"), Role::Client, {}), *this, - clientOpts); + params()); Headers headers; headers.add("Set-Cookie"_sl, "flavor=chocolate-chip"_sl); @@ -234,6 +238,14 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getBlob", "[ConnectedClient]") { } start(); + auto asyncResult1 = _client->getDoc("att1", nullslice, nullslice); + auto rev = waitForResponse(asyncResult1); + CHECK(rev.docID == "att1"); + auto doc = Doc(rev.body); + auto digest = C4Blob::keyFromDigestProperty(doc.asDict()["attached"].asArray()[0].asDict()); + REQUIRE(digest); + CHECK(*digest == blobKeys[0]); + auto asyncBlob1 = _client->getBlob(blobKeys[0], true); auto asyncBlob2 = _client->getBlob(blobKeys[1], false); auto asyncBadBlob = _client->getBlob(C4BlobKey{}, false); @@ -341,3 +353,81 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "observeCollection", "[ConnectedCl CHECK(change.sequence == expectedSeq++); } } + + +#pragma mark - ENCRYPTION: + +#if 0 //TEMP +#ifdef COUCHBASE_ENTERPRISE + +class ConnectedClientEncryptedLoopbackTest : public ConnectedClientLoopbackTest { + + C4ConnectedClientParameters params() override{ + auto p = ConnectedClientLoopbackTest::params(); + p.propertyEncryptor = &encryptor; + p.propertyDecryptor = &decryptor; + p.callbackContext = &_encryptorContext; + } + + static alloc_slice unbreakableEncryption(slice cleartext, int8_t delta) { + alloc_slice ciphertext(cleartext); + for (size_t i = 0; i < ciphertext.size; ++i) + (uint8_t&)ciphertext[i] += delta; // "I've got patent pending on that!" --Wallace + return ciphertext; + } + + struct TestEncryptorContext { + slice docID; + slice keyPath; + bool called; + } _encryptorContext; + + static C4SliceResult encryptor(void* rawCtx, + C4String documentID, + FLDict properties, + C4String keyPath, + C4Slice input, + C4StringResult* outAlgorithm, + C4StringResult* outKeyID, + C4Error* outError) + { + auto context = (TestEncryptorContext*)rawCtx; + context->called = true; + CHECK(documentID == context->docID); + CHECK(keyPath == context->keyPath); + return C4SliceResult(unbreakableEncryption(input, 1)); + } + + static C4SliceResult decryptor(void* rawCtx, + C4String documentID, + FLDict properties, + C4String keyPath, + C4Slice input, + C4String algorithm, + C4String keyID, + C4Error* outError) + { + auto context = (TestEncryptorContext*)rawCtx; + context->called = true; + CHECK(documentID == context->docID); + CHECK(keyPath == context->keyPath); + return C4SliceResult(unbreakableEncryption(input, -1)); + } + +}; + + +TEST_CASE_METHOD(ConnectedClientEncryptedLoopbackTest, "getRev encrypted", "[ConnectedClient]") { + createFleeceRev(db, "seekrit"_sl, "1-1111"_sl, + R"({"encrypted$SSN":{"alg":"CB_MOBILE_CUSTOM","ciphertext":"IzIzNC41Ni43ODk6Iw=="}})"_sl); + start(); + + Log("++++ Calling ConnectedClient::getDoc()..."); + auto asyncResult1 = _client->getDoc("seekrit", nullslice, nullslice); + auto rev = waitForResponse(asyncResult1); + Doc doc(rev.body); + CHECK(doc.asDict()["SSN"].toJSONString() == R"({"SSN":{"@type":"encryptable","value":"123-45-6789"}})"); +} + +#endif +#endif //TEMP diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 0393901db..a64329100 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -330,6 +330,7 @@ 27BF024B1FB62726003D5BB8 /* LibC++Debug.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27BF023C1FB61F5F003D5BB8 /* LibC++Debug.cc */; }; 27C319EE1A143F5D00A89EDC /* KeyStore.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27C319EC1A143F5D00A89EDC /* KeyStore.cc */; }; 27C77302216FCF5400D5FB44 /* c4PredictiveQueryTest+CoreML.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27C77301216FCF5400D5FB44 /* c4PredictiveQueryTest+CoreML.mm */; }; + 27CA3D6D27FE510C006918A7 /* LegacyAttachments2.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27CA3D6C27FE510C006918A7 /* LegacyAttachments2.cc */; }; 27CCD4AE2315DB03003DEB99 /* CookieStore.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2761F3EE1EE9CC58006D4BB8 /* CookieStore.cc */; }; 27CCD4AF2315DB11003DEB99 /* Address.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27CE4CF02077F51000ACA225 /* Address.cc */; }; 27CCD4B22315DBD3003DEB99 /* Address.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27CE4CF02077F51000ACA225 /* Address.cc */; }; @@ -1363,6 +1364,8 @@ 27C319EC1A143F5D00A89EDC /* KeyStore.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = KeyStore.cc; sourceTree = ""; }; 27C319ED1A143F5D00A89EDC /* KeyStore.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = KeyStore.hh; sourceTree = ""; }; 27C77301216FCF5400D5FB44 /* c4PredictiveQueryTest+CoreML.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "c4PredictiveQueryTest+CoreML.mm"; sourceTree = ""; }; + 27CA3D6827FE3D12006918A7 /* c4ConnectedClientTypes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = c4ConnectedClientTypes.h; sourceTree = ""; }; + 27CA3D6C27FE510C006918A7 /* LegacyAttachments2.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = LegacyAttachments2.cc; sourceTree = ""; }; 27CCC7D61E52613C00CE1989 /* Replicator.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Replicator.cc; sourceTree = ""; }; 27CCC7D71E52613C00CE1989 /* Replicator.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Replicator.hh; sourceTree = ""; }; 27CCC7DE1E526CCC00CE1989 /* Puller.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Puller.cc; sourceTree = ""; }; @@ -2417,6 +2420,7 @@ 273D25F52564666A008643D2 /* VectorDocument.cc */, 273D25F42564666A008643D2 /* VectorDocument.hh */, 2776AA252087FF6B004ACE85 /* LegacyAttachments.cc */, + 27CA3D6C27FE510C006918A7 /* LegacyAttachments2.cc */, 2776AA262087FF6B004ACE85 /* LegacyAttachments.hh */, 27DD1511193CD005009A367D /* RevID.cc */, 27DD1512193CD005009A367D /* RevID.hh */, @@ -2453,13 +2457,13 @@ 274D17C02615445B0018D39C /* DBAccessTestWrapper.hh */, 274D17C12615445B0018D39C /* DBAccessTestWrapper.cc */, 275CE1051E5B79A80084E014 /* ReplicatorLoopbackTest.cc */, - 2752C7A627BF18F2001C1B76 /* ConnectedClientTest.cc */, 273613F71F1696E700ECB9DF /* ReplicatorLoopbackTest.hh */, 2745DE4B1E735B9000F02CA0 /* ReplicatorAPITest.cc */, 273613FB1F16976300ECB9DF /* ReplicatorAPITest.hh */, 277FEE5721ED10FA00B60E3C /* ReplicatorSGTest.cc */, 2761F3F61EEA00C3006D4BB8 /* CookieStoreTest.cc */, 27A83D53269E3E69002B7EBA /* PropertyEncryptionTests.cc */, + 2752C7A627BF18F2001C1B76 /* ConnectedClientTest.cc */, ); path = tests; sourceTree = ""; @@ -3116,6 +3120,7 @@ 2722504A1D7884110006D5A5 /* c4BlobStore.h */, 27469D03233D488C00A1EE1A /* c4Certificate.h */, 274D166C2612906B0018D39C /* c4Collection.h */, + 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */, 2757DE571B9FC3C9002EE261 /* c4Database.h */, 274A69881BED288D00D16D37 /* c4Document.h */, 27F370821DC02C3D0096F717 /* c4Document+Fleece.h */, @@ -3131,6 +3136,7 @@ 27491C9A1E7B1001001DC54B /* c4Socket.h */, 27175AF1261B80E70045F3AC /* c4BlobStoreTypes.h */, 274D181C26165BDA0018D39C /* c4CertificateTypes.h */, + 27CA3D6827FE3D12006918A7 /* c4ConnectedClientTypes.h */, 2743E32625F853D4006F696D /* c4DatabaseTypes.h */, 2743E1DA25F69D58006F696D /* c4DocumentTypes.h */, 274D17A22614E7220018D39C /* c4DocumentStruct.h */, @@ -3140,7 +3146,6 @@ 2743E33525F8554F006F696D /* c4QueryTypes.h */, 2743E34625F8583D006F696D /* c4ReplicatorTypes.h */, 274D18852617B3660018D39C /* c4SocketTypes.h */, - 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */, 27B64936206971FC00FC12F7 /* LiteCore.h */, ); path = include; @@ -4320,6 +4325,7 @@ 2763011B1F32A7FD004A1592 /* UnicodeCollator_Stub.cc in Sources */, 93CD010D1E933BE100AFB3FA /* Replicator.cc in Sources */, 273855AF25B790B1009D746E /* DatabaseImpl+Upgrade.cc in Sources */, + 27CA3D6D27FE510C006918A7 /* LegacyAttachments2.cc in Sources */, 2716F91F248578D000BE21D9 /* mbedSnippets.cc in Sources */, 27229215260AB89900A3A41F /* BlobStreams.cc in Sources */, 272F00F62273D45000E62F72 /* LiveQuerier.cc in Sources */, diff --git a/cmake/platform_base.cmake b/cmake/platform_base.cmake index 913cbd5fe..dea6d8dae 100644 --- a/cmake/platform_base.cmake +++ b/cmake/platform_base.cmake @@ -40,6 +40,7 @@ function(set_litecore_source_base) LiteCore/Database/DatabaseImpl+Upgrade.cc LiteCore/Database/Housekeeper.cc LiteCore/Database/LegacyAttachments.cc + LiteCore/Database/LegacyAttachments2.cc LiteCore/Database/LiveQuerier.cc LiteCore/Database/PrebuiltCopier.cc LiteCore/Database/SequenceTracker.cc From 54430097d197cf27f062c2b1b73eba1816e76295 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 7 Apr 2022 14:35:20 -0700 Subject: [PATCH 51/78] ConnectedClient: Got encryption/decryption tests working --- Replicator/tests/ConnectedClientTest.cc | 82 ++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 892fe2874..2bf530738 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -357,16 +357,56 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "observeCollection", "[ConnectedCl #pragma mark - ENCRYPTION: -#if 0 //TEMP + +C4UNUSED static constexpr slice + kEncryptedDocJSON = R"({"encrypted$SSN":{"alg":"CB_MOBILE_CUSTOM","ciphertext":"IzIzNC41Ni43ODk6Iw=="}})", + kDecryptedDocJSON = R"({"SSN":{"@type":"encryptable","value":"123-45-6789"}})"; + + +// Make sure there's no error if no decryption callback is given +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev encrypted no callback", "[ConnectedClient]") { + createFleeceRev(db, "seekrit"_sl, "1-1111"_sl, kEncryptedDocJSON); + start(); + + Log("++++ Calling ConnectedClient::getDoc()..."); + auto asyncResult1 = _client->getDoc("seekrit", nullslice, nullslice); + auto rev = waitForResponse(asyncResult1); + Doc doc(rev.body); + CHECK(doc.root().toJSONString() == string(kEncryptedDocJSON)); +} + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc encrypted no callback", "[ConnectedClient]") { + start(); + + Doc doc = Doc::fromJSON(kDecryptedDocJSON); + + Log("++++ Calling ConnectedClient::putDoc()..."); + C4Error error = {}; + try { + ExpectingExceptions x; + auto rq1 = _client->putDoc("seekrit", nullslice, + "1-1111", + nullslice, + C4RevisionFlags{}, + doc.data()); + } catch(const exception &x) { + error = C4Error::fromCurrentException(); + } + CHECK(error == C4Error{LiteCoreDomain, kC4ErrorCrypto}); +} + + #ifdef COUCHBASE_ENTERPRISE class ConnectedClientEncryptedLoopbackTest : public ConnectedClientLoopbackTest { - +public: C4ConnectedClientParameters params() override{ auto p = ConnectedClientLoopbackTest::params(); p.propertyEncryptor = &encryptor; p.propertyDecryptor = &decryptor; p.callbackContext = &_encryptorContext; + return p; } static alloc_slice unbreakableEncryption(slice cleartext, int8_t delta) { @@ -379,7 +419,7 @@ class ConnectedClientEncryptedLoopbackTest : public ConnectedClientLoopbackTest struct TestEncryptorContext { slice docID; slice keyPath; - bool called; + bool called = false; } _encryptorContext; static C4SliceResult encryptor(void* rawCtx, @@ -418,16 +458,44 @@ class ConnectedClientEncryptedLoopbackTest : public ConnectedClientLoopbackTest TEST_CASE_METHOD(ConnectedClientEncryptedLoopbackTest, "getRev encrypted", "[ConnectedClient]") { - createFleeceRev(db, "seekrit"_sl, "1-1111"_sl, - R"({"encrypted$SSN":{"alg":"CB_MOBILE_CUSTOM","ciphertext":"IzIzNC41Ni43ODk6Iw=="}})"_sl); + createFleeceRev(db, "seekrit"_sl, "1-1111"_sl, kEncryptedDocJSON); start(); + _encryptorContext.docID = "seekrit"; + _encryptorContext.keyPath = "SSN"; + Log("++++ Calling ConnectedClient::getDoc()..."); auto asyncResult1 = _client->getDoc("seekrit", nullslice, nullslice); auto rev = waitForResponse(asyncResult1); + CHECK(_encryptorContext.called); Doc doc(rev.body); - CHECK(doc.asDict()["SSN"].toJSONString() == R"({"SSN":{"@type":"encryptable","value":"123-45-6789"}})"); + CHECK(doc.root().toJSON() == kDecryptedDocJSON); +} + + +TEST_CASE_METHOD(ConnectedClientEncryptedLoopbackTest, "putDoc encrypted", "[ConnectedClient]") { + start(); + + Doc doc = Doc::fromJSON(kDecryptedDocJSON); + + Log("++++ Calling ConnectedClient::getDoc()..."); + _encryptorContext.docID = "seekrit"; + _encryptorContext.keyPath = "SSN"; + auto rq1 = _client->putDoc("seekrit", nullslice, + "1-1111", + nullslice, + C4RevisionFlags{}, + doc.data()); + rq1.blockUntilReady(); + CHECK(_encryptorContext.called); + + // Read the doc from the database to make sure it was encrypted. + // Note that the replicator has no decryption callback so it will not decrypt the doc! + c4::ref savedDoc = c4db_getDoc(db, "seekrit"_sl, true, + kDocGetAll, ERROR_INFO()); + REQUIRE(savedDoc); + alloc_slice json = c4doc_bodyAsJSON(savedDoc, true, ERROR_INFO()); + CHECK(json == kEncryptedDocJSON); } #endif -#endif //TEMP From bb4cdf5d1f2578d754af36af3e13346662d38f43 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 7 Apr 2022 15:37:48 -0700 Subject: [PATCH 52/78] Connected Client: Minor C API & doc tweaks --- C/include/c4ConnectedClient.h | 37 +++++++++++++++++----------- C/include/c4ConnectedClientTypes.h | 17 ++++++++----- Replicator/c4ConnectedClient_CAPI.cc | 7 +++--- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index f1eef2952..ce737c469 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -16,29 +16,36 @@ C4_ASSUME_NONNULL_BEGIN C4API_BEGIN_DECLS +/** \defgroup ConnectedClient Connected Client (Remote Database) + @{ + The Connected Client API allows you to get and put documents, and listen for changes, + directly on a remote database server (Sync Gateway or a Couchbase Lite sync listener), + without any local database. */ + /** Creates a new connected client and starts it automatically. - \note No need to call the c4client_start(). + \note No need to call \ref c4client_start. - @param params Connected Client parameters (see above.) + @param params Connected Client parameters. @param error Error will be written here if the function fails. - @result A new C4ConnectedClient, or NULL on failure. */ + @result A new \ref C4ConnectedClient, or NULL on failure. */ C4ConnectedClient* c4client_new(const C4ConnectedClientParameters* params, C4Error* error) C4API; /** Gets the current revision of a document from the server. - You can set the `unlessRevID` parameter to avoid getting a redundant copy of a - revision you already have. + You can set the `unlessRevID` parameter to avoid getting a redundant copy of a + revision you already have. - @param docID The document ID. - @param collectionID The name of the document's collection, or `nullslice` for default. - @param unlessRevID If non-null, and equal to the current server-side revision ID, - the server will return error {WebSocketDomain, 304}. - @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. - @param callback Callback for getting document. - @param context Client value passed to getDocument callback - @param error On failure, the error info will be stored here. */ -void c4client_getDoc(C4ConnectedClient*, + @param docID The document ID. + @param collectionID The name of the document's collection, or `nullslice` for default. + @param unlessRevID If non-null, and equal to the current server-side revision ID, + the server will return error {WebSocketDomain, 304} instead of the document. + @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. + @param callback Callback for getting document. + @param context Client value passed to the callback + @param error On failure to issue the call, the error info will be stored here. + @return True if the request is sent, false if it failed (check `error`.) */ +bool c4client_getDoc(C4ConnectedClient*, C4Slice docID, C4Slice collectionID, C4Slice unlessRevID, @@ -60,5 +67,7 @@ void c4client_stop(C4ConnectedClient*) C4API; it will keep going. If you need the client to stop, call \ref c4client_stop first. */ void c4client_free(C4ConnectedClient*) C4API; +/** @} */ + C4API_END_DECLS C4_ASSUME_NONNULL_END diff --git a/C/include/c4ConnectedClientTypes.h b/C/include/c4ConnectedClientTypes.h index 2850dc863..8e698ab03 100644 --- a/C/include/c4ConnectedClientTypes.h +++ b/C/include/c4ConnectedClientTypes.h @@ -16,16 +16,19 @@ C4_ASSUME_NONNULL_BEGIN C4API_BEGIN_DECLS -/** Result of a successful `c4client_getDoc` call. */ +/** \defgroup ConnectedClient Connected Client (Remote Database) + @{ */ + +/** Result of a successful \ref c4client_getDoc call. */ typedef struct C4DocResponse { - C4HeapSlice docID; - C4HeapSlice revID; - C4HeapSlice body; - bool deleted; + C4HeapSlice docID; ///< The document ID + C4HeapSlice revID; ///< The revision ID + C4HeapSlice body; ///< The document body (Fleece or JSON, as requested) + bool deleted; ///< True if the document is deleted } C4DocResponse; -/** Parameters describing a connected client, used when creating a C4ConnectedClient. */ +/** Parameters describing a connected client, used when creating a \ref C4ConnectedClient. */ typedef struct C4ConnectedClientParameters { C4Slice url; /// Date: Thu, 7 Apr 2022 15:39:14 -0700 Subject: [PATCH 53/78] Doxygen fixes * Doxygen now ignores `C4API_BEGIN_DECLS` * Updated the list of headers that trigger a Doxygen rebuild --- C/Doxyfile | 3 ++- C/DoxygenDependencies.txt | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/C/Doxyfile b/C/Doxyfile index 6b231b3c2..3f909e383 100644 --- a/C/Doxyfile +++ b/C/Doxyfile @@ -597,7 +597,7 @@ SORT_MEMBERS_CTORS_1ST = NO # appear in their defined order. # The default value is: NO. -SORT_GROUP_NAMES = NO +SORT_GROUP_NAMES = YES # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by # fully-qualified names, including namespaces. If set to NO, the class list will @@ -2039,6 +2039,7 @@ INCLUDE_FILE_PATTERNS = PREDEFINED = DOXYGEN_PARSING=1 \ C4API= \ + C4API_BEGIN_DECLS= \ C4NULLABLE= \ C4NONNULL= \ C4_RETURNS_NONNULL= \ diff --git a/C/DoxygenDependencies.txt b/C/DoxygenDependencies.txt index 6ce57b9b1..4d683109e 100644 --- a/C/DoxygenDependencies.txt +++ b/C/DoxygenDependencies.txt @@ -1,19 +1,27 @@ -$(SRCROOT)/../C/include/c4.h +$(SRCROOT)/../C/Doxyfile $(SRCROOT)/../C/include/c4Base.h $(SRCROOT)/../C/include/c4BlobStore.h +$(SRCROOT)/../C/include/c4BlobStoreTypes.h $(SRCROOT)/../C/include/c4Certificate.h +$(SRCROOT)/../C/include/c4CertificateTypes.h $(SRCROOT)/../C/include/c4Collection.h $(SRCROOT)/../C/include/c4Compat.h +$(SRCROOT)/../C/include/c4ConnectedClient.h +$(SRCROOT)/../C/include/c4ConnectedClientTypes.h $(SRCROOT)/../C/include/c4Database.h $(SRCROOT)/../C/include/c4DatabaseTypes.h $(SRCROOT)/../C/include/c4DocEnumerator.h $(SRCROOT)/../C/include/c4DocEnumeratorTypes.h $(SRCROOT)/../C/include/c4Document+Fleece.h $(SRCROOT)/../C/include/c4Document.h +$(SRCROOT)/../C/include/c4DocumentStruct.h $(SRCROOT)/../C/include/c4DocumentTypes.h +$(SRCROOT)/../C/include/c4Error.h $(SRCROOT)/../C/include/c4Index.h $(SRCROOT)/../C/include/c4IndexTypes.h $(SRCROOT)/../C/include/c4Listener.h +$(SRCROOT)/../C/include/c4ListenerTypes.h +$(SRCROOT)/../C/include/c4Log.h $(SRCROOT)/../C/include/c4Observer.h $(SRCROOT)/../C/include/c4PredictiveQuery.h $(SRCROOT)/../C/include/c4Query.h @@ -21,3 +29,4 @@ $(SRCROOT)/../C/include/c4QueryTypes.h $(SRCROOT)/../C/include/c4Replicator.h $(SRCROOT)/../C/include/c4ReplicatorTypes.h $(SRCROOT)/../C/include/c4Socket.h +$(SRCROOT)/../C/include/c4SocketTypes.h From e889a68298f15e74b25bab9957dbacece6852b69 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 8 Apr 2022 17:27:19 -0700 Subject: [PATCH 54/78] ConnectedClient: added a blobs/attachments test --- Replicator/ConnectedClient/ConnectedClient.cc | 2 +- Replicator/ConnectedClient/ConnectedClient.hh | 4 +- Replicator/tests/ConnectedClientTest.cc | 80 +++++++++++++++++-- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index ed3eb3199..ac4a3d254 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -39,7 +39,7 @@ namespace litecore::client { using namespace blip; - alloc_slice ConnectedClient::Delegate::getBlobContents(slice hexDigest, C4Error *error) { + alloc_slice ConnectedClient::Delegate::getBlobContents(slice digestString, C4Error *error) { Warn("ConnectedClient's delegate needs to override getBlobContents!"); *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); return nullslice; diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 426981f15..c56665a49 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -58,10 +58,10 @@ namespace litecore::client { /** You must override this if you upload documents containing blobs. The default implementation always returns a Not Found error. - @param hexDigest The value of the blob's `digest` property, a hex SHA-1 digest. + @param digestString The value of the blob's `digest` property. @param error If you can't return the contents, store an error here. @return The blob's contents, or `nullslice` if an error occurred. */ - virtual alloc_slice getBlobContents(slice hexDigest, C4Error *error); + virtual alloc_slice getBlobContents(slice digestString, C4Error *error); virtual ~Delegate() =default; }; diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 2bf530738..c015e777a 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -20,7 +20,9 @@ #include "ConnectedClient.hh" #include "Replicator.hh" #include "LoopbackProvider.hh" +#include "StringUtil.hh" #include "fleece/Fleece.hh" +#include using namespace std; @@ -35,6 +37,16 @@ class ConnectedClientLoopbackTest : public C4Test, { public: + ConnectedClientLoopbackTest() { + _serverOptions = make_retained(kC4Passive,kC4Passive); + _serverOptions->setProperty(kC4ReplicatorOptionAllowConnectedClient, true); + _serverOptions->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); + } + + ~ConnectedClientLoopbackTest() { + stop(); + } + virtual C4ConnectedClientParameters params() { return {}; } @@ -43,16 +55,12 @@ class ConnectedClientLoopbackTest : public C4Test, std::unique_lock lock(_mutex); Assert(!_serverRunning && !_clientRunning); - auto serverOpts = make_retained(kC4Passive,kC4Passive); - serverOpts->setProperty(kC4ReplicatorOptionAllowConnectedClient, true); - serverOpts->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); - c4::ref serverDB = c4db_openAgain(db, ERROR_INFO()); REQUIRE(serverDB); _server = new repl::Replicator(serverDB, new LoopbackWebSocket(alloc_slice("ws://srv/"), Role::Server, {}), - *this, serverOpts); + *this, _serverOptions); _client = new client::ConnectedClient(new LoopbackWebSocket(alloc_slice("ws://cli/"), Role::Client, {}), @@ -108,8 +116,18 @@ class ConnectedClientLoopbackTest : public C4Test, } - ~ConnectedClientLoopbackTest() { - stop(); + // ConnectedClient delegate: + + alloc_slice getBlobContents(slice digestString, C4Error *error) override { + if (auto i = _blobs.find(string(digestString)); i != _blobs.end()) { + alloc_slice blob = i->second; + _blobs.erase(i); // remove blob after it's requested + return blob; + } else { + WarnError("getBlobContents called on unknown blob %.*s", FMTSLICE(digestString)); + *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); + return nullslice; + } } @@ -141,6 +159,8 @@ class ConnectedClientLoopbackTest : public C4Test, } + // Replicator delegate: + void replicatorGotHTTPResponse(repl::Replicator* NONNULL, int status, const websocket::Headers &headers) override { } @@ -163,10 +183,12 @@ class ConnectedClientLoopbackTest : public C4Test, Retained _server; + Retained _serverOptions; Retained _client; bool _clientRunning = false, _serverRunning = false; mutex _mutex; condition_variable _cond; + unordered_map _blobs; }; @@ -260,7 +282,7 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getBlob", "[ConnectedClient]") { } -TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putRev", "[ConnectedClient]") { +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc", "[ConnectedClient]") { importJSONLines(sFixturesDir + "names_100.json"); start(); @@ -355,6 +377,48 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "observeCollection", "[ConnectedCl } +#pragma mark - BLOBS / ATTACHMENTS: + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Blobs Legacy Mode", "[ConnectedClient][blob]") { + // Ensure the 'server' (LiteCore replicator) will not strip the `_attachments` property: + _serverOptions->setProperty("disable_blob_support"_sl, true); + start(); + + // Register the blobs with the ConnectedClient delegate, by digest: + _blobs["sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck="] = alloc_slice("Hey, this is an attachment!"); + _blobs["sha1-rATs731fnP+PJv2Pm/WXWZsCw48="] = alloc_slice("So is this"); + _blobs["sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk="] = alloc_slice(""); + + // Construct the document body, and PUT it: + string json = "{'attached':[{'@type':'blob','content_type':'text/plain','digest':'sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck=','length':27}," + "{'@type':'blob','content_type':'text/plain','digest':'sha1-rATs731fnP+PJv2Pm/WXWZsCw48=','length':10}," + "{'@type':'blob','content_type':'text/plain','digest':'sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk=','length':0}]}"; + replace(json, '\'', '"'); + auto rq = _client->putDoc("att1", nullslice, + "1-1111", + nullslice, + C4RevisionFlags{}, + Doc::fromJSON(json).data()); + rq.blockUntilReady(); + + // All blobs should have been requested by the server and removed from the map: + CHECK(_blobs.empty()); + + // Now read the doc from the server's database: + json = getDocJSON(db, "att1"_sl); + replace(json, '"', '\''); + CHECK(json == + "{'_attachments':{'blob_/attached/0':{'content_type':'text/plain','digest':'sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck=','length':27,'revpos':1,'stub':true}," + "'blob_/attached/1':{'content_type':'text/plain','digest':'sha1-rATs731fnP+PJv2Pm/WXWZsCw48=','length':10,'revpos':1,'stub':true}," + "'blob_/attached/2':{'content_type':'text/plain','digest':'sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk=','length':0,'revpos':1,'stub':true}}," + "'attached':[{'@type':'blob','content_type':'text/plain','digest':'sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck=','length':27}," + "{'@type':'blob','content_type':'text/plain','digest':'sha1-rATs731fnP+PJv2Pm/WXWZsCw48=','length':10}," + "{'@type':'blob','content_type':'text/plain','digest':'sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk=','length':0}]}"); + +} + + #pragma mark - ENCRYPTION: From 4495d4ff7d5f7b784938709e79efd27c24b59501 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Fri, 8 Apr 2022 18:01:29 -0700 Subject: [PATCH 55/78] ConnectedClient: getBlobContents() now takes a C4BlobKey --- Replicator/ConnectedClient/ConnectedClient.cc | 7 +++-- Replicator/ConnectedClient/ConnectedClient.hh | 28 ++++++++++++++----- Replicator/tests/ConnectedClientTest.cc | 7 +++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index ac4a3d254..39bf64872 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -39,7 +39,7 @@ namespace litecore::client { using namespace blip; - alloc_slice ConnectedClient::Delegate::getBlobContents(slice digestString, C4Error *error) { + alloc_slice ConnectedClient::Delegate::getBlobContents(const C4BlobKey &, C4Error *error) { Warn("ConnectedClient's delegate needs to override getBlobContents!"); *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); return nullslice; @@ -392,7 +392,10 @@ namespace litecore::client { alloc_slice contents; C4Error error = {}; try { - contents = _delegate->getBlobContents(req->property("digest"_sl), &error); + if (auto blobKey = C4BlobKey::withDigestString(req->property("digest"_sl))) + contents = _delegate->getBlobContents(*blobKey, &error); + else + error = C4Error::make(WebSocketDomain, 400, "Invalid 'digest' property in request"); } catch (...) { error = C4Error::fromCurrentException(); } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index c56665a49..b5d2363a8 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -43,7 +43,7 @@ namespace litecore::client { Delegate&, const C4ConnectedClientParameters&); - /** ConnectedClient Delegate API. Almost identical to `Replicator::Delegate` */ + /** ConnectedClient delegate API. (Similar to `Replicator::Delegate`) */ class Delegate { public: virtual void clientGotHTTPResponse(ConnectedClient* NONNULL, @@ -56,12 +56,20 @@ namespace litecore::client { virtual void clientConnectionClosed(ConnectedClient* NONNULL, const CloseStatus&) { } - /** You must override this if you upload documents containing blobs. - The default implementation always returns a Not Found error. - @param digestString The value of the blob's `digest` property. + /** Returns the contents of a blob given its key (SHA-1 digest) as found in a blob in + a document being uploaded to the server. + + This method is called after the \ref putDoc method is called, but before its async + value resolves. It's not guaranteed to be called for every blob in the document, + only those that are not yet known to the server. + + You must override this method if you upload documents containing blobs. + The default implementation always returns a Not Found error, + which will cause the upload to fail. + @param blobKey The blob's binary digest. @param error If you can't return the contents, store an error here. @return The blob's contents, or `nullslice` if an error occurred. */ - virtual alloc_slice getBlobContents(slice digestString, C4Error *error); + virtual alloc_slice getBlobContents(const C4BlobKey &blobKey, C4Error *error); virtual ~Delegate() =default; }; @@ -89,14 +97,18 @@ namespace litecore::client { slice unlessRevID, bool asFleece = true); - /// Gets the contents of a blob given its digest. + /// Downloads the contents of a blob given its digest. /// @param blobKey The binary digest of the blob. - /// @param compress True if the blob should be downloaded in compressed form. + /// @param compress If true, a request that the server compress the blob's data during + /// transmission. (This does not affect the data you receive.) /// @return An async value that, when resolved, contains either the blob body or a C4Error. actor::Async getBlob(C4BlobKey blobKey, bool compress); /// Pushes a new document revision to the server. + /// @note If the document body contains any blob references, your delegate must implement + /// the \ref getBlobContents method. + /// /// @param docID The document ID. /// @param collectionID The name of the document's collection, or `nullslice` for default. /// @param revID The revision ID you're sending. @@ -112,6 +124,8 @@ namespace litecore::client { C4RevisionFlags revisionFlags, slice fleeceData); + //---- Observer + /// Registers a listener function that will be called when any document is changed. /// @note To cancel, pass a null callback. /// @param collectionID The ID of the collection to observe. diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index c015e777a..a7629c61d 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -118,13 +118,14 @@ class ConnectedClientLoopbackTest : public C4Test, // ConnectedClient delegate: - alloc_slice getBlobContents(slice digestString, C4Error *error) override { - if (auto i = _blobs.find(string(digestString)); i != _blobs.end()) { + alloc_slice getBlobContents(const C4BlobKey &blobKey, C4Error *error) override { + string digestString = blobKey.digestString(); + if (auto i = _blobs.find(digestString); i != _blobs.end()) { alloc_slice blob = i->second; _blobs.erase(i); // remove blob after it's requested return blob; } else { - WarnError("getBlobContents called on unknown blob %.*s", FMTSLICE(digestString)); + WarnError("getBlobContents called on unknown blob %s", digestString.c_str()); *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); return nullslice; } From 3d43cab7f7ea9e6c0fc7684250c7abc6498340db Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 11 Apr 2022 14:02:16 -0700 Subject: [PATCH 56/78] ConnectedClient: Legacy-attachment unit tests also moved the test class into a header for clarity --- Replicator/tests/ConnectedClientTest.cc | 231 +++++------------------ Replicator/tests/ConnectedClientTest.hh | 181 ++++++++++++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 2 + 3 files changed, 229 insertions(+), 185 deletions(-) create mode 100644 Replicator/tests/ConnectedClientTest.hh diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index a7629c61d..811ac0678 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -16,184 +16,10 @@ // limitations under the License. // -#include "c4Test.hh" -#include "ConnectedClient.hh" -#include "Replicator.hh" -#include "LoopbackProvider.hh" -#include "StringUtil.hh" -#include "fleece/Fleece.hh" -#include +#include "ConnectedClientTest.hh" -using namespace std; -using namespace fleece; -using namespace litecore; -using namespace litecore::websocket; - - -class ConnectedClientLoopbackTest : public C4Test, - public repl::Replicator::Delegate, - public client::ConnectedClient::Delegate -{ -public: - - ConnectedClientLoopbackTest() { - _serverOptions = make_retained(kC4Passive,kC4Passive); - _serverOptions->setProperty(kC4ReplicatorOptionAllowConnectedClient, true); - _serverOptions->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); - } - - ~ConnectedClientLoopbackTest() { - stop(); - } - - virtual C4ConnectedClientParameters params() { - return {}; - } - - void start() { - std::unique_lock lock(_mutex); - Assert(!_serverRunning && !_clientRunning); - - c4::ref serverDB = c4db_openAgain(db, ERROR_INFO()); - REQUIRE(serverDB); - _server = new repl::Replicator(serverDB, - new LoopbackWebSocket(alloc_slice("ws://srv/"), - Role::Server, {}), - *this, _serverOptions); - - _client = new client::ConnectedClient(new LoopbackWebSocket(alloc_slice("ws://cli/"), - Role::Client, {}), - *this, - params()); - - Headers headers; - headers.add("Set-Cookie"_sl, "flavor=chocolate-chip"_sl); - LoopbackWebSocket::bind(_server->webSocket(), _client->webSocket(), headers); - - _clientRunning = _serverRunning = true; - _server->start(); - _client->start(); - } - - - void stop() { - std::unique_lock lock(_mutex); - if (_server) { - _server->stop(); - _server = nullptr; - } - if (_client) { - _client->stop(); - _client = nullptr; - } - - Log("+++ Waiting for client & replicator to stop..."); - _cond.wait(lock, [&]{return !_clientRunning && !_serverRunning;}); - } - - - template - auto waitForResponse(actor::Async &asyncResult) { - asyncResult.blockUntilReady(); - - Log("++++ Async response available!"); - if (auto err = asyncResult.error()) - FAIL("Response returned an error " << err); - return asyncResult.result().value(); - } - - - template - C4Error waitForErrorResponse(actor::Async &asyncResult) { - asyncResult.blockUntilReady(); - - Log("++++ Async response available!"); - auto err = asyncResult.error(); - if (!err) - FAIL("Response did not return an error"); - return err; - } - - - // ConnectedClient delegate: - - alloc_slice getBlobContents(const C4BlobKey &blobKey, C4Error *error) override { - string digestString = blobKey.digestString(); - if (auto i = _blobs.find(digestString); i != _blobs.end()) { - alloc_slice blob = i->second; - _blobs.erase(i); // remove blob after it's requested - return blob; - } else { - WarnError("getBlobContents called on unknown blob %s", digestString.c_str()); - *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); - return nullslice; - } - } - - - void clientGotHTTPResponse(client::ConnectedClient* NONNULL, - int status, - const websocket::Headers &headers) override - { - Log("+++ Client got HTTP response"); - } - void clientGotTLSCertificate(client::ConnectedClient* NONNULL, - slice certData) override - { - Log("+++ Client got TLS certificate"); - } - void clientStatusChanged(client::ConnectedClient* NONNULL, - C4ReplicatorActivityLevel level) override { - Log("+++ Client status changed: %d", int(level)); - if (level == kC4Stopped) { - std::unique_lock lock(_mutex); - _clientRunning = false; - if (!_clientRunning && !_serverRunning) - _cond.notify_all(); - } - } - void clientConnectionClosed(client::ConnectedClient* NONNULL, - const CloseStatus &close) override { - Log("+++ Client connection closed: reason=%d, code=%d, message=%.*s", - int(close.reason), close.code, FMTSLICE(close.message)); - } - - - // Replicator delegate: - - void replicatorGotHTTPResponse(repl::Replicator* NONNULL, - int status, - const websocket::Headers &headers) override { } - void replicatorGotTLSCertificate(slice certData) override { } - void replicatorStatusChanged(repl::Replicator* NONNULL, - const repl::Replicator::Status &status) override { - if (status.level == kC4Stopped) { - std::unique_lock lock(_mutex); - _serverRunning = false; - if (!_clientRunning && !_serverRunning) - _cond.notify_all(); - } - } - void replicatorConnectionClosed(repl::Replicator* NONNULL, - const CloseStatus&) override { } - void replicatorDocumentsEnded(repl::Replicator* NONNULL, - const repl::Replicator::DocumentsEnded&) override { } - void replicatorBlobProgress(repl::Replicator* NONNULL, - const repl::Replicator::BlobProgress&) override { } - - - Retained _server; - Retained _serverOptions; - Retained _client; - bool _clientRunning = false, _serverRunning = false; - mutex _mutex; - condition_variable _cond; - unordered_map _blobs; -}; - - -#pragma mark - TESTS: +#pragma mark - GET: TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev", "[ConnectedClient]") { @@ -283,6 +109,9 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getBlob", "[ConnectedClient]") { } +#pragma mark - PUT: + + TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc", "[ConnectedClient]") { importJSONLines(sFixturesDir + "names_100.json"); start(); @@ -337,6 +166,9 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Failure", "[ConnectedClien } +#pragma mark - OBSERVE: + + TEST_CASE_METHOD(ConnectedClientLoopbackTest, "observeCollection", "[ConnectedClient]") { { // Start with a single doc that should not be sent to the observer @@ -378,7 +210,36 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "observeCollection", "[ConnectedCl } -#pragma mark - BLOBS / ATTACHMENTS: +#pragma mark - LEGACY ATTACHMENTS: + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "getRev Blobs Legacy Mode", "[ConnectedClient][blob]") { + static constexpr slice kJSON5WithAttachments = + "{_attachments:{'blob_/attached/0':{content_type:'text/plain',digest:'sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck=',length:27,revpos:1,stub:true}," + "'blob_/attached/1':{content_type:'text/plain',digest:'sha1-rATs731fnP+PJv2Pm/WXWZsCw48=',length:10,revpos:1,stub:true}," + "empty:{content_type:'text/plain',digest:'sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk=',length:0,revpos:1,stub:true}}," + "attached:[{'@type':'blob',content_type:'text/plain',digest:'sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck=',length:27}," + "{'@type':'blob',content_type:'text/plain',digest:'sha1-rATs731fnP+PJv2Pm/WXWZsCw48=',length:10}]}"; + static constexpr slice kJSON5WithoutAttachments = + "{_attachments:{empty:{content_type:'text/plain',digest:'sha1-2jmj7l5rSw0yVb/vlWAYkK/YBwk=',length:0,revpos:1,stub:true}}," + "attached:[{'@type':'blob',content_type:'text/plain',digest:'sha1-ERWD9RaGBqLSWOQ+96TZ6Kisjck=',length:27}," + "{'@type':'blob',content_type:'text/plain',digest:'sha1-rATs731fnP+PJv2Pm/WXWZsCw48=',length:10}]}"; + + createFleeceRev(db, "att1"_sl, "1-1111"_sl, slice(json5(kJSON5WithAttachments))); + + // Ensure the 'server' (LiteCore replicator) will not strip the `_attachments` property: + _serverOptions->setProperty("disable_blob_support"_sl, true); + start(); + + auto asyncResult = _client->getDoc("att1", nullslice, nullslice); + auto rev = waitForResponse(asyncResult); + CHECK(rev.docID == "att1"); + Doc doc(rev.body); + Dict props = doc.asDict(); + string json(props.toJSON5()); + replace(json, '"', '\''); + CHECK(slice(json) == kJSON5WithoutAttachments); +} TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Blobs Legacy Mode", "[ConnectedClient][blob]") { @@ -466,12 +327,10 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc encrypted no callback", "[ class ConnectedClientEncryptedLoopbackTest : public ConnectedClientLoopbackTest { public: - C4ConnectedClientParameters params() override{ - auto p = ConnectedClientLoopbackTest::params(); - p.propertyEncryptor = &encryptor; - p.propertyDecryptor = &decryptor; - p.callbackContext = &_encryptorContext; - return p; + ConnectedClientEncryptedLoopbackTest() { + _params.propertyEncryptor = &encryptor; + _params.propertyDecryptor = &decryptor; + _params.callbackContext = &_encryptorContext; } static alloc_slice unbreakableEncryption(slice cleartext, int8_t delta) { @@ -485,7 +344,9 @@ class ConnectedClientEncryptedLoopbackTest : public ConnectedClientLoopbackTest slice docID; slice keyPath; bool called = false; - } _encryptorContext; + }; + + TestEncryptorContext _encryptorContext; static C4SliceResult encryptor(void* rawCtx, C4String documentID, @@ -563,4 +424,4 @@ TEST_CASE_METHOD(ConnectedClientEncryptedLoopbackTest, "putDoc encrypted", "[Con CHECK(json == kEncryptedDocJSON); } -#endif +#endif // COUCHBASE_ENTERPRISE diff --git a/Replicator/tests/ConnectedClientTest.hh b/Replicator/tests/ConnectedClientTest.hh new file mode 100644 index 000000000..d21523591 --- /dev/null +++ b/Replicator/tests/ConnectedClientTest.hh @@ -0,0 +1,181 @@ +// +// ConnectedClientTest.hh +// +// Copyright © 2022 Couchbase. All rights reserved. +// + +#pragma once +#include "c4Test.hh" +#include "ConnectedClient.hh" +#include "Replicator.hh" +#include "LoopbackProvider.hh" +#include "StringUtil.hh" +#include "fleece/Fleece.hh" +#include + + +using namespace std; +using namespace fleece; +using namespace litecore; +using namespace litecore::websocket; + + +/// Test class for Connected Client unit tests. Runs a LiteCore replicator in passive mode. +class ConnectedClientLoopbackTest : public C4Test, + public repl::Replicator::Delegate, + public client::ConnectedClient::Delegate +{ +public: + + ConnectedClientLoopbackTest() { + _serverOptions = make_retained(kC4Passive,kC4Passive); + _serverOptions->setProperty(kC4ReplicatorOptionAllowConnectedClient, true); + _serverOptions->setProperty(kC4ReplicatorOptionNoIncomingConflicts, true); + } + + ~ConnectedClientLoopbackTest() { + stop(); + } + + void start() { + std::unique_lock lock(_mutex); + Assert(!_serverRunning && !_clientRunning); + + c4::ref serverDB = c4db_openAgain(db, ERROR_INFO()); + REQUIRE(serverDB); + _server = new repl::Replicator(serverDB, + new LoopbackWebSocket(alloc_slice("ws://srv/"), + Role::Server, {}), + *this, _serverOptions); + + _client = new client::ConnectedClient(new LoopbackWebSocket(alloc_slice("ws://cli/"), + Role::Client, {}), + *this, + _params); + + Headers headers; + headers.add("Set-Cookie"_sl, "flavor=chocolate-chip"_sl); + LoopbackWebSocket::bind(_server->webSocket(), _client->webSocket(), headers); + + _clientRunning = _serverRunning = true; + _server->start(); + _client->start(); + } + + + void stop() { + std::unique_lock lock(_mutex); + if (_server) { + _server->stop(); + _server = nullptr; + } + if (_client) { + _client->stop(); + _client = nullptr; + } + + Log("+++ Waiting for client & replicator to stop..."); + _cond.wait(lock, [&]{return !_clientRunning && !_serverRunning;}); + } + + + template + auto waitForResponse(actor::Async &asyncResult) { + asyncResult.blockUntilReady(); + + Log("++++ Async response available!"); + if (auto err = asyncResult.error()) + FAIL("Response returned an error " << err); + return asyncResult.result().value(); + } + + + template + C4Error waitForErrorResponse(actor::Async &asyncResult) { + asyncResult.blockUntilReady(); + + Log("++++ Async response available!"); + auto err = asyncResult.error(); + if (!err) + FAIL("Response did not return an error"); + return err; + } + + + //---- ConnectedClient delegate: + + alloc_slice getBlobContents(const C4BlobKey &blobKey, C4Error *error) override { + string digestString = blobKey.digestString(); + if (auto i = _blobs.find(digestString); i != _blobs.end()) { + alloc_slice blob = i->second; + _blobs.erase(i); // remove blob after it's requested + return blob; + } else { + WarnError("getBlobContents called on unknown blob %s", digestString.c_str()); + *error = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); + return nullslice; + } + } + + + void clientGotHTTPResponse(client::ConnectedClient* NONNULL, + int status, + const websocket::Headers &headers) override + { + Log("+++ Client got HTTP response"); + } + void clientGotTLSCertificate(client::ConnectedClient* NONNULL, + slice certData) override + { + Log("+++ Client got TLS certificate"); + } + void clientStatusChanged(client::ConnectedClient* NONNULL, + C4ReplicatorActivityLevel level) override { + Log("+++ Client status changed: %d", int(level)); + if (level == kC4Stopped) { + std::unique_lock lock(_mutex); + _clientRunning = false; + if (!_clientRunning && !_serverRunning) + _cond.notify_all(); + } + } + void clientConnectionClosed(client::ConnectedClient* NONNULL, + const CloseStatus &close) override { + Log("+++ Client connection closed: reason=%d, code=%d, message=%.*s", + int(close.reason), close.code, FMTSLICE(close.message)); + } + + + //---- Replicator delegate: + + void replicatorGotHTTPResponse(repl::Replicator* NONNULL, + int status, + const websocket::Headers &headers) override { } + void replicatorGotTLSCertificate(slice certData) override { } + void replicatorStatusChanged(repl::Replicator* NONNULL, + const repl::Replicator::Status &status) override { + if (status.level == kC4Stopped) { + std::unique_lock lock(_mutex); + _serverRunning = false; + if (!_clientRunning && !_serverRunning) + _cond.notify_all(); + } + } + void replicatorConnectionClosed(repl::Replicator* NONNULL, + const CloseStatus&) override { } + void replicatorDocumentsEnded(repl::Replicator* NONNULL, + const repl::Replicator::DocumentsEnded&) override { } + void replicatorBlobProgress(repl::Replicator* NONNULL, + const repl::Replicator::BlobProgress&) override { } + + + C4ConnectedClientParameters _params {}; + Retained _server; + Retained _serverOptions; + Retained _client; + mutex _mutex; + condition_variable _cond; + unordered_map _blobs; + bool _clientRunning = false; + bool _serverRunning = false; +}; diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index a64329100..7a61d200f 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1175,6 +1175,7 @@ 2764ED3623870F15007F020F /* c4Database.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4Database.hh; sourceTree = ""; }; 2764ED3723873B9E007F020F /* c4.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = c4.txt; path = scripts/c4.txt; sourceTree = ""; }; 2764ED3823873B9E007F020F /* c4_exp.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = c4_exp.txt; path = scripts/c4_exp.txt; sourceTree = ""; }; + 276592A02804B6700037DD04 /* ConnectedClientTest.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ConnectedClientTest.hh; sourceTree = ""; }; 276683B41DC7DD2E00E3F187 /* SequenceTracker.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SequenceTracker.cc; sourceTree = ""; }; 276683B51DC7DD2E00E3F187 /* SequenceTracker.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SequenceTracker.hh; sourceTree = ""; }; 2766F9E51E64CC03008FC9E5 /* SequenceSet.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SequenceSet.hh; sourceTree = ""; }; @@ -2464,6 +2465,7 @@ 2761F3F61EEA00C3006D4BB8 /* CookieStoreTest.cc */, 27A83D53269E3E69002B7EBA /* PropertyEncryptionTests.cc */, 2752C7A627BF18F2001C1B76 /* ConnectedClientTest.cc */, + 276592A02804B6700037DD04 /* ConnectedClientTest.hh */, ); path = tests; sourceTree = ""; From 6f24c66f808a0f78d966aa0df31704246c5a21fa Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 11 Apr 2022 14:58:17 -0700 Subject: [PATCH 57/78] Added ConnectedClientProtocol.md --- docs/overview/ConnectedClientProtocol.md | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/overview/ConnectedClientProtocol.md diff --git a/docs/overview/ConnectedClientProtocol.md b/docs/overview/ConnectedClientProtocol.md new file mode 100644 index 000000000..dd72c46d1 --- /dev/null +++ b/docs/overview/ConnectedClientProtocol.md @@ -0,0 +1,66 @@ +# Connected Client Protocol + +Couchbase Mobile 3.x + +Jens Alfke — 11 April 2022 + +## 1. Introduction + +The Connected Client protocol allows mobile clients to access a server-side database (bucket) directly, without needing a local database replica. In its first incarnation it supports document CRUD operations and database change listeners. + +Sync Gateway already supports a REST API (inherited from CouchDB) that allows this, but our past experience with version 1.x showed that this API is difficult to support properly and inherits significant overhead from HTTP. As a result **we’ve chosen to implement Connected Client over BLIP, using an extension of the replicator protocol**. + +This document describes those extensions. + +## 2. Connecting + +Opening a client connection is identical to opening a replicator connection. It’s the same WebSocket endpoint (`/dbname/_blipsync`), the same BLIP protocol, and the same authentication. + +## 3. Message Types + +These are the only messages the Connected Client implementation currently sends. + +### 3.1. `getRev` + +Requests a document’s current revision from the peer. + +> **Note**: This is very much like the replicator protocol’s `rev` message, except that it’s sent as a *request* for a revision, not an announcement of one. + +Request: + +* `id`: Document ID +* `ifNotRev`: (optional) If present, and its value is equal to the document’s current revision ID, the peer SHOULD respond with a HTTP/304 error instead of sending the revision + +Response: + +* `rev`: The current revision ID +* `deleted`: (optional) Set to `true` if the document is deleted and this revision is a “tombstone” +* Body: The current revision body as JSON + +### 3.2. `getAttachment` + +Exactly as in the replicator protocol. + +### 3.3. `putRev` + +Uploads a new revision to the peer. + +The properties and behavior are identical to the replicator protocol’s `rev` message. The reason for a new message type is because the LiteCore replicator assumes that incoming `rev` messages are caused by prior `changes` responses, and would become confused if it received a standalone `rev` message. This made it apparent that it would be cleaner to treat this as a separate type of request. + +Request: *same as existing `rev` message* + +Response: *same as existing `rev` message* (never sent no-reply) + +> **Note:** As usual, the receiving peer may send one or more getAttachment requests back to the originator if it needs the contents of any blobs/attachments in the revision. + +### 3.4. `subChanges` + +As in the replicator protocol, with one addition: the request’s `since` property may have a value of “`NOW`”. The receiving peer MUST interpret this as equal to the database/bucket’s latest sequence number. This causes only future changes to be sent. + +> **Note**: This value is always used along with the `continuous` property, since otherwise no changes would be returned. + +### 3.5. `unsubChanges` + +This terminates the effect of `subChanges`: the receiving peer MUST stop sending `changes` messages as soon as possible. + +_(No request properties or body defined.)_ From 2afbcc5f2f80139af7796a36a5ef3b2c45e0eff3 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 14 Apr 2022 11:01:12 -0700 Subject: [PATCH 58/78] Connected Client: Some changes to protocol * subChanges: Use "future:true" instead of "since:NOW" * getRev: Don't return tombstones, just respond with 404 error. --- Replicator/ConnectedClient/ConnectedClient.cc | 2 +- Replicator/Pusher+Revs.cc | 61 ++++++++++--------- Replicator/Pusher.cc | 4 +- Replicator/Pusher.hh | 2 +- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 + docs/overview/ConnectedClientProtocol.md | 11 +++- 6 files changed, 47 insertions(+), 35 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 39bf64872..714eb4aa7 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -434,7 +434,7 @@ namespace litecore::client { _registeredChangesHandler = true; } req.setProfile("subChanges"); - req["since"] = "NOW"; + req["future"] = true; req["continuous"] = true; } else { req.setProfile("unsubChanges"); diff --git a/Replicator/Pusher+Revs.cc b/Replicator/Pusher+Revs.cc index 4f95cdcbb..fe66610fa 100644 --- a/Replicator/Pusher+Revs.cc +++ b/Replicator/Pusher+Revs.cc @@ -45,43 +45,31 @@ namespace litecore::repl { } - // Creates a revision message from a RevToSend. Returns a BLIP error code. + // Creates a revision message from a RevToSend. Used by `sendRevision` and `handleGetRev`. bool Pusher::buildRevisionMessage(RevToSend *request, + C4Document *doc, MessageBuilder &msg, - slice ifNotRevID, C4Error *outError) { - // Get the document & revision: + // Select the revision and get its properties: C4Error c4err = {}; Dict root; - Retained doc = _db->getDoc(request->docID, kDocGetAll); if (doc) { - if (request->revID.empty()) { - // When called from `handleGetRev`, all the request has is the docID. - // First check for a conditional get: - if (doc->revID() == ifNotRevID) { - c4error_return(WebSocketDomain, 304, "Not Changed"_sl, outError); - return false; - } - // Populate the request with the revision metadata: + if (doc->selectRevision(request->revID, true)) root = doc->getProperties(); - request->setRevID(doc->revID()); - request->sequence = doc->sequence(); + if (root) { request->flags = doc->selectedRev().flags; - } else if (doc->selectRevision(request->revID, true)) { - root = doc->getProperties(); - } - if (root) - request->flags = doc->selectedRev().flags; - else + } else { revToSendIsObsolete(*request, &c4err); + doc = nullptr; + } } else { c4err = C4Error::make(LiteCoreDomain, kC4ErrorNotFound); } // Encrypt any encryptable properties MutableDict encryptedRoot; - if (root && MayContainPropertiesToEncrypt(doc->getRevisionBody())) { + if (doc && MayContainPropertiesToEncrypt(doc->getRevisionBody())) { logVerbose("Encrypting properties in doc '%.*s'", SPLAT(request->docID)); encryptedRoot = EncryptDocumentProperties(request->docID, root, _options->propertyEncryptor, @@ -90,7 +78,7 @@ namespace litecore::repl { if (encryptedRoot) root = encryptedRoot; else if (c4err) { - root = nullptr; + doc = nullptr; finishedDocumentWithError(request, c4err, false); } } @@ -102,7 +90,8 @@ namespace litecore::repl { msg["id"_sl] = request->docID; msg["rev"_sl] = fullRevID; msg["sequence"_sl] = uint64_t(request->sequence); - if (root) { + + if (doc) { if (!msg.isResponse()) msg.setProfile("rev"); if (request->noConflicts) @@ -162,7 +151,8 @@ namespace litecore::repl { MessageBuilder msg; C4Error c4err; - if (buildRevisionMessage(request, msg, {}, &c4err)) { + Retained doc = _db->getDoc(request->docID, kDocGetAll); + if (buildRevisionMessage(request, doc, msg, &c4err)) { logVerbose("Transmitting 'rev' message with '%.*s' #%.*s", SPLAT(request->docID), SPLAT(request->revID)); sendRequest(msg, [this, request](MessageProgress progress) { @@ -394,12 +384,27 @@ namespace litecore::repl { void Pusher::handleGetRev(Retained req) { alloc_slice docID(req->property("id")); slice ifNotRev = req->property("ifNotRev"); - C4DocumentInfo info = {}; - info.docID = docID; - auto rev = make_retained(info); MessageBuilder response(req); + Retained rev; C4Error c4err; - if (buildRevisionMessage(rev, response, ifNotRev, &c4err)) { + bool ok = false; + + Retained doc = _db->getDoc(docID, kDocGetCurrentRev); + if (!doc || (doc->flags() & kDocDeleted)) { + c4err = C4Error::make(LiteCoreDomain, kC4ErrorNotFound, "Deleted"_sl); + } else if (doc->revID() == ifNotRev) { + c4err = C4Error::make(WebSocketDomain, 304, "Not Changed"_sl); + } else { + C4DocumentInfo info = {}; + info.docID = docID; + info.revID = doc->revID(); + info.sequence = doc->sequence(); + info.flags = doc->flags(); + rev = make_retained(info); + ok = buildRevisionMessage(rev, doc, response, &c4err); + } + + if (ok) { logVerbose("Responding to getRev('%.*s') with rev #%.*s", SPLAT(docID), SPLAT(rev->revID)); req->respond(response); diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index c4da558be..526510574 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -84,8 +84,8 @@ namespace litecore { namespace repl { } C4SequenceNumber since = {}; - if (req->property("since") == "NOW") - since = _db->useLocked()->getLastSequence(); + if (req->boolProperty("future", false)) + since = _db->useLocked()->getLastSequence(); // "future:true" means no past changes else since = C4SequenceNumber(max(req->intProperty("since"_sl), 0l)); _continuous = req->boolProperty("continuous"_sl); diff --git a/Replicator/Pusher.hh b/Replicator/Pusher.hh index 33cc25685..dcee576cc 100644 --- a/Replicator/Pusher.hh +++ b/Replicator/Pusher.hh @@ -83,7 +83,7 @@ namespace litecore { namespace repl { // Pusher+Revs.cc: void maybeSendMoreRevs(); void retryRevs(RevToSendList, bool immediate); - bool buildRevisionMessage(RevToSend*, blip::MessageBuilder&, slice ifNotRevID, C4Error*); + bool buildRevisionMessage(RevToSend*, C4Document*, blip::MessageBuilder&, C4Error*); void sendRevision(Retained); void onRevProgress(Retained rev, const blip::MessageProgress&); void couldntSendRevision(RevToSend* NONNULL); diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 7a61d200f..dfac60a2b 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -1176,6 +1176,7 @@ 2764ED3723873B9E007F020F /* c4.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = c4.txt; path = scripts/c4.txt; sourceTree = ""; }; 2764ED3823873B9E007F020F /* c4_exp.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = c4_exp.txt; path = scripts/c4_exp.txt; sourceTree = ""; }; 276592A02804B6700037DD04 /* ConnectedClientTest.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ConnectedClientTest.hh; sourceTree = ""; }; + 276592A42804DB810037DD04 /* ConnectedClientProtocol.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ConnectedClientProtocol.md; sourceTree = ""; }; 276683B41DC7DD2E00E3F187 /* SequenceTracker.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SequenceTracker.cc; sourceTree = ""; }; 276683B51DC7DD2E00E3F187 /* SequenceTracker.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SequenceTracker.hh; sourceTree = ""; }; 2766F9E51E64CC03008FC9E5 /* SequenceSet.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SequenceSet.hh; sourceTree = ""; }; @@ -2144,6 +2145,7 @@ 2745B7D126825F970012A17A /* Class Diagram.diagrams */, 2745B7D726825F970012A17A /* Class Diagram.pdf */, 2745B7D626825F970012A17A /* Class Diagram.png */, + 276592A42804DB810037DD04 /* ConnectedClientProtocol.md */, 2745B7D026825F970012A17A /* Replicator Diagram.svg */, 2745B7CD26825F970012A17A /* Replicator Diagram.t2d */, ); diff --git a/docs/overview/ConnectedClientProtocol.md b/docs/overview/ConnectedClientProtocol.md index dd72c46d1..1c55e70da 100644 --- a/docs/overview/ConnectedClientProtocol.md +++ b/docs/overview/ConnectedClientProtocol.md @@ -34,7 +34,6 @@ Request: Response: * `rev`: The current revision ID -* `deleted`: (optional) Set to `true` if the document is deleted and this revision is a “tombstone” * Body: The current revision body as JSON ### 3.2. `getAttachment` @@ -55,12 +54,18 @@ Response: *same as existing `rev` message* (never sent no-reply) ### 3.4. `subChanges` -As in the replicator protocol, with one addition: the request’s `since` property may have a value of “`NOW`”. The receiving peer MUST interpret this as equal to the database/bucket’s latest sequence number. This causes only future changes to be sent. +As in the replicator protocol, with one addition: -> **Note**: This value is always used along with the `continuous` property, since otherwise no changes would be returned. +Request: + +* `future`: (Optional) If `true`, the receiving peer MUST not send any existing sequences, only future changes. In other words, this has the same effect as a `since` property whose value is the current sequence ID. (The message SHOULD NOT also contain a `since` property, and the recipient MUST ignore it if present.) + +> **Note**: `future` will always combined with `continuous`, since otherwise no changes would be sent at all! ### 3.5. `unsubChanges` This terminates the effect of `subChanges`: the receiving peer MUST stop sending `changes` messages as soon as possible. +The sender MAY send another `subChanges` message later, to start a new feed. + _(No request properties or body defined.)_ From 4495cf187cd68fe1a5ed2ba77a26e4e2c79a7339 Mon Sep 17 00:00:00 2001 From: Jayahari Vavachan <10448770+jayahariv@users.noreply.github.com> Date: Mon, 18 Apr 2022 10:34:48 +0530 Subject: [PATCH 59/78] Expose docUpdate API (#1437) * Add update doc API * generate revision ID and pass it back to platform as well. --- C/Cpp_include/c4ConnectedClient.hh | 38 +++++++++++++++++++--- C/c4.def | 1 + C/c4.exp | 1 + C/c4.gnu | 1 + C/c4_ee.def | 7 +---- C/c4_ee.exp | 7 +---- C/c4_ee.gnu | 7 +---- C/include/c4ConnectedClient.h | 33 ++++++++++++++----- C/include/c4ConnectedClientTypes.h | 8 +++++ C/scripts/c4.txt | 1 + C/scripts/c4_ee.txt | 6 ---- LiteCore/Database/DocumentFactory.hh | 2 +- LiteCore/Database/TreeDocument.cc | 39 ++++++++++++----------- LiteCore/Database/TreeDocument.hh | 2 ++ Replicator/c4ConnectedClientImpl.hh | 40 ++++++++++++++++++++---- Replicator/c4ConnectedClient_CAPI.cc | 28 +++++++++++++++-- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 +- 17 files changed, 158 insertions(+), 65 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index de7b6390e..74eb48082 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -1,7 +1,7 @@ // // c4ConnectedClient.hh // -// Copyright 2021-Present Couchbase, Inc. +// Copyright 2022-Present Couchbase, Inc. // // Use of this software is governed by the Business Source License included // in the file licenses/BSL-Couchbase.txt. As of the Change Date specified @@ -22,16 +22,46 @@ struct C4ConnectedClient : public fleece::RefCounted, C4Base { /// Creates a new ConnectedClient + /// \note It will automatically starts the client, no need to call `start()`. + /// + /// @param params Connected Client parameters. + /// @result A new \ref C4ConnectedClient, or NULL on failure. static Retained newClient(const C4ConnectedClientParameters ¶ms); /// Gets the current revision of a document from the server. - virtual litecore::actor::Async getDoc(C4Slice, C4Slice, C4Slice, bool) noexcept=0; + /// You can set the `unlessRevID` parameter to avoid getting a redundant copy of a + /// revision you already have. + /// @param docID The document ID. + /// @param collectionID The name of the document's collection, or `nullslice` for default. + /// @param unlessRevID If non-null, and equal to the current server-side revision ID, + /// the server will return error {WebSocketDomain, 304}. + /// @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. + /// @result An async value that, when resolved, contains either a `C4DocResponse` struct + /// or a C4Error. + virtual litecore::actor::Async getDoc(slice docID, + slice collectionID, + slice unlessRevID, + bool asFleece)=0; + + /// Pushes a new document revision to the server. + /// @param docID The document ID. + /// @param collectionID The name of the document's collection, or `nullslice` for default. + /// @param parentRevID The ID of the parent revision on the server, + /// or `nullslice` if this is a new document. + /// @param revisionFlags Flags of this revision. + /// @param fleeceData The document body encoded as Fleece (without shared keys!) + /// @return An async value that, when resolved, contains new revisionID or the status as a C4Error + virtual litecore::actor::Async putDoc(slice docID, + slice collectionID, + slice parentRevID, + C4RevisionFlags revisionFlags, + slice fleeceData)=0; /// Tells a connected client to start. - virtual void start() noexcept=0; + virtual void start()=0; /// Tells a replicator to stop. - virtual void stop() noexcept=0; + virtual void stop()=0; }; C4_ASSUME_NONNULL_END diff --git a/C/c4.def b/C/c4.def index 5cf99919d..a527a79fc 100644 --- a/C/c4.def +++ b/C/c4.def @@ -421,4 +421,5 @@ c4client_getDoc c4client_start c4client_stop c4client_free +c4client_putDoc diff --git a/C/c4.exp b/C/c4.exp index 51b5445b8..cf6f8ffed 100644 --- a/C/c4.exp +++ b/C/c4.exp @@ -419,6 +419,7 @@ _c4client_getDoc _c4client_start _c4client_stop _c4client_free +_c4client_putDoc # Apple specific _FLEncoder_WriteNSObject diff --git a/C/c4.gnu b/C/c4.gnu index 923a928ad..91b486035 100644 --- a/C/c4.gnu +++ b/C/c4.gnu @@ -419,6 +419,7 @@ CBL { c4client_start; c4client_stop; c4client_free; + c4client_putDoc; local: *; }; \ No newline at end of file diff --git a/C/c4_ee.def b/C/c4_ee.def index 242dae129..4a65b8d4c 100644 --- a/C/c4_ee.def +++ b/C/c4_ee.def @@ -460,6 +460,7 @@ c4client_getDoc c4client_start c4client_stop c4client_free +c4client_putDoc c4db_URINameFromPath @@ -475,9 +476,3 @@ c4keypair_privateKeyData c4keypair_publicKeyData c4keypair_publicKeyDigest -c4client_new -c4client_getDoc -c4client_start -c4client_stop -c4client_free - diff --git a/C/c4_ee.exp b/C/c4_ee.exp index 0a87506ef..da6a3e500 100644 --- a/C/c4_ee.exp +++ b/C/c4_ee.exp @@ -458,6 +458,7 @@ _c4client_getDoc _c4client_start _c4client_stop _c4client_free +_c4client_putDoc _c4db_URINameFromPath @@ -473,11 +474,5 @@ _c4keypair_privateKeyData _c4keypair_publicKeyData _c4keypair_publicKeyDigest -_c4client_new -_c4client_getDoc -_c4client_start -_c4client_stop -_c4client_free - # Apple specific _FLEncoder_WriteNSObject diff --git a/C/c4_ee.gnu b/C/c4_ee.gnu index 83d68a371..48f4454e0 100644 --- a/C/c4_ee.gnu +++ b/C/c4_ee.gnu @@ -458,6 +458,7 @@ CBL { c4client_start; c4client_stop; c4client_free; + c4client_putDoc; c4db_URINameFromPath; @@ -472,12 +473,6 @@ CBL { c4keypair_privateKeyData; c4keypair_publicKeyData; c4keypair_publicKeyDigest; - - c4client_new; - c4client_getDoc; - c4client_start; - c4client_stop; - c4client_free; local: *; }; \ No newline at end of file diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index ce737c469..4bfd0ca95 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -29,22 +29,19 @@ C4API_BEGIN_DECLS @param error Error will be written here if the function fails. @result A new \ref C4ConnectedClient, or NULL on failure. */ C4ConnectedClient* c4client_new(const C4ConnectedClientParameters* params, - C4Error* error) C4API; + C4Error* C4NULLABLE error) C4API; /** Gets the current revision of a document from the server. - You can set the `unlessRevID` parameter to avoid getting a redundant copy of a revision you already have. - @param docID The document ID. @param collectionID The name of the document's collection, or `nullslice` for default. @param unlessRevID If non-null, and equal to the current server-side revision ID, - the server will return error {WebSocketDomain, 304} instead of the document. + the server will return error {WebSocketDomain, 304}. @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. @param callback Callback for getting document. - @param context Client value passed to the callback - @param error On failure to issue the call, the error info will be stored here. - @return True if the request is sent, false if it failed (check `error`.) */ + @param context Client value passed to getDocument callback + @param outError On failure, the error info will be stored here. */ bool c4client_getDoc(C4ConnectedClient*, C4Slice docID, C4Slice collectionID, @@ -52,7 +49,27 @@ bool c4client_getDoc(C4ConnectedClient*, bool asFleece, C4ConnectedClientGetDocumentCallback callback, void * C4NULLABLE context, - C4Error* error) C4API; + C4Error* C4NULLABLE outError) C4API; + +/** Pushes a new document revision to the server. + @param docID The document ID. + @param collectionID The name of the document's collection, or `nullslice` for default. + @param revID The ID of the parent revision on the server, + or `nullslice` if this is a new document. + @param revisionFlags Flags of this revision. + @param fleeceData The document body encoded as Fleece (without shared keys!) + @param callback Callback once the document is updated. + @param context Client value passed to updateDocument callback + @param outError On failure, the error info will be stored here. */ +bool c4client_putDoc(C4ConnectedClient* client, + C4Slice docID, + C4Slice collectionID, + C4Slice revID, + C4RevisionFlags revisionFlags, + C4Slice fleeceData, + C4ConnectedClientUpdateDocumentCallback callback, + void * C4NULLABLE context, + C4Error* C4NULLABLE outError) C4API; /** Tells a connected client to start. \note This function is thread-safe.*/ diff --git a/C/include/c4ConnectedClientTypes.h b/C/include/c4ConnectedClientTypes.h index 8e698ab03..235c776d9 100644 --- a/C/include/c4ConnectedClientTypes.h +++ b/C/include/c4ConnectedClientTypes.h @@ -49,6 +49,14 @@ typedef void (*C4ConnectedClientGetDocumentCallback)(C4ConnectedClient* client, C4Error* C4NULLABLE err, void * C4NULLABLE context); +/** Callback for updating the document result. + @param client The client that initiated the callback. + @param err Error will be written here if the get-document fails. + @param context user-defined parameter given when registering the callback. */ +typedef void (*C4ConnectedClientUpdateDocumentCallback)(C4ConnectedClient* client, + C4HeapSlice revID, + C4Error* C4NULLABLE err, + void * C4NULLABLE context); /** @} */ C4API_END_DECLS diff --git a/C/scripts/c4.txt b/C/scripts/c4.txt index d7880e120..64747800d 100644 --- a/C/scripts/c4.txt +++ b/C/scripts/c4.txt @@ -427,3 +427,4 @@ c4client_getDoc c4client_start c4client_stop c4client_free +c4client_putDoc diff --git a/C/scripts/c4_ee.txt b/C/scripts/c4_ee.txt index 20f851f20..674186769 100644 --- a/C/scripts/c4_ee.txt +++ b/C/scripts/c4_ee.txt @@ -54,9 +54,3 @@ c4keypair_isPersistent c4keypair_privateKeyData c4keypair_publicKeyData c4keypair_publicKeyDigest - -c4client_new -c4client_getDoc -c4client_start -c4client_stop -c4client_free diff --git a/LiteCore/Database/DocumentFactory.hh b/LiteCore/Database/DocumentFactory.hh index 569019900..dd1269222 100644 --- a/LiteCore/Database/DocumentFactory.hh +++ b/LiteCore/Database/DocumentFactory.hh @@ -15,6 +15,7 @@ #include "c4DocumentTypes.h" #include "Record.hh" #include +#include "RevID.hh" C4_ASSUME_NONNULL_BEGIN @@ -40,7 +41,6 @@ namespace litecore { unsigned maxAncestors, bool mustHaveBodies, C4RemoteID remoteDBID) =0; - private: C4Collection* const _coll; // Unretained, to avoid ref-cycle }; diff --git a/LiteCore/Database/TreeDocument.cc b/LiteCore/Database/TreeDocument.cc index 6fe2bb3ec..ced4331ef 100644 --- a/LiteCore/Database/TreeDocument.cc +++ b/LiteCore/Database/TreeDocument.cc @@ -594,7 +594,9 @@ namespace litecore { if (!body) return false; - revidBuffer encodedNewRevID = generateDocRevID(body, _selected.revID, deletion); + revidBuffer encodedNewRevID = TreeDocumentFactory::generateDocRevID(body, + _selected.revID, + deletion); C4ErrorCode errorCode = {}; int httpStatus; @@ -645,24 +647,6 @@ namespace litecore { return true; } - - static revidBuffer generateDocRevID(slice body, slice parentRevID, bool deleted) { - // Get SHA-1 digest of (length-prefixed) parent rev ID, deletion flag, and revision body: - uint8_t revLen = (uint8_t)min((unsigned long)parentRevID.size, 255ul); - uint8_t delByte = deleted; - SHA1 digest = (SHA1Builder() << revLen << slice(parentRevID.buf, revLen) - << delByte << body) - .finish(); - // Derive new rev's generation #: - unsigned generation = 1; - if (parentRevID.buf) { - revidBuffer parentID(parentRevID); - generation = parentID.generation() + 1; - } - return revidBuffer(generation, slice(digest)); - } - - private: RevTreeRecord _revTree; const Rev *_selectedRev {nullptr}; @@ -764,5 +748,22 @@ namespace litecore { return asInternal(collection())->keyStore().withDocBodies(docIDs, callback); } + /*static*/ revidBuffer TreeDocumentFactory::generateDocRevID(slice body, + slice parentRevID, + bool deleted) { + // Get SHA-1 digest of (length-prefixed) parent rev ID, deletion flag, and revision body: + uint8_t revLen = (uint8_t)min((unsigned long)parentRevID.size, 255ul); + uint8_t delByte = deleted; + SHA1 digest = (SHA1Builder() << revLen << slice(parentRevID.buf, revLen) + << delByte << body) + .finish(); + // Derive new rev's generation #: + unsigned generation = 1; + if (parentRevID.buf) { + revidBuffer parentID(parentRevID); + generation = parentID.generation() + 1; + } + return revidBuffer(generation, slice(digest)); + } } // end namespace litecore diff --git a/LiteCore/Database/TreeDocument.hh b/LiteCore/Database/TreeDocument.hh index 4370f45ea..b5ed4f32c 100644 --- a/LiteCore/Database/TreeDocument.hh +++ b/LiteCore/Database/TreeDocument.hh @@ -31,6 +31,8 @@ namespace litecore { C4RemoteID remoteDBID) override; static C4Document* documentContaining(FLValue value); + + static revidBuffer generateDocRevID(slice body, slice parentRevID, bool deleted); }; } diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index bf0ec7b47..8f2886e61 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -17,11 +17,14 @@ #include "c4ConnectedClient.hh" #include "c4Socket+Internal.hh" #include "c4Internal.hh" +#include "RevTree.hh" +#include "TreeDocument.hh" namespace litecore::client { using namespace litecore::websocket; using namespace litecore::actor; + using namespace std; struct C4ConnectedClientImpl: public C4ConnectedClient, public ConnectedClient::Delegate { @@ -62,10 +65,10 @@ namespace litecore::client { } #pragma mark - - Async getDoc(C4Slice docID, - C4Slice collectionID, - C4Slice unlessRevID, - bool asFleece) noexcept override { + Async getDoc(slice docID, + slice collectionID, + slice unlessRevID, + bool asFleece) override { return _client->getDoc(docID, collectionID, unlessRevID, @@ -74,12 +77,37 @@ namespace litecore::client { }); } - virtual void start() noexcept override { + Async putDoc(slice docID, + slice collectionID, + slice parentRevisionID, + C4RevisionFlags flags, + slice fleeceData) override { + bool deletion = (flags & kRevDeleted) != 0; + revidBuffer generatedRev = TreeDocumentFactory::generateDocRevID(fleeceData, + parentRevisionID, + deletion); + auto provider = Async::makeProvider(); + _client->putDoc(docID, + collectionID, + revid(generatedRev).expanded(), + parentRevisionID, + flags, + fleeceData).then([=](Result i) { + if (i.ok()) { + auto revID = revid(generatedRev).expanded(); + provider->setResult(revID.asString()); + } else + provider->setError(i.error()); + }); + return provider->asyncValue(); + } + + virtual void start() override { LOCK(_mutex); _client->start(); } - virtual void stop() noexcept override { + virtual void stop() override { LOCK(_mutex); _client->stop(); } diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index 7aad850a7..e7543f4c9 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -17,6 +17,8 @@ #include "Async.hh" using namespace litecore::repl; +using namespace litecore; +using namespace fleece; C4ConnectedClient* c4client_new(const C4ConnectedClientParameters* params, C4Error *outError) noexcept { try { @@ -31,8 +33,8 @@ bool c4client_getDoc(C4ConnectedClient* client, C4Slice unlessRevID, bool asFleece, C4ConnectedClientGetDocumentCallback callback, - void *context, - C4Error* outError) noexcept { + void* C4NULLABLE context, + C4Error* C4NULLABLE outError) noexcept { try { auto res = client->getDoc(docID, collectionID, unlessRevID, asFleece); res.then([=](C4DocResponse response) { @@ -57,3 +59,25 @@ void c4client_free(C4ConnectedClient* client) noexcept { release(client); } +bool c4client_putDoc(C4ConnectedClient* client, + C4Slice docID, + C4Slice collectionID, + C4Slice revID, + C4RevisionFlags revisionFlags, + C4Slice fleeceData, + C4ConnectedClientUpdateDocumentCallback callback, + void* C4NULLABLE context, + C4Error* C4NULLABLE outError) noexcept { + try { + auto res = client->putDoc(docID, collectionID, revID, revisionFlags, fleeceData); + res.then([=](string result) { + callback(client, alloc_slice(result), nullptr, context); + }).onError([=](C4Error err) { + callback(client, FLHeapSlice(), &err, context); + }); + return true; + } catchError(outError); + + return false; +} + diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index dfac60a2b..59d9182bb 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -2029,6 +2029,7 @@ 2758DFD225F161CD007C7487 /* c4BlobStore.hh */, 274D181826165AFB0018D39C /* c4Certificate.hh */, 27229268260D171D00A3A41F /* c4Collection.hh */, + 1A5726AF27D7474900A9B412 /* c4ConnectedClient.hh */, 2764ED3623870F15007F020F /* c4Database.hh */, 2758DFE425F197B9007C7487 /* c4Document.hh */, 2758E09825F3026F007C7487 /* c4DocEnumerator.hh */, @@ -2037,7 +2038,6 @@ 272BA50A23F61591000EB6E8 /* c4Query.hh */, 2758E0A725F30A37007C7487 /* c4Replicator.hh */, 274D18862617B4300018D39C /* c4Socket.hh */, - 1A5726AF27D7474900A9B412 /* c4ConnectedClient.hh */, ); path = Cpp_include; sourceTree = ""; From f8f4d472c85c91365d1975bc2ec1f6460e924213 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 14 Apr 2022 18:02:14 -0700 Subject: [PATCH 60/78] Connected Client: experimental implementation of "query" --- C/include/c4ReplicatorTypes.h | 3 + Replicator/ConnectedClient/ConnectedClient.cc | 62 +++++++++++ Replicator/ConnectedClient/ConnectedClient.hh | 20 +++- Replicator/ConnectedClient/QueryServer.cc | 102 ++++++++++++++++++ Replicator/ConnectedClient/QueryServer.hh | 25 +++++ Replicator/Replicator.cc | 7 ++ Replicator/Replicator.hh | 2 + Replicator/ReplicatorOptions.hh | 4 + Replicator/tests/ConnectedClientTest.cc | 44 ++++++++ Xcode/LiteCore.xcodeproj/project.pbxproj | 8 +- docs/overview/ConnectedClientProtocol.md | 15 +++ 11 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 Replicator/ConnectedClient/QueryServer.cc create mode 100644 Replicator/ConnectedClient/QueryServer.hh diff --git a/C/include/c4ReplicatorTypes.h b/C/include/c4ReplicatorTypes.h index 4311241fe..79658d530 100644 --- a/C/include/c4ReplicatorTypes.h +++ b/C/include/c4ReplicatorTypes.h @@ -238,6 +238,9 @@ C4API_BEGIN_DECLS // BLIP options: #define kC4ReplicatorCompressionLevel "BLIPCompressionLevel" ///< Data compression level, 0..9 + // Queries + #define kC4ReplicatorOptionNamedQueries "queries" ///< Queries to serve (Dict name->N1QL) + // [1]: Auth dictionary keys: #define kC4ReplicatorAuthType "type" ///< Auth type; see [2] (string) #define kC4ReplicatorAuthUserName "username" ///< User name for basic auth (string) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 714eb4aa7..a5d08425e 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -522,4 +522,66 @@ namespace litecore::client { return valid; } + + void ConnectedClient::query(slice name, fleece::Dict parameters, QueryReceiver receiver) { + MessageBuilder req("query"); + req["name"] = name; + req.jsonBody().writeValue(parameters); + sendAsyncRequest(req) + .then([=](Retained response) { + logInfo("...query got response"); + C4Error err = responseError(response); + if (!err) { + if (!sendQueryRows(response, receiver)) + err = C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, + "Invalid query response"); + } + // Final call to receiver: + receiver(nullptr, err ? &err : nullptr); + + }).onError([=](C4Error err) { + logInfo("...query got error"); + receiver(nullptr, &err); + }); + //OPT: If we stream the response we can call the receiver function on results as they arrive. + } + + + bool ConnectedClient::sendQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { + Array rows = response->JSONBody().asArray(); + if (!rows) + return false; + for (Array::iterator i(rows); i; ++i) { + Array row = i->asArray(); + if (!row) + return false; + receiver(row, nullptr); + } + return true; + } + + + bool ConnectedClient::sendMultiLineQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { + slice body = response->body(); + while (!body.empty()) { + // Get next line of JSON, up to a newline: + slice rowData; + if (const void *nl = body.findByte('\n')) { + rowData = body.upTo(nl); + body.setStart(offsetby(nl, 1)); + } else { + rowData = body; + body = nullslice; + } + Doc rowDoc = Doc::fromJSON(rowData); + if (Array row = rowDoc.asArray()) { + // Pass row to receiver: + receiver(row, nullptr); + } else { + return false; + } + } + return true; + } + } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index b5d2363a8..cb665327f 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -11,6 +11,7 @@ #include "c4ConnectedClientTypes.h" #include "c4Observer.hh" #include "c4ReplicatorTypes.h" +#include "fleece/Fleece.hh" #include #include #include @@ -28,6 +29,11 @@ namespace litecore::client { using CollectionObserver = std::function const&)>; + /** A callback invoked for every row of a query result. + @param result An array of column values, or NULL if the query is complete or failed. + @param error Points to the error, else NULL. */ + using QueryReceiver = std::function; + /** A live connection to Sync Gateway (or a CBL peer) that can do interactive CRUD operations. No C4Database necessary! Its API is somewhat similar to `Replicator`. */ @@ -134,6 +140,17 @@ namespace litecore::client { actor::Async observeCollection(slice collectionID, CollectionObserver callback); + //---- Query + + /// Runs a query on the server and gets the results. + /// @param name The name by which the query has been registered on the server. + /// @param parameters A Dict mapping query parameter names to values. + /// @param receiver A callback that will be invoked for each row of the result, + /// and/or if there's an error. + void query(slice name, + fleece::Dict parameters, + QueryReceiver receiver); + // exposed for unit tests: websocket::WebSocket* webSocket() const {return connection().webSocket();} @@ -155,7 +172,8 @@ namespace litecore::client { bool validateDocAndRevID(slice docID, slice revID); alloc_slice processIncomingDoc(slice docID, alloc_slice body, bool asFleece); void processOutgoingDoc(slice docID, slice revID, slice fleeceData, fleece::JSONEncoder &enc); - + bool sendQueryRows(blip::MessageIn*, const QueryReceiver&); + bool sendMultiLineQueryRows(blip::MessageIn*, const QueryReceiver&); Delegate* _delegate; // Delegate whom I report progress/errors to C4ConnectedClientParameters _params; ActivityLevel _status; diff --git a/Replicator/ConnectedClient/QueryServer.cc b/Replicator/ConnectedClient/QueryServer.cc new file mode 100644 index 000000000..5f6b4e550 --- /dev/null +++ b/Replicator/ConnectedClient/QueryServer.cc @@ -0,0 +1,102 @@ +// +// QueryServer.cc +// +// Copyright © 2022 Couchbase. All rights reserved. +// +// 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. +// + +#include "QueryServer.hh" +#include "Replicator.hh" +#include "DBAccess.hh" +#include "MessageBuilder.hh" +#include "c4Query.hh" + +namespace litecore::repl { + + QueryServer::QueryServer(Replicator *replicator) + :Worker(replicator, "query") + { + registerHandler("query", &QueryServer::handleQuery); + } + + + C4Query* QueryServer::getQuery(const string &name) { + if (auto i = _queries.find(name); i != _queries.end()) + return i->second; + slice queryStr = _options->namedQueries()[name].asString(); + logInfo("Compiling query '%s' from %.*s", name.c_str(), FMTSLICE(queryStr)); + if (!queryStr) + return nullptr; + C4QueryLanguage language = (queryStr.hasPrefix("{") ? kC4JSONQuery : kC4N1QLQuery); + Retained query = _db->useLocked()->newQuery(language, queryStr); + _queries.insert({name, query}); + return query; + } + + + void QueryServer::handleQuery(Retained request) { + try { + string name(request->property("name")); + C4Query *query = getQuery(name); + if (!query) { + request->respondWithError(404, "No such query"); + return; + } + + if (!request->JSONBody().asDict()) { + request->respondWithError(400, "Missing query parameters"); + return; + } + + blip::MessageBuilder reply(request); + JSONEncoder &enc = reply.jsonBody(); + enc.beginArray(); + _db->useLocked([&](C4Database*) { + logInfo("Running named query '%s'", name.c_str()); + Stopwatch st; + // Run the query: + query->setParameters(request->body()); + auto e = query->run(); + + // Send the column names as the first row: + unsigned nCols = query->columnCount(); + enc.beginArray(); + for (unsigned i = 0; i < nCols; i++) { + enc.writeString(query->columnTitle(i)); + } + enc.endArray(); + //enc.writeRaw("\n"); + + // Now send the real rows: + while (e.next()) { + enc.beginArray(); + for (Array::iterator i(e.columns()); i; ++i) { + enc.writeValue(*i); + } + enc.endArray(); + //enc.writeRaw("\n"); + } + logInfo("...query took %.1f ms", st.elapsedMS()); + }); + enc.endArray(); + request->respond(reply); + + } catch (...) { + C4Error err = C4Error::fromCurrentException(); + WarnError("Exception while handling query: %s", err.description().c_str()); + request->respondWithError(c4ToBLIPError(err)); + } + } + +} diff --git a/Replicator/ConnectedClient/QueryServer.hh b/Replicator/ConnectedClient/QueryServer.hh new file mode 100644 index 000000000..65e63688b --- /dev/null +++ b/Replicator/ConnectedClient/QueryServer.hh @@ -0,0 +1,25 @@ +// +// QueryServer.hh +// +// Copyright © 2022 Couchbase. All rights reserved. +// + +#pragma once +#include "Worker.hh" +#include + +namespace litecore::repl { + + class QueryServer final : public Worker { + public: + QueryServer(Replicator *replicator NONNULL); + + C4Query* getQuery(const std::string &name); + + private: + void handleQuery(Retained request); + + std::unordered_map> _queries; + }; + +} diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index c509e8737..e24cb2b2a 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -15,6 +15,7 @@ #include "ReplicatorTuning.hh" #include "Pusher.hh" #include "Puller.hh" +#include "QueryServer.hh" #include "Checkpoint.hh" #include "DBAccess.hh" #include "Delimiter.hh" @@ -103,6 +104,10 @@ namespace litecore { namespace repl { registerHandler("getCheckpoint", &Replicator::handleGetCheckpoint); registerHandler("setCheckpoint", &Replicator::handleSetCheckpoint); + + if (!_options->namedQueries().empty()) { + _queryServer = new QueryServer(this); + } } @@ -188,6 +193,7 @@ namespace litecore { namespace repl { connection().terminate(); _pusher = nullptr; _puller = nullptr; + _queryServer = nullptr; } // CBL-1061: This used to be inside the connected(), but static analysis shows @@ -349,6 +355,7 @@ namespace litecore { namespace repl { DebugAssert(!connected()); // must already have gotten _onClose() delegate callback _pusher = nullptr; _puller = nullptr; + _queryServer = nullptr; _db->close(); Signpost::end(Signpost::replication, uintptr_t(this)); } diff --git a/Replicator/Replicator.hh b/Replicator/Replicator.hh index 9fb92e12f..9274cf23d 100644 --- a/Replicator/Replicator.hh +++ b/Replicator/Replicator.hh @@ -25,6 +25,7 @@ namespace litecore { namespace repl { class Pusher; class Puller; + class QueryServer; class ReplicatedRev; static const array kCompatProtocols = {{ @@ -176,6 +177,7 @@ namespace litecore { namespace repl { Delegate* _delegate; // Delegate whom I report progress/errors to Retained _pusher; // Object that manages outgoing revs Retained _puller; // Object that manages incoming revs + Retained _queryServer; // Object that runs query requests (listener) blip::Connection::State _connectionState; // Current BLIP connection state Status _pushStatus {}; // Current status of Pusher diff --git a/Replicator/ReplicatorOptions.hh b/Replicator/ReplicatorOptions.hh index 6a0764151..2657351c9 100644 --- a/Replicator/ReplicatorOptions.hh +++ b/Replicator/ReplicatorOptions.hh @@ -110,6 +110,10 @@ namespace litecore { namespace repl { return uniqueID ? uniqueID : remoteURL; } + fleece::Dict namedQueries() const { + return dictProperty(kC4ReplicatorOptionNamedQueries); + } + fleece::Array arrayProperty(const char *name) const { return properties[name].asArray(); } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 811ac0678..6b40dff0c 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -17,6 +17,7 @@ // #include "ConnectedClientTest.hh" +#include "fleece/Mutable.hh" #pragma mark - GET: @@ -425,3 +426,46 @@ TEST_CASE_METHOD(ConnectedClientEncryptedLoopbackTest, "putDoc encrypted", "[Con } #endif // COUCHBASE_ENTERPRISE + + +#pragma mark - QUERIES: + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "query from connected client", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + + MutableDict queries = fleece::MutableDict::newDict(); + queries["guysIn"] = "SELECT name.first, name.last FROM _ WHERE gender='male' and contact.address.state=$STATE"; + _serverOptions->setProperty(kC4ReplicatorOptionNamedQueries, queries); + + start(); + + mutex mut; + condition_variable cond; + + vector results; + + MutableDict params = fleece::MutableDict::newDict(); + params["STATE"] = "CA"; + _client->query("guysIn", params, [&](fleece::Array row, const C4Error *error) { + if (row) { + CHECK(!error); + Log("*** Got query row: %s", row.toJSONString().c_str()); + results.push_back(row.toJSONString()); + } else { + Log("*** Got final row"); + if (error) + results.push_back("Error: " + error->description()); + unique_lock lock(mut); + cond.notify_one(); + } + }); + + Log("Waiting for query..."); + unique_lock lock(mut); + cond.wait(lock); + Log("Query complete"); + vector expectedResults {R"(["first","last"])", + R"(["Cleveland","Bejcek"])", R"(["Rico","Hoopengardner"])"}; + CHECK(results == expectedResults); +} diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 59d9182bb..47c9c5403 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -454,6 +454,7 @@ 27F0426C2196264900D7C6FA /* SQLiteDataFile+Indexes.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27F0426B2196264900D7C6FA /* SQLiteDataFile+Indexes.cc */; }; 27F2BEA0221DF1A0006C13EE /* DBAccess.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27F2BE9F221DF1A0006C13EE /* DBAccess.cc */; }; 27F6F51D1BAA0482003FD798 /* c4Test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27F6F51B1BAA0482003FD798 /* c4Test.cc */; }; + 27F7177A2808E20E000CEA5B /* QueryServer.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27F717792808E20E000CEA5B /* QueryServer.cc */; }; 27F7A1351D61F7EB00447BC6 /* LiteCoreTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 2708FE5A1CF4D3370022F721 /* LiteCoreTest.cc */; }; 27F7A1431D61F8B700447BC6 /* c4Test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 27F6F51B1BAA0482003FD798 /* c4Test.cc */; }; 27F7A1441D61F8B700447BC6 /* c4DatabaseTest.cc in Sources */ = {isa = PBXBuildFile; fileRef = 274D04001BA75C0400FF7C35 /* c4DatabaseTest.cc */; }; @@ -1550,6 +1551,8 @@ 27F4F48323070C7F0075D7CB /* tls_context.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = tls_context.h; sourceTree = ""; }; 27F6F51B1BAA0482003FD798 /* c4Test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4Test.cc; sourceTree = ""; }; 27F6F51C1BAA0482003FD798 /* c4Test.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = c4Test.hh; sourceTree = ""; }; + 27F717782808E20E000CEA5B /* QueryServer.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = QueryServer.hh; sourceTree = ""; }; + 27F717792808E20E000CEA5B /* QueryServer.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = QueryServer.cc; sourceTree = ""; }; 27F7A0BD1D5E2BAB00447BC6 /* DatabaseImpl.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DatabaseImpl.hh; sourceTree = ""; }; 27FA09D31D70EDBF005888AA /* Catch_Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Catch_Tests.mm; sourceTree = ""; }; 27FA568324AD0E9300B2F1F8 /* Pusher+Attachments.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "Pusher+Attachments.cc"; sourceTree = ""; }; @@ -2327,8 +2330,10 @@ 2752C6B127BC417E001C1B76 /* ConnectedClient */ = { isa = PBXGroup; children = ( - 2752C6B527BC41DC001C1B76 /* ConnectedClient.hh */, 2752C6B627BC41DC001C1B76 /* ConnectedClient.cc */, + 2752C6B527BC41DC001C1B76 /* ConnectedClient.hh */, + 27F717792808E20E000CEA5B /* QueryServer.cc */, + 27F717782808E20E000CEA5B /* QueryServer.hh */, ); path = ConnectedClient; sourceTree = ""; @@ -4346,6 +4351,7 @@ 27D74A7E1D4D3F2300D806E0 /* Database.cpp in Sources */, 27ADA79B1F2BF64100D9DE25 /* UnicodeCollator.cc in Sources */, 27BF024B1FB62726003D5BB8 /* LibC++Debug.cc in Sources */, + 27F7177A2808E20E000CEA5B /* QueryServer.cc in Sources */, 2712F5AF25D5A9AB0082D526 /* c4Error.cc in Sources */, 93CD010E1E933BE100AFB3FA /* Puller.cc in Sources */, 274EDDF61DA30B43003AD158 /* QueryParser.cc in Sources */, diff --git a/docs/overview/ConnectedClientProtocol.md b/docs/overview/ConnectedClientProtocol.md index 1c55e70da..694f3a3bb 100644 --- a/docs/overview/ConnectedClientProtocol.md +++ b/docs/overview/ConnectedClientProtocol.md @@ -69,3 +69,18 @@ This terminates the effect of `subChanges`: the receiving peer MUST stop sending The sender MAY send another `subChanges` message later, to start a new feed. _(No request properties or body defined.)_ + +### 3.6 `query` + +Runs a query on the peer, identified by a name. Queries can take zero or more named parameters, each of which is a JSON value. + +The result of a query is a list of rows, each of which is an array of column values. Each row has the same number of columns. Each column has a name. + +Request: + +* `name`: The name of the query +* Body: A JSON object mapping parameter names to values + +Response: + +* Body: A JSON array. The first element MUST be an array of column names, each of which MUST be a string. The remaining elements are the rows of the query result. Each row MUST be an array of JSON values (columns.) Each row of the body, including the colum names, MUST have the same number of items. From 67ba28ef5aa54de2609e00648269ba6b058e7c24 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 20 Apr 2022 15:47:30 -0700 Subject: [PATCH 61/78] C4ConnectedClient API fixes - getDoc() method can't return C4DocResponse because that struct points to alloc_slices without owning them. Created a DocResponse struct whose members are actual alloc_slices. - Since C4ConnectedClient is RefCounted, the C retain and release functions can just be c4base_retain and c4base_release. - c4client_free is superseded by c4client_release. - Added retain/release wrappers in c4CppUtils.hh --- C/Cpp_include/c4ConnectedClient.hh | 19 +++++++++++++------ C/include/c4Base.h | 8 ++++++-- C/include/c4ConnectedClient.h | 5 ----- C/tests/c4CppUtils.hh | 2 ++ Replicator/c4ConnectedClientImpl.hh | 4 ++-- Replicator/c4ConnectedClient_CAPI.cc | 9 +++------ 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index 74eb48082..ba369e430 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -13,7 +13,7 @@ #pragma once #include "c4Base.hh" #include "Async.hh" -#include "c4ConnectedClient.h" +#include "c4ConnectedClientTypes.h" C4_ASSUME_NONNULL_BEGIN @@ -28,6 +28,13 @@ struct C4ConnectedClient : public fleece::RefCounted, /// @result A new \ref C4ConnectedClient, or NULL on failure. static Retained newClient(const C4ConnectedClientParameters ¶ms); + + /** Result of a successful `getDoc()` call. */ + struct DocResponse { + alloc_slice docID, revID, body; + bool deleted; + }; + /// Gets the current revision of a document from the server. /// You can set the `unlessRevID` parameter to avoid getting a redundant copy of a /// revision you already have. @@ -36,12 +43,12 @@ struct C4ConnectedClient : public fleece::RefCounted, /// @param unlessRevID If non-null, and equal to the current server-side revision ID, /// the server will return error {WebSocketDomain, 304}. /// @param asFleece If true, the response's `body` field is Fleece; if false, it's JSON. - /// @result An async value that, when resolved, contains either a `C4DocResponse` struct + /// @result An async value that, when resolved, contains either a `DocResponse` struct /// or a C4Error. - virtual litecore::actor::Async getDoc(slice docID, - slice collectionID, - slice unlessRevID, - bool asFleece)=0; + virtual litecore::actor::Async getDoc(slice docID, + slice collectionID, + slice unlessRevID, + bool asFleece)=0; /// Pushes a new document revision to the server. /// @param docID The document ID. diff --git a/C/include/c4Base.h b/C/include/c4Base.h index 07721f01f..6b0ff09d5 100644 --- a/C/include/c4Base.h +++ b/C/include/c4Base.h @@ -117,6 +117,9 @@ typedef struct C4Cert C4Cert; /** Opaque handle to a namespace of documents in an opened database. */ typedef struct C4Collection C4Collection; +/** Opaque reference to a Connected Client. */ +typedef struct C4ConnectedClient C4ConnectedClient; + /** Opaque handle to an opened database. */ typedef struct C4Database C4Database; @@ -169,8 +172,6 @@ typedef struct C4SocketFactory C4SocketFactory; /** An open stream for writing data to a blob. */ typedef struct C4WriteStream C4WriteStream; -/** Opaque reference to a Connected Client. */ -typedef struct C4ConnectedClient C4ConnectedClient; #pragma mark - REFERENCE COUNTING: @@ -182,6 +183,8 @@ void c4base_release(void * C4NULLABLE obj) C4API; // These types are reference counted and have c4xxx_retain / c4xxx_release functions: static inline C4Cert* C4NULLABLE c4cert_retain(C4Cert* C4NULLABLE r) C4API {return (C4Cert*)c4base_retain(r);} +static inline C4ConnectedClient* C4NULLABLE + c4client_retain(C4ConnectedClient* C4NULLABLE r) C4API {return (C4ConnectedClient*)c4base_retain(r);} static inline C4KeyPair* C4NULLABLE c4keypair_retain(C4KeyPair* C4NULLABLE r) C4API {return (C4KeyPair*)c4base_retain(r);} static inline C4Database* C4NULLABLE @@ -197,6 +200,7 @@ C4Socket* C4NULLABLE c4socket_retain(C4Socket* C4NULLABLE) C4API; static inline void c4cert_release (C4Cert* C4NULLABLE r) C4API {c4base_release(r);} +static inline void c4client_release (C4ConnectedClient* C4NULLABLE r) C4API {c4base_release(r);} static inline void c4keypair_release(C4KeyPair* C4NULLABLE r) C4API {c4base_release(r);} static inline void c4db_release (C4Database* C4NULLABLE r) C4API {c4base_release(r);} static inline void c4query_release (C4Query* C4NULLABLE r) C4API {c4base_release(r);} diff --git a/C/include/c4ConnectedClient.h b/C/include/c4ConnectedClient.h index 4bfd0ca95..781bbf0db 100644 --- a/C/include/c4ConnectedClient.h +++ b/C/include/c4ConnectedClient.h @@ -79,11 +79,6 @@ void c4client_start(C4ConnectedClient*) C4API; \note This function is thread-safe. */ void c4client_stop(C4ConnectedClient*) C4API; -/** Frees a connected client reference. - Does not stop the connected client -- if the client still has other internal references, - it will keep going. If you need the client to stop, call \ref c4client_stop first. */ -void c4client_free(C4ConnectedClient*) C4API; - /** @} */ C4API_END_DECLS diff --git a/C/tests/c4CppUtils.hh b/C/tests/c4CppUtils.hh index e65f46051..274896c84 100644 --- a/C/tests/c4CppUtils.hh +++ b/C/tests/c4CppUtils.hh @@ -30,6 +30,7 @@ namespace c4 { static inline void releaseRef(C4Cert* c) noexcept {c4cert_release(c);} static inline void releaseRef(C4Database* c) noexcept {c4db_release(c);} static inline void releaseRef(C4CollectionObserver* c)noexcept {c4dbobs_free(c);} + static inline void releaseRef(C4ConnectedClient* c) noexcept {c4client_release(c);} static inline void releaseRef(C4DocEnumerator* c) noexcept {c4enum_free(c);} static inline void releaseRef(C4Document* c) noexcept {c4doc_release(c);} static inline void releaseRef(C4DocumentObserver* c) noexcept {c4docobs_free(c);} @@ -45,6 +46,7 @@ namespace c4 { // The functions the ref<> template calls to retain a reference. (Not all types can be retained) static inline C4Cert* retainRef(C4Cert* c) noexcept {return c4cert_retain(c);} + static inline C4ConnectedClient* retainRef(C4ConnectedClient* c) noexcept {return c4client_retain(c);} static inline C4Database* retainRef(C4Database* c) noexcept {return c4db_retain(c);} static inline C4Document* retainRef(C4Document* c) noexcept {return c4doc_retain(c);} static inline C4KeyPair* retainRef(C4KeyPair* c) noexcept {return c4keypair_retain(c);} diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 8f2886e61..4fd15b5fa 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -65,14 +65,14 @@ namespace litecore::client { } #pragma mark - - Async getDoc(slice docID, + Async getDoc(slice docID, slice collectionID, slice unlessRevID, bool asFleece) override { return _client->getDoc(docID, collectionID, unlessRevID, - asFleece).then([](DocResponse a) -> C4DocResponse { + asFleece).then([](auto a) -> DocResponse { return { a.docID, a.revID, a.body, a.deleted }; }); } diff --git a/Replicator/c4ConnectedClient_CAPI.cc b/Replicator/c4ConnectedClient_CAPI.cc index e7543f4c9..1b98f3cfd 100644 --- a/Replicator/c4ConnectedClient_CAPI.cc +++ b/Replicator/c4ConnectedClient_CAPI.cc @@ -37,8 +37,9 @@ bool c4client_getDoc(C4ConnectedClient* client, C4Error* C4NULLABLE outError) noexcept { try { auto res = client->getDoc(docID, collectionID, unlessRevID, asFleece); - res.then([=](C4DocResponse response) { - return callback(client, &response, nullptr, context); + res.then([=](const C4ConnectedClient::DocResponse &r) { + C4DocResponse cResponse = {r.docID, r.revID, r.body, r.deleted}; + return callback(client, &cResponse, nullptr, context); }).onError([=](C4Error err) { return callback(client, nullptr, &err, context); }); @@ -55,10 +56,6 @@ void c4client_stop(C4ConnectedClient* client) noexcept { client->stop(); } -void c4client_free(C4ConnectedClient* client) noexcept { - release(client); -} - bool c4client_putDoc(C4ConnectedClient* client, C4Slice docID, C4Slice collectionID, From 920e034099191db2692ee052e19fce57a470b8a7 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 20 Apr 2022 15:50:31 -0700 Subject: [PATCH 62/78] Made C4ConnectedClient with with BuiltInWebSocket The way the WebSocket was created wasn't compatible with the built-in WebSocket implementation used by CBL-C and the cblite tool, so I changed the call from `new C4SocketImpl` to `CreateWebSocket`. However, this function required a C4Database pointer ... turns out that's only so the WebSocket can access cookies. I changed the code to allow a null pointer, in which case it just won't do any cookie stuff. This is a short-term fix; long-term we need some other way to persist cookies, I think. --- Networking/WebSockets/BuiltInWebSocket.cc | 7 +++++-- Replicator/c4ConnectedClientImpl.hh | 10 +++++----- Replicator/c4Socket+Internal.hh | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Networking/WebSockets/BuiltInWebSocket.cc b/Networking/WebSockets/BuiltInWebSocket.cc index 766fb8788..7e6d25420 100644 --- a/Networking/WebSockets/BuiltInWebSocket.cc +++ b/Networking/WebSockets/BuiltInWebSocket.cc @@ -322,7 +322,9 @@ namespace litecore { namespace websocket { alloc_slice BuiltInWebSocket::cookiesForRequest(const Address &addr) { - alloc_slice cookies(_database->getCookies(addr)); + alloc_slice cookies; + if (_database) + cookies = _database->getCookies(addr); slice cookiesOption = options()[kC4ReplicatorOptionCookies].asString(); if (cookiesOption) { @@ -340,7 +342,8 @@ namespace litecore { namespace websocket { void BuiltInWebSocket::setCookie(const Address &addr, slice cookieHeader) { - _database->setCookie(cookieHeader, addr.hostname, addr.path); + if (_database) + _database->setCookie(cookieHeader, addr.hostname, addr.path); } diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 4fd15b5fa..b8c2bbadb 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -35,11 +35,11 @@ namespace litecore::client { _customSocketFactory = *params.socketFactory; _socketFactory = &_customSocketFactory; } - - auto webSocket = new repl::C4SocketImpl(effectiveURL(params.url), - Role::Client, - socketOptions(), - _socketFactory); + + auto webSocket = repl::CreateWebSocket(effectiveURL(params.url), + socketOptions(), + nullptr, + _socketFactory); _client = new ConnectedClient(webSocket, *this, params); _client->start(); } diff --git a/Replicator/c4Socket+Internal.hh b/Replicator/c4Socket+Internal.hh index 5c1bd7646..a48770727 100644 --- a/Replicator/c4Socket+Internal.hh +++ b/Replicator/c4Socket+Internal.hh @@ -22,7 +22,7 @@ namespace litecore { namespace repl { // Main factory function to create a WebSocket. fleece::Retained CreateWebSocket(websocket::URL, fleece::alloc_slice options, - C4Database* NONNULL, + C4Database*, const C4SocketFactory*, void *nativeHandle =nullptr); From d531c461287935a908a1e8aab0b01c35b7f1d1f6 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 20 Apr 2022 16:51:10 -0700 Subject: [PATCH 63/78] Added C4ConnectedClient::getStatus --- C/Cpp_include/c4ConnectedClient.hh | 3 +- C/include/c4ConnectedClientTypes.h | 2 ++ Replicator/ConnectedClient/ConnectedClient.cc | 32 ++++++++++++++----- Replicator/ConnectedClient/ConnectedClient.hh | 8 +++-- Replicator/c4ConnectedClientImpl.hh | 4 +++ 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index ba369e430..ca656542d 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -27,7 +27,8 @@ struct C4ConnectedClient : public fleece::RefCounted, /// @param params Connected Client parameters. /// @result A new \ref C4ConnectedClient, or NULL on failure. static Retained newClient(const C4ConnectedClientParameters ¶ms); - + + virtual litecore::actor::Async getStatus() const =0; /** Result of a successful `getDoc()` call. */ struct DocResponse { diff --git a/C/include/c4ConnectedClientTypes.h b/C/include/c4ConnectedClientTypes.h index 235c776d9..e4df91673 100644 --- a/C/include/c4ConnectedClientTypes.h +++ b/C/include/c4ConnectedClientTypes.h @@ -39,6 +39,8 @@ typedef struct C4ConnectedClientParameters { } C4ConnectedClientParameters; +typedef C4ReplicatorStatus C4ConnectedClientStatus; + /** Callback for getting the document result. @param client The client that initiated the callback. @param doc Resulting document response, NULL on failure. diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 714eb4aa7..c0c6b45cd 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -54,15 +54,15 @@ namespace litecore::client { nullptr, nullptr, nullptr, "Client") ,_delegate(&delegate) ,_params(params) - ,_status(kC4Stopped) + ,_activityLevel(kC4Stopped) { _importance = 2; } void ConnectedClient::setStatus(ActivityLevel status) { - if (status != _status) { - _status = status; + if (status != _activityLevel) { + _activityLevel = status; LOCK(_mutex); if (_delegate) @@ -71,11 +71,16 @@ namespace litecore::client { } + Async ConnectedClient::status() { + return asCurrentActor([this]() {return C4ReplicatorStatus(Worker::status());}); + } + + void ConnectedClient::start() { + Assert(_activityLevel == kC4Stopped); + setStatus(kC4Connecting); asCurrentActor([=] { logInfo("Connecting..."); - Assert(_status == kC4Stopped); - setStatus(kC4Connecting); connection().start(); registerHandler("getAttachment", &ConnectedClient::handleGetAttachment); _selfRetain = this; // retain myself while the connection is open @@ -103,6 +108,11 @@ namespace litecore::client { } + ConnectedClient::ActivityLevel ConnectedClient::computeActivityLevel() const { + return _activityLevel; + } + + #pragma mark - BLIP DELEGATE: @@ -134,7 +144,7 @@ namespace litecore::client { void ConnectedClient::onConnect() { asCurrentActor([=] { logInfo("Connected!"); - if (_status != kC4Stopping) // skip this if stop() already called + if (_activityLevel != kC4Stopping) // skip this if stop() already called setStatus(kC4Idle); }); } @@ -145,7 +155,7 @@ namespace litecore::client { logInfo("Connection closed with %-s %d: \"%.*s\" (state=%d)", status.reasonName(), status.code, FMTSLICE(status.message), state); - bool closedByPeer = (_status != kC4Stopping); + bool closedByPeer = (_activityLevel != kC4Stopping); setStatus(kC4Stopped); _connectionClosed(); @@ -198,12 +208,18 @@ namespace litecore::client { C4Error error; if (!response) { // Disconnected! - error = status().error; + error = Worker::status().error; if (!error) error = C4Error::make(LiteCoreDomain, kC4ErrorIOError, "network connection lost"); // TODO: Use a better default error than the one above } else if (response->isError()) { error = blipToC4Error(response->getError()); + if (error.domain == WebSocketDomain) { + switch (error.code) { + case 404: error.domain = LiteCoreDomain; error.code = kC4ErrorNotFound; break; + case 409: error.domain = LiteCoreDomain; error.code = kC4ErrorConflict; break; + } + } } else { error = {}; } diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index b5d2363a8..e35966129 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -36,8 +36,9 @@ namespace litecore::client { { public: class Delegate; - using CloseStatus = blip::Connection::CloseStatus; + using CloseStatus = blip::Connection::CloseStatus; using ActivityLevel = C4ReplicatorActivityLevel; + using Status = C4ReplicatorStatus; ConnectedClient(websocket::WebSocket* NONNULL, Delegate&, @@ -80,6 +81,8 @@ namespace litecore::client { void terminate(); + actor::Async status(); + //---- CRUD! /// Gets the current revision of a document from the server. @@ -139,6 +142,7 @@ namespace litecore::client { protected: std::string loggingClassName() const override {return "Client";} + ActivityLevel computeActivityLevel() const override; void onHTTPResponse(int status, const websocket::Headers &headers) override; void onTLSCertificate(slice certData) override; void onConnect() override; @@ -158,7 +162,7 @@ namespace litecore::client { Delegate* _delegate; // Delegate whom I report progress/errors to C4ConnectedClientParameters _params; - ActivityLevel _status; + ActivityLevel _activityLevel; Retained _selfRetain; CollectionObserver _observer; mutable std::mutex _mutex; diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index b8c2bbadb..84b2a497e 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -43,6 +43,10 @@ namespace litecore::client { _client = new ConnectedClient(webSocket, *this, params); _client->start(); } + + Async getStatus() const override { + return _client->status(); + } protected: #pragma mark - ConnectedClient Delegate From c29a15154f9c1061e9961202a3c6e0b4b9228ebe Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 28 Apr 2022 15:25:34 -0700 Subject: [PATCH 64/78] WIP: allDocs for ConnectedClient --- C/Cpp_include/c4ConnectedClient.hh | 6 +++ Replicator/ConnectedClient/ConnectedClient.cc | 53 +++++++++++++++++-- Replicator/ConnectedClient/ConnectedClient.hh | 29 ++++++++-- Replicator/c4ConnectedClientImpl.hh | 7 +++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index ca656542d..962ee0d00 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -65,6 +65,12 @@ struct C4ConnectedClient : public fleece::RefCounted, C4RevisionFlags revisionFlags, slice fleeceData)=0; + using AllDocsReceiver = std::function& ids, const C4Error *err)>; + + virtual void getAllDocIDs(slice collectionID, + slice globPattern, + AllDocsReceiver callback) =0; + /// Tells a connected client to start. virtual void start()=0; diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index c19a58101..26080a169 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -429,6 +429,52 @@ namespace litecore::client { } + void ConnectedClient::getAllDocIDs(slice collectionID, + slice globPattern, + AllDocsReceiver receiver) + { + MessageBuilder req("allDocs"); + if (globPattern) + req["idPattern"] = globPattern; + sendAsyncRequest(req) + .then([=](Retained response) { + logInfo("...allDocs got response"); + C4Error err = responseError(response); + if (!err) { + if (!receiveAllDocs(response, receiver)) + err = C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, + "Invalid allDocs response"); + } + // Final call to receiver: + receiver({}, err ? &err : nullptr); + + }).onError([=](C4Error err) { + logInfo("...allDocs got error"); + receiver({}, &err); + }); + //OPT: If we stream the response we can call the receiver function on results as they arrive. + } + + + bool ConnectedClient::receiveAllDocs(blip::MessageIn *response, const AllDocsReceiver &receiver) { + Array body = response->JSONBody().asArray(); + if (!body) + return false; + if (body.empty()) + return true; + vector docIDs; + docIDs.reserve(body.count()); + for (Array::iterator i(body); i; ++i) { + slice docID = i->asString(); + if (!docID) + return false; + docIDs.push_back(docID); + } + receiver(docIDs, nullptr); + return true; + } + + Async ConnectedClient::observeCollection(slice collectionID_, CollectionObserver callback_) { @@ -548,7 +594,7 @@ namespace litecore::client { logInfo("...query got response"); C4Error err = responseError(response); if (!err) { - if (!sendQueryRows(response, receiver)) + if (!receiveQueryRows(response, receiver)) err = C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, "Invalid query response"); } @@ -563,7 +609,7 @@ namespace litecore::client { } - bool ConnectedClient::sendQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { + bool ConnectedClient::receiveQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { Array rows = response->JSONBody().asArray(); if (!rows) return false; @@ -577,7 +623,8 @@ namespace litecore::client { } - bool ConnectedClient::sendMultiLineQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { + // not currently used; keeping it in case we decide to change the response format to lines-of-JSON + bool ConnectedClient::receiveMultiLineQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { slice body = response->body(); while (!body.empty()) { // Get next line of JSON, up to a newline: diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index ab2ebbd7c..472924ccd 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -25,6 +25,12 @@ namespace litecore::client { }; + /** A callback invoked when one or more document IDs are received from a getAllDocIDs call. + @param ids A vector of document IDs. An empty vector indicates the result is complete. + @param err Points to the error, if any, else NULL. */ + using AllDocsReceiver = std::function& ids, const C4Error *err)>; + + /** A callback invoked when one or more documents change on the server. */ using CollectionObserver = std::function const&)>; @@ -32,7 +38,7 @@ namespace litecore::client { /** A callback invoked for every row of a query result. @param result An array of column values, or NULL if the query is complete or failed. @param error Points to the error, else NULL. */ - using QueryReceiver = std::function; + using QueryReceiver = std::function; /** A live connection to Sync Gateway (or a CBL peer) that can do interactive CRUD operations. No C4Database necessary! @@ -133,6 +139,21 @@ namespace litecore::client { C4RevisionFlags revisionFlags, slice fleeceData); + //---- All Documents + + /// Requests a list of all document IDs, optionally only those matching a pattern. + /// The async return value resolves once a response is received from the server. + /// The docIDs themselves are passed to a callback. + /// The callback will be called zero or more times with a non-empty vector of docIDs, + /// then once with an empty vector and an optional error. + /// @param collectionID The ID of the collection to observe. + /// @param globPattern Either `nullslice` or a glob-style pattern string (with `*` or + /// `?` wildcards) for docIDs to match. + /// @param callback The callback to receive the docIDs. + void getAllDocIDs(slice collectionID, + slice globPattern, + AllDocsReceiver callback); + //---- Observer /// Registers a listener function that will be called when any document is changed. @@ -176,8 +197,10 @@ namespace litecore::client { bool validateDocAndRevID(slice docID, slice revID); alloc_slice processIncomingDoc(slice docID, alloc_slice body, bool asFleece); void processOutgoingDoc(slice docID, slice revID, slice fleeceData, fleece::JSONEncoder &enc); - bool sendQueryRows(blip::MessageIn*, const QueryReceiver&); - bool sendMultiLineQueryRows(blip::MessageIn*, const QueryReceiver&); + bool receiveAllDocs(blip::MessageIn *, const AllDocsReceiver &); + bool receiveQueryRows(blip::MessageIn*, const QueryReceiver&); + bool receiveMultiLineQueryRows(blip::MessageIn*, const QueryReceiver&); + Delegate* _delegate; // Delegate whom I report progress/errors to C4ConnectedClientParameters _params; ActivityLevel _activityLevel; diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 84b2a497e..41c092a32 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -105,6 +105,13 @@ namespace litecore::client { }); return provider->asyncValue(); } + + void getAllDocIDs(slice collectionID, + slice globPattern, + AllDocsReceiver callback) override + { + _client->getAllDocIDs(collectionID, globPattern, callback); + } virtual void start() override { LOCK(_mutex); From c3f6d89e50e817b6f0b614993e3f818d418d5701 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 28 Apr 2022 15:58:50 -0700 Subject: [PATCH 65/78] WIP: Server side of ConnectedClient allDocs --- LiteCore/Support/StringUtil.cc | 24 +++++++++++++++++ LiteCore/Support/StringUtil.hh | 6 +++++ Replicator/Pusher.cc | 31 +++++++++++++++++++++- Replicator/Pusher.hh | 1 + Replicator/tests/ConnectedClientTest.cc | 35 +++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/LiteCore/Support/StringUtil.cc b/LiteCore/Support/StringUtil.cc index 6483f986e..357fbf509 100644 --- a/LiteCore/Support/StringUtil.cc +++ b/LiteCore/Support/StringUtil.cc @@ -16,6 +16,17 @@ #include #include +// For matchGlobPattern: +#ifdef _MSC_VER + #include + #include + #pragma comment(lib, "shlwapi.lib") + #undef min + #undef max +#else + #include // POSIX (?) +#endif + namespace litecore { using namespace std; @@ -140,6 +151,19 @@ namespace litecore { c = (char)tolower(c); } + + bool matchGlobPattern(const string &str, const string &pattern) { +#ifdef _MSC_VER + return PathMatchSpecA(name, pattern); +#else + return fnmatch(pattern.c_str(), str.c_str(), 0) == 0; +#endif + } + + +#pragma mark - UNICODE / UTF-8 FUNCTIONS + + // Based on utf8_check.c by Markus Kuhn, 2005 // https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c bool isValidUTF8(fleece::slice sl) noexcept diff --git a/LiteCore/Support/StringUtil.hh b/LiteCore/Support/StringUtil.hh index 8338fe99c..7756f25f7 100644 --- a/LiteCore/Support/StringUtil.hh +++ b/LiteCore/Support/StringUtil.hh @@ -95,6 +95,12 @@ namespace litecore { return str; } + /** Returns true if `str` matches the pattern `pattern`, which uses typical (Unix) shell + wildcard syntax: `?` matches a single character, `*` matches any number of characters, + `[...]` matches a specific character `\\` escapes the next character. + See for details.*/ + bool matchGlobPattern(const std::string &str, const std::string &pattern); + //////// UNICODE_AWARE FUNCTIONS: /** Returns true if the UTF-8 encoded slice contains no characters with code points < 32. */ diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index 07166e7b1..5b531e918 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -19,6 +19,7 @@ #include "StringUtil.hh" #include "BLIP.hh" #include "HTTPTypes.hh" +#include "c4DocEnumerator.hh" #include "c4ExceptionUtils.hh" #include @@ -52,8 +53,10 @@ namespace litecore { namespace repl { registerHandler("getAttachment", &Pusher::handleGetAttachment); registerHandler("proveAttachment", &Pusher::handleProveAttachment); - if (_options->properties[kC4ReplicatorOptionAllowConnectedClient]) + if (_options->properties[kC4ReplicatorOptionAllowConnectedClient]) { + registerHandler("allDocs", &Pusher::handleAllDocs); registerHandler("getRev", &Pusher::handleGetRev); + } } @@ -635,4 +638,30 @@ namespace litecore { namespace repl { } } + + // Connected Client `allDocs` request handler: + void Pusher::handleAllDocs(Retained req) { + optional pattern; + if (slice pat = req->property("idPattern")) + pattern = string(pat); + logInfo("Handling allDocs"); + + MessageBuilder response(req); + response.compressed = true; + auto &enc = response.jsonBody(); + enc.beginArray(); + + _db->useLocked([&](C4Database *db) { + C4DocEnumerator docEnum(db, {kC4Unsorted | kC4IncludeNonConflicted}); + while (docEnum.next()) { + C4DocumentInfo info = docEnum.documentInfo(); + if (!pattern || matchGlobPattern(string(info.docID), *pattern)) + enc.writeString(info.docID); + } + }); + + enc.endArray(); + req->respond(response); + } + } } diff --git a/Replicator/Pusher.hh b/Replicator/Pusher.hh index dcee576cc..fa7e9ad96 100644 --- a/Replicator/Pusher.hh +++ b/Replicator/Pusher.hh @@ -72,6 +72,7 @@ namespace litecore { namespace repl { bool shouldRetryConflictWithNewerAncestor(RevToSend* NONNULL, slice receivedRevID); void _docRemoteAncestorChanged(alloc_slice docID, alloc_slice remoteAncestorRevID); bool getForeignAncestors() const {return _proposeChanges || !_proposeChangesKnown;} + void handleAllDocs(Retained); // Pusher+Attachments.cc: void handleGetAttachment(Retained); diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 6b40dff0c..146887d05 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -282,6 +282,41 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "putDoc Blobs Legacy Mode", "[Conn } +#pragma mark - ALL-DOCS: + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "allDocs from connected client", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + start(); + + mutex mut; + condition_variable cond; + unique_lock lock(mut); + + vector results; + + _client->getAllDocIDs(nullslice, nullslice, [&](const vector &docIDs, const C4Error *error) { + unique_lock lock(mut); + if (!docIDs.empty()) { + Log("*** Got %zu docIDs", docIDs.size()); + CHECK(!error); + for (slice id : docIDs) + results.emplace_back(id); + } else { + Log("*** Got final row"); + if (error) + results.push_back("Error: " + error->description()); + cond.notify_one(); + } + }); + + Log("Waiting for docIDs..."); + cond.wait(lock); + Log("docIDs ready"); + CHECK(results.size() == 100); +} + + #pragma mark - ENCRYPTION: From 25fc306d73dc9c775bc43d7bd33ec2436def737a Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 28 Apr 2022 16:20:52 -0700 Subject: [PATCH 66/78] Connected Client: allDocs fixes --- Replicator/ConnectedClient/ConnectedClient.cc | 2 +- Replicator/Pusher.cc | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 26080a169..e0f5a9062 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -434,7 +434,7 @@ namespace litecore::client { AllDocsReceiver receiver) { MessageBuilder req("allDocs"); - if (globPattern) + if (!globPattern.empty()) req["idPattern"] = globPattern; sendAsyncRequest(req) .then([=](Retained response) { diff --git a/Replicator/Pusher.cc b/Replicator/Pusher.cc index 5b531e918..7b82834b4 100644 --- a/Replicator/Pusher.cc +++ b/Replicator/Pusher.cc @@ -641,10 +641,8 @@ namespace litecore { namespace repl { // Connected Client `allDocs` request handler: void Pusher::handleAllDocs(Retained req) { - optional pattern; - if (slice pat = req->property("idPattern")) - pattern = string(pat); - logInfo("Handling allDocs"); + string pattern( req->property("idPattern") ); + logInfo("Handling allDocs; pattern=`%s`", pattern.c_str()); MessageBuilder response(req); response.compressed = true; @@ -655,7 +653,7 @@ namespace litecore { namespace repl { C4DocEnumerator docEnum(db, {kC4Unsorted | kC4IncludeNonConflicted}); while (docEnum.next()) { C4DocumentInfo info = docEnum.documentInfo(); - if (!pattern || matchGlobPattern(string(info.docID), *pattern)) + if (pattern.empty() || matchGlobPattern(string(info.docID), pattern)) enc.writeString(info.docID); } }); From 91e10e89885e3e12298dfb7e6da0cb4ba67b10ff Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 2 May 2022 15:39:16 -0700 Subject: [PATCH 67/78] ConnectedClient: query improvements * Support for sending full query strings, if the server allows it. * Added C4ConnectedClient::query() * Added replicator constant kC4ReplicatorOptionAllQueries * Updated the protocol documentation. --- C/Cpp_include/c4ConnectedClient.hh | 34 ++++++++++- C/include/c4ReplicatorTypes.h | 3 +- Replicator/ConnectedClient/ConnectedClient.cc | 12 +++- Replicator/ConnectedClient/ConnectedClient.hh | 6 +- Replicator/ConnectedClient/QueryServer.cc | 57 +++++++++++++++---- Replicator/ConnectedClient/QueryServer.hh | 5 +- Replicator/ReplicatorOptions.hh | 4 ++ Replicator/c4ConnectedClientImpl.hh | 11 ++-- docs/overview/ConnectedClientProtocol.md | 50 ++++++++++++---- 9 files changed, 147 insertions(+), 35 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index 962ee0d00..ceb149ace 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -14,6 +14,7 @@ #include "c4Base.hh" #include "Async.hh" #include "c4ConnectedClientTypes.h" +#include "fleece/Fleece.h" C4_ASSUME_NONNULL_BEGIN @@ -65,12 +66,43 @@ struct C4ConnectedClient : public fleece::RefCounted, C4RevisionFlags revisionFlags, slice fleeceData)=0; - using AllDocsReceiver = std::function& ids, const C4Error *err)>; + /// Callback for \ref getAllDocIDs. + /// @param ids A vector of docIDs; empty on the final call. + /// @param error NULL or a pointer to an error. + using AllDocsReceiver = std::function& ids, + const C4Error* C4NULLABLE error)>; + /// Requests a list of all document IDs, or optionally only those matching a pattern. + /// The docIDs themselves are passed to a callback. + /// The callback will be called zero or more times with a non-empty vector of docIDs, + /// then once with an empty vector and an optional error. + /// @param collectionID The ID of the collection to observe. + /// @param globPattern Either `nullslice` or a glob-style pattern string (with `*` or + /// `?` wildcards) for docIDs to match. + /// @param callback The callback to receive the docIDs. virtual void getAllDocIDs(slice collectionID, slice globPattern, AllDocsReceiver callback) =0; + /// Callback for \ref query. + /// @param row On the first call, an array of colum names. + /// On subsequent calls, a result row as an array of column values. + /// On the final call, `nullptr`. + /// @param error NULL or, on the final call, a pointer to an error. + using QueryReceiver = std::function; + + /// Runs a query on the server and gets the results. + /// The callback will be called one or more times; see its documentation for details. + /// @param name The name by which the query has been registered on the server; + /// or a full query string beginning with "SELECT " or "{". + /// @param parameters A Dict mapping query parameter names to values. + /// @param receiver A callback that will be invoked for each row of the result, + /// and/or if there's an error. + virtual void query(slice name, + FLDict C4NULLABLE parameters, + QueryReceiver receiver) =0; + /// Tells a connected client to start. virtual void start()=0; diff --git a/C/include/c4ReplicatorTypes.h b/C/include/c4ReplicatorTypes.h index 79658d530..acf8e6db0 100644 --- a/C/include/c4ReplicatorTypes.h +++ b/C/include/c4ReplicatorTypes.h @@ -239,7 +239,8 @@ C4API_BEGIN_DECLS #define kC4ReplicatorCompressionLevel "BLIPCompressionLevel" ///< Data compression level, 0..9 // Queries - #define kC4ReplicatorOptionNamedQueries "queries" ///< Queries to serve (Dict name->N1QL) + #define kC4ReplicatorOptionNamedQueries "queries" ///< Queries to serve (Dict name->N1QL) + #define kC4ReplicatorOptionAllQueries "allQueries" ///< Allow any N1QL query? (bool) // [1]: Auth dictionary keys: #define kC4ReplicatorAuthType "type" ///< Auth type; see [2] (string) diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index e0f5a9062..730964d2e 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -587,8 +587,16 @@ namespace litecore::client { void ConnectedClient::query(slice name, fleece::Dict parameters, QueryReceiver receiver) { MessageBuilder req("query"); - req["name"] = name; - req.jsonBody().writeValue(parameters); + if (name.hasPrefix("SELECT ") || name.hasPrefix("select ") || name.hasPrefix("{")) + req["src"] = name; + else + req["name"] = name; + if (parameters) { + req.jsonBody().writeValue(parameters); + } else { + req.jsonBody().beginDict(); + req.jsonBody().endDict(); + } sendAsyncRequest(req) .then([=](Retained response) { logInfo("...query got response"); diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 472924ccd..84c0adea2 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -141,8 +141,7 @@ namespace litecore::client { //---- All Documents - /// Requests a list of all document IDs, optionally only those matching a pattern. - /// The async return value resolves once a response is received from the server. + /// Requests a list of all document IDs, or optionally only those matching a pattern. /// The docIDs themselves are passed to a callback. /// The callback will be called zero or more times with a non-empty vector of docIDs, /// then once with an empty vector and an optional error. @@ -167,7 +166,8 @@ namespace litecore::client { //---- Query /// Runs a query on the server and gets the results. - /// @param name The name by which the query has been registered on the server. + /// @param name The name by which the query has been registered on the server; + /// or a full query string beginning with "SELECT " or "{". /// @param parameters A Dict mapping query parameter names to values. /// @param receiver A callback that will be invoked for each row of the result, /// and/or if there's an error. diff --git a/Replicator/ConnectedClient/QueryServer.cc b/Replicator/ConnectedClient/QueryServer.cc index 5f6b4e550..1c4fcca59 100644 --- a/Replicator/ConnectedClient/QueryServer.cc +++ b/Replicator/ConnectedClient/QueryServer.cc @@ -20,6 +20,7 @@ #include "Replicator.hh" #include "DBAccess.hh" #include "MessageBuilder.hh" +#include "StringUtil.hh" #include "c4Query.hh" namespace litecore::repl { @@ -31,39 +32,73 @@ namespace litecore::repl { } - C4Query* QueryServer::getQuery(const string &name) { + static bool isJSONQuery(slice queryStr) { + return queryStr.hasPrefix("{"); + } + + + Retained QueryServer::compileQuery(slice queryStr) { + C4QueryLanguage language = (isJSONQuery(queryStr) ? kC4JSONQuery : kC4N1QLQuery); + return _db->useLocked()->newQuery(language, queryStr); + } + + + C4Query* QueryServer::getNamedQuery(const string &name) { if (auto i = _queries.find(name); i != _queries.end()) return i->second; slice queryStr = _options->namedQueries()[name].asString(); - logInfo("Compiling query '%s' from %.*s", name.c_str(), FMTSLICE(queryStr)); if (!queryStr) return nullptr; - C4QueryLanguage language = (queryStr.hasPrefix("{") ? kC4JSONQuery : kC4N1QLQuery); - Retained query = _db->useLocked()->newQuery(language, queryStr); - _queries.insert({name, query}); + logInfo("Compiling query '%s' from %.*s", name.c_str(), FMTSLICE(queryStr)); + Retained query = compileQuery(queryStr); + if (query) + _queries.insert({name, query}); return query; } void QueryServer::handleQuery(Retained request) { try { - string name(request->property("name")); - C4Query *query = getQuery(name); - if (!query) { - request->respondWithError(404, "No such query"); + Retained query; + slice name = request->property("name"); + slice src = request->property("src"); + if (!name == !src) { + request->respondWithError(blip::Error("HTTP", 400, + "Exactly one of 'name' or 'src' must be given")); return; } + if (name) { + // Named query: + query = getNamedQuery(string(name)); + if (!query) { + request->respondWithError(blip::Error("HTTP", 404, "No such query")); + return; + } + logInfo("Running named query '%.*s'", FMTSLICE(name)); + } else { + if (!_options->allQueries()) { + request->respondWithError(blip::Error("HTTP", 403, + "Arbitrary queries are not allowed")); + return; + } + logInfo("Compiling requested query: %.*s", FMTSLICE(src)); + query = compileQuery(src); + if (!query) { + request->respondWithError(blip::Error("HTTP", 400, "Syntax error in query")); + return; + } + } if (!request->JSONBody().asDict()) { - request->respondWithError(400, "Missing query parameters"); + request->respondWithError(blip::Error("HTTP", 400, "Invalid query parameter dict")); return; } + // Now run the query: blip::MessageBuilder reply(request); JSONEncoder &enc = reply.jsonBody(); enc.beginArray(); _db->useLocked([&](C4Database*) { - logInfo("Running named query '%s'", name.c_str()); Stopwatch st; // Run the query: query->setParameters(request->body()); diff --git a/Replicator/ConnectedClient/QueryServer.hh b/Replicator/ConnectedClient/QueryServer.hh index 65e63688b..047ae0335 100644 --- a/Replicator/ConnectedClient/QueryServer.hh +++ b/Replicator/ConnectedClient/QueryServer.hh @@ -14,8 +14,9 @@ namespace litecore::repl { public: QueryServer(Replicator *replicator NONNULL); - C4Query* getQuery(const std::string &name); - + C4Query* getNamedQuery(const std::string &name); + Retained compileQuery(slice queryStr); + private: void handleQuery(Retained request); diff --git a/Replicator/ReplicatorOptions.hh b/Replicator/ReplicatorOptions.hh index 60cfb9eda..09e37c521 100644 --- a/Replicator/ReplicatorOptions.hh +++ b/Replicator/ReplicatorOptions.hh @@ -115,6 +115,10 @@ namespace litecore { namespace repl { return dictProperty(kC4ReplicatorOptionNamedQueries); } + bool allQueries() const { + return boolProperty(kC4ReplicatorOptionAllQueries); + } + fleece::Array arrayProperty(const char *name) const { return properties[name].asArray(); } diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 41c092a32..2aff393bb 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -106,13 +106,14 @@ namespace litecore::client { return provider->asyncValue(); } - void getAllDocIDs(slice collectionID, - slice globPattern, - AllDocsReceiver callback) override - { - _client->getAllDocIDs(collectionID, globPattern, callback); + void getAllDocIDs(slice collectionID, slice pattern, AllDocsReceiver callback) override { + _client->getAllDocIDs(collectionID, pattern, callback); } + void query(slice name, FLDict C4NULLABLE parameters, QueryReceiver receiver) override { + _client->query(name, parameters, receiver); + } + virtual void start() override { LOCK(_mutex); _client->start(); diff --git a/docs/overview/ConnectedClientProtocol.md b/docs/overview/ConnectedClientProtocol.md index 694f3a3bb..fe11248e6 100644 --- a/docs/overview/ConnectedClientProtocol.md +++ b/docs/overview/ConnectedClientProtocol.md @@ -26,12 +26,12 @@ Requests a document’s current revision from the peer. > **Note**: This is very much like the replicator protocol’s `rev` message, except that it’s sent as a *request* for a revision, not an announcement of one. -Request: +**Request**: * `id`: Document ID * `ifNotRev`: (optional) If present, and its value is equal to the document’s current revision ID, the peer SHOULD respond with a HTTP/304 error instead of sending the revision -Response: +**Response**: * `rev`: The current revision ID * Body: The current revision body as JSON @@ -46,9 +46,9 @@ Uploads a new revision to the peer. The properties and behavior are identical to the replicator protocol’s `rev` message. The reason for a new message type is because the LiteCore replicator assumes that incoming `rev` messages are caused by prior `changes` responses, and would become confused if it received a standalone `rev` message. This made it apparent that it would be cleaner to treat this as a separate type of request. -Request: *same as existing `rev` message* +**Request**: *same as existing `rev` message* -Response: *same as existing `rev` message* (never sent no-reply) +**Response**: *same as existing `rev` message* (never sent no-reply) > **Note:** As usual, the receiving peer may send one or more getAttachment requests back to the originator if it needs the contents of any blobs/attachments in the revision. @@ -56,7 +56,7 @@ Response: *same as existing `rev` message* (never sent no-reply) As in the replicator protocol, with one addition: -Request: +**Request**: * `future`: (Optional) If `true`, the receiving peer MUST not send any existing sequences, only future changes. In other words, this has the same effect as a `since` property whose value is the current sequence ID. (The message SHOULD NOT also contain a `since` property, and the recipient MUST ignore it if present.) @@ -72,15 +72,45 @@ _(No request properties or body defined.)_ ### 3.6 `query` -Runs a query on the peer, identified by a name. Queries can take zero or more named parameters, each of which is a JSON value. +Runs a query on the peer, identified by a name. Queries take zero or more named parameters, each of which is a JSON value. + +Optionally, a server MAY allow a client to run arbitrary queries. (Sync Gateway will not support this for security and performance reasons, but Couchbase Lite applications can choose to.) The result of a query is a list of rows, each of which is an array of column values. Each row has the same number of columns. Each column has a name. -Request: +**Request**: -* `name`: The name of the query +* `name`: The name of the query (if named) +* `src`: N1QL or JSON query source (if not named) * Body: A JSON object mapping parameter names to values -Response: +**Response**: + +* Body: A JSON array: + * The array MAY be empty if there were no query results. + * Otherwise its first item MUST be an array of column names, each of which MUST be a string. + * The remaining items are the rows of the query result. Each row MUST be an array with the same number of elements as the first item. + +**Errors**: + +- HTTP, 400 — Missing `name` or `src`, or both were given, or the body is not a JSON object, or a N1QL syntax error. +- HTTP, 403 — Arbitrary queries are not allowed +- HTTP, 404 — Query name is not registered + +### 3.7 `allDocs` + +Requests the IDs of all documents, or those matching a pattern. + +Deleted documents are ignored. + +**Request:** + +- `idPattern`: (optional) A pattern for the returned docIDs to match. Uses Unix shell “glob” syntax, with `?` matching a single character, `*` matching any number of characters, and `\` escaping the next character. + +**Response**: + +- Body: A JSON array of docIDs. Each item is a string. The order of the docIDs is arbitrary. + +**Errors:** -* Body: A JSON array. The first element MUST be an array of column names, each of which MUST be a string. The remaining elements are the rows of the query result. Each row MUST be an array of JSON values (columns.) Each row of the body, including the colum names, MUST have the same number of items. +- HTTP, 400 — If patterns are not supported. From a36a68dd4fd60beacff3400dc017b0c5d982d59d Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 2 May 2022 16:14:37 -0700 Subject: [PATCH 68/78] ConnectedClient: Added a query unit test, fixed a bug. --- Replicator/Replicator.cc | 2 +- Replicator/tests/ConnectedClientTest.cc | 44 +++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index e24cb2b2a..92ea11ee5 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -105,7 +105,7 @@ namespace litecore { namespace repl { registerHandler("getCheckpoint", &Replicator::handleGetCheckpoint); registerHandler("setCheckpoint", &Replicator::handleSetCheckpoint); - if (!_options->namedQueries().empty()) { + if (!_options->namedQueries().empty() || _options->allQueries()) { _queryServer = new QueryServer(this); } } diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index 146887d05..d51e3e451 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -466,11 +466,13 @@ TEST_CASE_METHOD(ConnectedClientEncryptedLoopbackTest, "putDoc encrypted", "[Con #pragma mark - QUERIES: -TEST_CASE_METHOD(ConnectedClientLoopbackTest, "query from connected client", "[ConnectedClient]") { +static constexpr slice kQueryStr = "SELECT name.first, name.last FROM _ WHERE gender='male' and contact.address.state=$STATE"; + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "named query from connected client", "[ConnectedClient]") { importJSONLines(sFixturesDir + "names_100.json"); MutableDict queries = fleece::MutableDict::newDict(); - queries["guysIn"] = "SELECT name.first, name.last FROM _ WHERE gender='male' and contact.address.state=$STATE"; + queries["guysIn"] = kQueryStr; _serverOptions->setProperty(kC4ReplicatorOptionNamedQueries, queries); start(); @@ -504,3 +506,41 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "query from connected client", "[C R"(["Cleveland","Bejcek"])", R"(["Rico","Hoopengardner"])"}; CHECK(results == expectedResults); } + + +TEST_CASE_METHOD(ConnectedClientLoopbackTest, "n1ql query from connected client", "[ConnectedClient]") { + importJSONLines(sFixturesDir + "names_100.json"); + + _serverOptions->setProperty(kC4ReplicatorOptionAllQueries, true); + + start(); + + mutex mut; + condition_variable cond; + + vector results; + + MutableDict params = fleece::MutableDict::newDict(); + params["STATE"] = "CA"; + _client->query(kQueryStr, params, [&](fleece::Array row, const C4Error *error) { + if (row) { + CHECK(!error); + Log("*** Got query row: %s", row.toJSONString().c_str()); + results.push_back(row.toJSONString()); + } else { + Log("*** Got final row"); + if (error) + results.push_back("Error: " + error->description()); + unique_lock lock(mut); + cond.notify_one(); + } + }); + + Log("Waiting for query..."); + unique_lock lock(mut); + cond.wait(lock); + Log("Query complete"); + vector expectedResults {R"(["first","last"])", + R"(["Cleveland","Bejcek"])", R"(["Rico","Hoopengardner"])"}; + CHECK(results == expectedResults); +} From df838078df0d22775e61c8636c2e801a8591b99a Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Mon, 2 May 2022 16:16:11 -0700 Subject: [PATCH 69/78] ConnectedClient: Added listener options [API] --- C/include/c4ListenerTypes.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/C/include/c4ListenerTypes.h b/C/include/c4ListenerTypes.h index 292d234ff..3cd9c6102 100644 --- a/C/include/c4ListenerTypes.h +++ b/C/include/c4ListenerTypes.h @@ -12,6 +12,7 @@ #pragma once #include "c4Base.h" +#include "fleece/Fleece.h" C4_ASSUME_NONNULL_BEGIN C4API_BEGIN_DECLS @@ -84,6 +85,10 @@ typedef struct C4ListenerConfig { bool allowPush; ///< Allow peers to push changes to local db bool allowPull; ///< Allow peers to pull changes from local db bool enableDeltaSync; ///< Enable document-deltas optimization + + bool allowConnectedClient; ///< Allow peers to use Connected Client API + FLDict namedQueries; ///< Maps query names to N1QL or JSON source + bool allowArbitraryQueries; ///< If true, client can run arbitrary queries } C4ListenerConfig; From 0979404676bc275baefca06b090665226c91a866 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 4 May 2022 15:10:49 -0700 Subject: [PATCH 70/78] blip::IncomingMessage now uses fleece::Doc This allows Fleece values in message bodies to be retained and copied, and eliminates a use of ValueFromData. --- Networking/BLIP/Message.cc | 4 ++-- Networking/BLIP/Message.hh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Networking/BLIP/Message.cc b/Networking/BLIP/Message.cc index 5363ecb0e..f201753a8 100644 --- a/Networking/BLIP/Message.cc +++ b/Networking/BLIP/Message.cc @@ -327,11 +327,11 @@ namespace litecore { namespace blip { return nullptr; } - _bodyAsFleece = FLData_ConvertJSON({_body.buf, _body.size}, nullptr); + _bodyAsFleece = Doc::fromJSON({_body.buf, _body.size}); if (!_bodyAsFleece && _body != "null"_sl) Warn("MessageIn::JSONBody: Body does not contain valid JSON: %.*s", SPLAT(_body)); } - return fleece::ValueFromData(_bodyAsFleece); + return _bodyAsFleece.root(); } diff --git a/Networking/BLIP/Message.hh b/Networking/BLIP/Message.hh index b9a3b6401..801707516 100644 --- a/Networking/BLIP/Message.hh +++ b/Networking/BLIP/Message.hh @@ -214,7 +214,7 @@ namespace litecore { namespace blip { uint32_t _unackedBytes {0}; // # bytes received that haven't been ACKed yet alloc_slice _properties; // Just the (still encoded) properties alloc_slice _body; // Just the body - alloc_slice _bodyAsFleece; // Body re-encoded into Fleece [lazy] + fleece::Doc _bodyAsFleece; // Body re-encoded into Fleece [lazy] const MessageSize _outgoingSize {0}; bool _complete {false}; bool _responded {false}; From fd5b451969fa681753c4e014b9e2d4e3ba22edbf Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 4 May 2022 15:18:45 -0700 Subject: [PATCH 71/78] Fixed CMake build --- LiteCore/tests/CMakeLists.txt | 1 + cmake/platform_base.cmake | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/LiteCore/tests/CMakeLists.txt b/LiteCore/tests/CMakeLists.txt index af9c3fc56..8a4c9c8de 100644 --- a/LiteCore/tests/CMakeLists.txt +++ b/LiteCore/tests/CMakeLists.txt @@ -82,6 +82,7 @@ add_executable( ${TOP}vendor/fleece/Tests/SupportTests.cc ${TOP}vendor/fleece/Tests/ValueTests.cc ${TOP}vendor/fleece/Experimental/KeyTree.cc + ${TOP}Replicator/tests/ConnectedClientTest.cc ${TOP}Replicator/tests/DBAccessTestWrapper.cc ${TOP}Replicator/tests/PropertyEncryptionTests.cc ${TOP}Replicator/tests/ReplicatorLoopbackTest.cc diff --git a/cmake/platform_base.cmake b/cmake/platform_base.cmake index 968c8c8e8..8913a3205 100644 --- a/cmake/platform_base.cmake +++ b/cmake/platform_base.cmake @@ -109,10 +109,11 @@ function(set_litecore_source_base) Replicator/RevFinder.cc Replicator/URLTransformer.cc Replicator/Worker.cc - Replicator/c4ConnectedClient_CAPI.cc - Replicator/c4ConnectedClient.cc - Replicator/c4ConnectedClientImpl.cc - Replicator/ConnectedClient/ConnectedClient.cc + Replicator/c4ConnectedClient_CAPI.cc + Replicator/c4ConnectedClient.cc + Replicator/c4ConnectedClientImpl.cc + Replicator/ConnectedClient/ConnectedClient.cc + Replicator/ConnectedClient/QueryServer.cc LiteCore/Support/Logging.cc LiteCore/Support/DefaultLogger.cc LiteCore/Support/Error.cc From b0bd654f2491dc90826e20c14c4ce554e8afc3b2 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 4 May 2022 16:00:50 -0700 Subject: [PATCH 72/78] Bumped LITECORE_VERSION and LITECORE_API_VERSION --- C/include/c4Base.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/C/include/c4Base.h b/C/include/c4Base.h index fb833a06c..feecc6c71 100644 --- a/C/include/c4Base.h +++ b/C/include/c4Base.h @@ -27,10 +27,10 @@ C4API_BEGIN_DECLS // Corresponds to Couchbase Lite product version number, with 2 digits for minor and patch versions. // i.e. `10000 * MajorVersion + 100 * MinorVersion + PatchVersion` -#define LITECORE_VERSION 30000 +#define LITECORE_VERSION 30100 // This number has no absolute meaning but is bumped whenever the LiteCore public API changes. -#define LITECORE_API_VERSION 351 +#define LITECORE_API_VERSION 352 /** \defgroup Base Data Types and Base Functions From 8682cbfb5c328e4a4727f7249a9a5a11878170bd Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 19 May 2022 17:00:56 -0700 Subject: [PATCH 73/78] ConnectedClient: Changed query result data format In the BLIP protocol, query results are now a series of JSON objects separated by newlines. In the LiteCore API, rows are now given to the callback as Dict, not Array, and the JSON value is given too. The Fleece form is optional. --- C/Cpp_include/c4ConnectedClient.hh | 11 +-- Replicator/ConnectedClient/ConnectedClient.cc | 69 ++++++++++--------- Replicator/ConnectedClient/ConnectedClient.hh | 12 ++-- Replicator/ConnectedClient/QueryServer.cc | 21 ++---- Replicator/c4ConnectedClientImpl.hh | 4 +- Replicator/tests/ConnectedClientTest.cc | 34 +++++---- docs/overview/ConnectedClientProtocol.md | 9 ++- 7 files changed, 81 insertions(+), 79 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index ceb149ace..b7b2619aa 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -85,11 +85,12 @@ struct C4ConnectedClient : public fleece::RefCounted, AllDocsReceiver callback) =0; /// Callback for \ref query. - /// @param row On the first call, an array of colum names. - /// On subsequent calls, a result row as an array of column values. - /// On the final call, `nullptr`. + /// @param rowJSON A row of the result, encoded as a JSON object. + /// On the final call, will be `nullslice`. + /// @param rowDict The row parsed as a Fleece Dict, if you requested it. /// @param error NULL or, on the final call, a pointer to an error. - using QueryReceiver = std::function; /// Runs a query on the server and gets the results. @@ -97,10 +98,12 @@ struct C4ConnectedClient : public fleece::RefCounted, /// @param name The name by which the query has been registered on the server; /// or a full query string beginning with "SELECT " or "{". /// @param parameters A Dict mapping query parameter names to values. + /// @param rowsAsFleece True if you want the rows to be Fleece-encoded, false for JSON. /// @param receiver A callback that will be invoked for each row of the result, /// and/or if there's an error. virtual void query(slice name, FLDict C4NULLABLE parameters, + bool rowsAsFleece, QueryReceiver receiver) =0; /// Tells a connected client to start. diff --git a/Replicator/ConnectedClient/ConnectedClient.cc b/Replicator/ConnectedClient/ConnectedClient.cc index 730964d2e..4d0a3cf4b 100644 --- a/Replicator/ConnectedClient/ConnectedClient.cc +++ b/Replicator/ConnectedClient/ConnectedClient.cc @@ -25,9 +25,11 @@ #include "MessageBuilder.hh" #include "NumConversion.hh" #include "PropertyEncryption.hh" +#include "slice_stream.hh" #include "WebSocketInterface.hh" #include "c4Internal.hh" #include "fleece/Mutable.hh" +#include #define _options DONT_USE_OPTIONS // inherited from Worker, but replicator-specific, not used here @@ -585,7 +587,11 @@ namespace litecore::client { } - void ConnectedClient::query(slice name, fleece::Dict parameters, QueryReceiver receiver) { + void ConnectedClient::query(slice name, + fleece::Dict parameters, + bool asFleece, + QueryReceiver receiver) + { MessageBuilder req("query"); if (name.hasPrefix("SELECT ") || name.hasPrefix("select ") || name.hasPrefix("{")) req["src"] = name; @@ -602,54 +608,49 @@ namespace litecore::client { logInfo("...query got response"); C4Error err = responseError(response); if (!err) { - if (!receiveQueryRows(response, receiver)) + if (!receiveQueryRows(response, receiver, asFleece)) err = C4Error::make(LiteCoreDomain, kC4ErrorRemoteError, - "Invalid query response"); + "Couldn't parse server's response"); } // Final call to receiver: - receiver(nullptr, err ? &err : nullptr); + receiver(nullslice, nullptr, err ? &err : nullptr); }).onError([=](C4Error err) { logInfo("...query got error"); - receiver(nullptr, &err); + receiver(nullslice, nullptr, &err); }); //OPT: If we stream the response we can call the receiver function on results as they arrive. } - bool ConnectedClient::receiveQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { - Array rows = response->JSONBody().asArray(); - if (!rows) - return false; - for (Array::iterator i(rows); i; ++i) { - Array row = i->asArray(); - if (!row) - return false; - receiver(row, nullptr); - } - return true; - } +#if DEBUG + static constexpr bool kCheckJSON = true; +#else + static constexpr bool kCheckJSON = false; +#endif // not currently used; keeping it in case we decide to change the response format to lines-of-JSON - bool ConnectedClient::receiveMultiLineQueryRows(blip::MessageIn *response, const QueryReceiver &receiver) { - slice body = response->body(); - while (!body.empty()) { + bool ConnectedClient::receiveQueryRows(blip::MessageIn *response, + const QueryReceiver &receiver, + bool asFleece) + { + slice_istream body(response->body()); + while (!body.eof()) { // Get next line of JSON, up to a newline: - slice rowData; - if (const void *nl = body.findByte('\n')) { - rowData = body.upTo(nl); - body.setStart(offsetby(nl, 1)); - } else { - rowData = body; - body = nullslice; - } - Doc rowDoc = Doc::fromJSON(rowData); - if (Array row = rowDoc.asArray()) { - // Pass row to receiver: - receiver(row, nullptr); - } else { - return false; + slice rowData = body.readToDelimiterOrEnd("\n"); + if (!rowData.empty()) { + Dict rowDict; + Doc doc; + if (asFleece || kCheckJSON) { + doc = Doc::fromJSON(rowData); + rowDict = doc.asDict(); + if (!rowDict) + return false; + if (!asFleece) + rowDict = nullptr; + } + receiver(rowData, rowDict, nullptr); } } return true; diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index 84c0adea2..f7b4e3861 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -36,9 +36,12 @@ namespace litecore::client { /** A callback invoked for every row of a query result. - @param result An array of column values, or NULL if the query is complete or failed. + @param rowJSON The row as a JSON-encoded object, or `nullslice` on the final call. + @param rowDict The row as a Fleece `Dict` object, if you requested it, or `nullptr`. @param error Points to the error, else NULL. */ - using QueryReceiver = std::function; + using QueryReceiver = std::function; /** A live connection to Sync Gateway (or a CBL peer) that can do interactive CRUD operations. No C4Database necessary! @@ -169,10 +172,12 @@ namespace litecore::client { /// @param name The name by which the query has been registered on the server; /// or a full query string beginning with "SELECT " or "{". /// @param parameters A Dict mapping query parameter names to values. + /// @param asFleece If true, rows will be parsed as Fleece dicts for the callback. /// @param receiver A callback that will be invoked for each row of the result, /// and/or if there's an error. void query(slice name, fleece::Dict parameters, + bool asFleece, QueryReceiver receiver); // exposed for unit tests: @@ -198,8 +203,7 @@ namespace litecore::client { alloc_slice processIncomingDoc(slice docID, alloc_slice body, bool asFleece); void processOutgoingDoc(slice docID, slice revID, slice fleeceData, fleece::JSONEncoder &enc); bool receiveAllDocs(blip::MessageIn *, const AllDocsReceiver &); - bool receiveQueryRows(blip::MessageIn*, const QueryReceiver&); - bool receiveMultiLineQueryRows(blip::MessageIn*, const QueryReceiver&); + bool receiveQueryRows(blip::MessageIn*, const QueryReceiver&, bool asFleece); Delegate* _delegate; // Delegate whom I report progress/errors to C4ConnectedClientParameters _params; diff --git a/Replicator/ConnectedClient/QueryServer.cc b/Replicator/ConnectedClient/QueryServer.cc index 1c4fcca59..362354bfc 100644 --- a/Replicator/ConnectedClient/QueryServer.cc +++ b/Replicator/ConnectedClient/QueryServer.cc @@ -97,34 +97,23 @@ namespace litecore::repl { // Now run the query: blip::MessageBuilder reply(request); JSONEncoder &enc = reply.jsonBody(); - enc.beginArray(); _db->useLocked([&](C4Database*) { Stopwatch st; // Run the query: query->setParameters(request->body()); auto e = query->run(); - - // Send the column names as the first row: - unsigned nCols = query->columnCount(); - enc.beginArray(); - for (unsigned i = 0; i < nCols; i++) { - enc.writeString(query->columnTitle(i)); - } - enc.endArray(); - //enc.writeRaw("\n"); - - // Now send the real rows: while (e.next()) { - enc.beginArray(); + enc.beginDict(); + unsigned col = 0; for (Array::iterator i(e.columns()); i; ++i) { + enc.writeKey(query->columnTitle(col++)); enc.writeValue(*i); } - enc.endArray(); - //enc.writeRaw("\n"); + enc.endDict(); + enc.nextDocument(); // Writes a newline } logInfo("...query took %.1f ms", st.elapsedMS()); }); - enc.endArray(); request->respond(reply); } catch (...) { diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 2aff393bb..8596f46ab 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -110,8 +110,8 @@ namespace litecore::client { _client->getAllDocIDs(collectionID, pattern, callback); } - void query(slice name, FLDict C4NULLABLE parameters, QueryReceiver receiver) override { - _client->query(name, parameters, receiver); + void query(slice name, FLDict C4NULLABLE params, bool asFleece, QueryReceiver rcvr) override { + _client->query(name, params, asFleece, rcvr); } virtual void start() override { diff --git a/Replicator/tests/ConnectedClientTest.cc b/Replicator/tests/ConnectedClientTest.cc index d51e3e451..6a726354e 100644 --- a/Replicator/tests/ConnectedClientTest.cc +++ b/Replicator/tests/ConnectedClientTest.cc @@ -480,19 +480,20 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "named query from connected client mutex mut; condition_variable cond; - vector results; + vector jsonResults, fleeceResults; MutableDict params = fleece::MutableDict::newDict(); params["STATE"] = "CA"; - _client->query("guysIn", params, [&](fleece::Array row, const C4Error *error) { + _client->query("guysIn", params, true, [&](slice json, fleece::Dict row, const C4Error *error) { if (row) { CHECK(!error); Log("*** Got query row: %s", row.toJSONString().c_str()); - results.push_back(row.toJSONString()); + jsonResults.push_back(string(json)); + fleeceResults.push_back(row.toJSONString()); } else { Log("*** Got final row"); if (error) - results.push_back("Error: " + error->description()); + fleeceResults.push_back("Error: " + error->description()); unique_lock lock(mut); cond.notify_one(); } @@ -502,9 +503,11 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "named query from connected client unique_lock lock(mut); cond.wait(lock); Log("Query complete"); - vector expectedResults {R"(["first","last"])", - R"(["Cleveland","Bejcek"])", R"(["Rico","Hoopengardner"])"}; - CHECK(results == expectedResults); + vector expectedResults { + R"({"first":"Cleveland","last":"Bejcek"})", + R"({"first":"Rico","last":"Hoopengardner"})"}; + CHECK(fleeceResults == expectedResults); + CHECK(jsonResults == expectedResults); } @@ -518,19 +521,20 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "n1ql query from connected client" mutex mut; condition_variable cond; - vector results; + vector jsonResults, fleeceResults; MutableDict params = fleece::MutableDict::newDict(); params["STATE"] = "CA"; - _client->query(kQueryStr, params, [&](fleece::Array row, const C4Error *error) { + _client->query(kQueryStr, params, true, [&](slice json, fleece::Dict row, const C4Error *error) { if (row) { CHECK(!error); Log("*** Got query row: %s", row.toJSONString().c_str()); - results.push_back(row.toJSONString()); + jsonResults.push_back(string(json)); + fleeceResults.push_back(row.toJSONString()); } else { Log("*** Got final row"); if (error) - results.push_back("Error: " + error->description()); + fleeceResults.push_back("Error: " + error->description()); unique_lock lock(mut); cond.notify_one(); } @@ -540,7 +544,9 @@ TEST_CASE_METHOD(ConnectedClientLoopbackTest, "n1ql query from connected client" unique_lock lock(mut); cond.wait(lock); Log("Query complete"); - vector expectedResults {R"(["first","last"])", - R"(["Cleveland","Bejcek"])", R"(["Rico","Hoopengardner"])"}; - CHECK(results == expectedResults); + vector expectedResults { + R"({"first":"Cleveland","last":"Bejcek"})", + R"({"first":"Rico","last":"Hoopengardner"})"}; + CHECK(fleeceResults == expectedResults); + CHECK(jsonResults == expectedResults); } diff --git a/docs/overview/ConnectedClientProtocol.md b/docs/overview/ConnectedClientProtocol.md index fe11248e6..98964895e 100644 --- a/docs/overview/ConnectedClientProtocol.md +++ b/docs/overview/ConnectedClientProtocol.md @@ -76,7 +76,9 @@ Runs a query on the peer, identified by a name. Queries take zero or more named Optionally, a server MAY allow a client to run arbitrary queries. (Sync Gateway will not support this for security and performance reasons, but Couchbase Lite applications can choose to.) -The result of a query is a list of rows, each of which is an array of column values. Each row has the same number of columns. Each column has a name. +The result of a query is a list of zero or more rows. Each row is a JSON object. Rows are separated by single newline (ASCII 0A) characters. There MAY be a trailing newline at the end. + + Note that the entire result is not parseable as JSON (unless there's only one row.) But it's easy to split up by newlines. This format is easier to parse incrementally if results are being streamed. **Request**: @@ -86,10 +88,7 @@ The result of a query is a list of rows, each of which is an array of column val **Response**: -* Body: A JSON array: - * The array MAY be empty if there were no query results. - * Otherwise its first item MUST be an array of column names, each of which MUST be a string. - * The remaining items are the rows of the query result. Each row MUST be an array with the same number of elements as the first item. +* Body: Zero or more JSON objects (`{...}`) separated by newlines. **Errors**: From 4bb9a2258ef877484932f23b7c111830d2244c5f Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Thu, 26 May 2022 16:12:23 -0700 Subject: [PATCH 74/78] oops, updated Fleece to fix build --- vendor/fleece | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/fleece b/vendor/fleece index 1606c4d9b..ceb8c531e 160000 --- a/vendor/fleece +++ b/vendor/fleece @@ -1 +1 @@ -Subproject commit 1606c4d9bf166b1b8f873a852efd96f786c8344f +Subproject commit ceb8c531efc1480c2089fce94bf15afddf2bc901 From d3850cb0c148a1a207d7c580839ded6a94757cec Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 1 Jun 2022 11:12:57 -0700 Subject: [PATCH 75/78] Renamed Replicator::ProtocolName() -> protocolName() Method names shouldn't be uppercase... --- Replicator/Replicator.cc | 2 +- Replicator/Replicator.hh | 2 +- Replicator/c4RemoteReplicator.hh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Replicator/Replicator.cc b/Replicator/Replicator.cc index 92ea11ee5..66326224e 100644 --- a/Replicator/Replicator.cc +++ b/Replicator/Replicator.cc @@ -52,7 +52,7 @@ namespace litecore { namespace repl { }; - std::string Replicator::ProtocolName() { + std::string Replicator::protocolName() { stringstream result; delimiter delim(","); for (auto &name : kCompatProtocols) diff --git a/Replicator/Replicator.hh b/Replicator/Replicator.hh index cecc976de..6b96f8c6c 100644 --- a/Replicator/Replicator.hh +++ b/Replicator/Replicator.hh @@ -64,7 +64,7 @@ namespace litecore { namespace repl { using DocumentsEnded = std::vector>; - static std::string ProtocolName(); + static std::string protocolName(); /** Replicator delegate; receives progress & error notifications. */ class Delegate { diff --git a/Replicator/c4RemoteReplicator.hh b/Replicator/c4RemoteReplicator.hh index 7b6dfe0cc..754c2d0a9 100644 --- a/Replicator/c4RemoteReplicator.hh +++ b/Replicator/c4RemoteReplicator.hh @@ -243,7 +243,7 @@ namespace litecore { // Options to pass to the C4Socket alloc_slice socketOptions() const { Replicator::Options opts(kC4Disabled, kC4Disabled, _options->properties); - opts.setProperty(kC4SocketOptionWSProtocols, Replicator::ProtocolName().c_str()); + opts.setProperty(kC4SocketOptionWSProtocols, Replicator::protocolName().c_str()); return opts.properties.data(); } From f530bd9ecb49a15c784f05155d9c6e573f662c15 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 1 Jun 2022 11:15:26 -0700 Subject: [PATCH 76/78] Fixed: C4ConnectedClient doesn't use auth The options dict given to the WebSocket needs to contain all the options _plus_ `kC4SocketOptionWSProtocols`, not just the latter. Otherwise the WebSocket doesn't see the auth settings. While I was at it I removed c4ConnectedClientImpl.cc since it's only got two methods in it, making c4ConnectedClientImpl fully header-only. (It's only included in one place.) --- Replicator/c4ConnectedClientImpl.cc | 36 ------------------------ Replicator/c4ConnectedClientImpl.hh | 22 ++++++++++++--- Xcode/LiteCore.xcodeproj/project.pbxproj | 4 --- cmake/platform_base.cmake | 1 - 4 files changed, 18 insertions(+), 45 deletions(-) delete mode 100644 Replicator/c4ConnectedClientImpl.cc diff --git a/Replicator/c4ConnectedClientImpl.cc b/Replicator/c4ConnectedClientImpl.cc deleted file mode 100644 index 0af5c6441..000000000 --- a/Replicator/c4ConnectedClientImpl.cc +++ /dev/null @@ -1,36 +0,0 @@ -// -// c4ConnectedClientImpl.cc -// -// Copyright 2022-Present Couchbase, Inc. -// -// Use of this software is governed by the Business Source License included -// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified -// in that file, in accordance with the Business Source License, use of this -// software will be governed by the Apache License, Version 2.0, included in -// the file licenses/APL2.txt. -// - -#include "c4ConnectedClientImpl.hh" -#include "Replicator.hh" - -using namespace fleece; - -namespace litecore::client { - - alloc_slice C4ConnectedClientImpl::effectiveURL(slice url) { - string newPath = string(url); - if (!url.hasSuffix("/"_sl)) - newPath += "/"; - newPath += "_blipsync"; - return alloc_slice(newPath); - } - - alloc_slice C4ConnectedClientImpl::socketOptions() { - fleece::Encoder enc; - enc.beginDict(); - enc.writeKey(C4STR(kC4SocketOptionWSProtocols)); - enc.writeString(repl::Replicator::ProtocolName().c_str()); - enc.endDict(); - return enc.finish(); - } -} diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index 8596f46ab..baa52b846 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -17,6 +17,7 @@ #include "c4ConnectedClient.hh" #include "c4Socket+Internal.hh" #include "c4Internal.hh" +#include "Replicator.hh" #include "RevTree.hh" #include "TreeDocument.hh" @@ -26,7 +27,7 @@ namespace litecore::client { using namespace litecore::actor; using namespace std; - struct C4ConnectedClientImpl: public C4ConnectedClient, public ConnectedClient::Delegate { + struct C4ConnectedClientImpl final: public C4ConnectedClient, public ConnectedClient::Delegate { public: C4ConnectedClientImpl(const C4ConnectedClientParameters ¶ms) { @@ -37,7 +38,7 @@ namespace litecore::client { } auto webSocket = repl::CreateWebSocket(effectiveURL(params.url), - socketOptions(), + socketOptions(params), nullptr, _socketFactory); _client = new ConnectedClient(webSocket, *this, params); @@ -132,8 +133,21 @@ namespace litecore::client { _client = nullptr; } - alloc_slice effectiveURL(slice); - alloc_slice socketOptions(); + alloc_slice effectiveURL(slice url) { + string newPath(url); + if (!url.hasSuffix("/")) + newPath += "/"; + newPath += "_blipsync"; + return alloc_slice(newPath); + } + + alloc_slice socketOptions(const C4ConnectedClientParameters ¶ms) { + // Use a temporary repl::Options object, + // because it has the handy ability to add properties to an existing Fleece dict. + repl::Options opts(kC4Disabled, kC4Disabled, params.optionsDictFleece); + opts.setProperty(kC4SocketOptionWSProtocols, repl::Replicator::protocolName().c_str()); + return opts.properties.data(); + } mutable std::mutex _mutex; Retained _client; diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 2c2a01bed..7bd810ffd 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ /* Begin PBXBuildFile section */ 1A5726B227D74A8900A9B412 /* c4ConnectedClient.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */; }; - 1A6F835127E305710055C9F8 /* c4ConnectedClientImpl.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1A6F835027E305710055C9F8 /* c4ConnectedClientImpl.cc */; }; 1AE26CC727D9C432003C3043 /* c4ConnectedClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1AE26CC927D9C488003C3043 /* c4ConnectedClient_CAPI.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */; }; 2700BB53216FF2DB00797537 /* CoreML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2700BB4D216FF2DA00797537 /* CoreML.framework */; }; @@ -840,7 +839,6 @@ /* Begin PBXFileReference section */ 1A5726AF27D7474900A9B412 /* c4ConnectedClient.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4ConnectedClient.hh; sourceTree = ""; }; 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClient.cc; sourceTree = ""; }; - 1A6F835027E305710055C9F8 /* c4ConnectedClientImpl.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClientImpl.cc; sourceTree = ""; }; 1AE26CC327D9C42B003C3043 /* c4ConnectedClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = c4ConnectedClient.h; sourceTree = ""; }; 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = c4ConnectedClient_CAPI.cc; sourceTree = ""; }; 1AE26CCA27D9D456003C3043 /* c4ConnectedClientImpl.hh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = c4ConnectedClientImpl.hh; sourceTree = ""; }; @@ -2663,7 +2661,6 @@ 278BD6891EEB6756000DBF41 /* DatabaseCookies.cc */, 278BD68A1EEB6756000DBF41 /* DatabaseCookies.hh */, 1AE26CCA27D9D456003C3043 /* c4ConnectedClientImpl.hh */, - 1A6F835027E305710055C9F8 /* c4ConnectedClientImpl.cc */, 1A5726B127D74A8900A9B412 /* c4ConnectedClient.cc */, 1AE26CC827D9C488003C3043 /* c4ConnectedClient_CAPI.cc */, ); @@ -4371,7 +4368,6 @@ 2734F61A206ABEB000C982FF /* ReplicatorTypes.cc in Sources */, 2753AF721EBD190600C12E98 /* LogDecoder.cc in Sources */, 275CED451D3ECE9B001DE46C /* TreeDocument.cc in Sources */, - 1A6F835127E305710055C9F8 /* c4ConnectedClientImpl.cc in Sources */, 2708FE5E1CF6197D0022F721 /* RawRevTree.cc in Sources */, 27D9655C23355DC900F4A51C /* SecureSymmetricCrypto.cc in Sources */, 27469D05233D58D900A1EE1A /* c4Certificate.cc in Sources */, diff --git a/cmake/platform_base.cmake b/cmake/platform_base.cmake index 8913a3205..d97556938 100644 --- a/cmake/platform_base.cmake +++ b/cmake/platform_base.cmake @@ -111,7 +111,6 @@ function(set_litecore_source_base) Replicator/Worker.cc Replicator/c4ConnectedClient_CAPI.cc Replicator/c4ConnectedClient.cc - Replicator/c4ConnectedClientImpl.cc Replicator/ConnectedClient/ConnectedClient.cc Replicator/ConnectedClient/QueryServer.cc LiteCore/Support/Logging.cc From 6d231ff867d8041034ef68c37e54063b5a452430 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 1 Jun 2022 11:16:22 -0700 Subject: [PATCH 77/78] Xcode: Only run Doxygen in Release builds It's too annoying having hundreds of HTML files update due to small C API changes when you're switching between branches. --- Xcode/LiteCore.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcode/LiteCore.xcodeproj/project.pbxproj b/Xcode/LiteCore.xcodeproj/project.pbxproj index 7bd810ffd..5926ad568 100644 --- a/Xcode/LiteCore.xcodeproj/project.pbxproj +++ b/Xcode/LiteCore.xcodeproj/project.pbxproj @@ -3875,7 +3875,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = "/bin/sh -e"; - shellScript = "if which doxygen >/dev/null\nthen\n cd $SRCROOT/../C\n doxygen\nfi\n"; + shellScript = "if [[ \"$CONFIGURATION\" == Release* ]]\n if which doxygen >/dev/null\n then\n cd $SRCROOT/../C\n doxygen\n fi\nfi\n"; }; 275BF36C1F5F67940051374A /* Generate repo_version.h */ = { isa = PBXShellScriptBuildPhase; From f17bbc3306c844286d3c50cdacb60bc8eb130252 Mon Sep 17 00:00:00 2001 From: Jens Alfke Date: Wed, 1 Jun 2022 13:36:11 -0700 Subject: [PATCH 78/78] C4ConnectedClient: Implemented onStatusChanged callback Also getResponseHeaders() and getPeerTLSCertificate(). --- C/Cpp_include/c4ConnectedClient.hh | 11 ++- C/include/c4ConnectedClientTypes.h | 13 ++- Replicator/ConnectedClient/ConnectedClient.cc | 13 +-- Replicator/ConnectedClient/ConnectedClient.hh | 2 +- Replicator/c4ConnectedClientImpl.hh | 84 ++++++++++++++----- Replicator/tests/ConnectedClientTest.hh | 10 ++- 6 files changed, 98 insertions(+), 35 deletions(-) diff --git a/C/Cpp_include/c4ConnectedClient.hh b/C/Cpp_include/c4ConnectedClient.hh index b7b2619aa..1dd88a9a6 100644 --- a/C/Cpp_include/c4ConnectedClient.hh +++ b/C/Cpp_include/c4ConnectedClient.hh @@ -29,9 +29,18 @@ struct C4ConnectedClient : public fleece::RefCounted, /// @result A new \ref C4ConnectedClient, or NULL on failure. static Retained newClient(const C4ConnectedClientParameters ¶ms); + /// The current connection status. virtual litecore::actor::Async getStatus() const =0; - /** Result of a successful `getDoc()` call. */ + /// The HTTP response headers. + virtual alloc_slice getResponseHeaders() const noexcept =0; + +#ifdef COUCHBASE_ENTERPRISE + /// The server's TLS certificate. + virtual C4Cert* C4NULLABLE getPeerTLSCertificate() const =0; +#endif + + /// Result of a successful `getDoc()` call. struct DocResponse { alloc_slice docID, revID, body; bool deleted; diff --git a/C/include/c4ConnectedClientTypes.h b/C/include/c4ConnectedClientTypes.h index e4df91673..59171ee7a 100644 --- a/C/include/c4ConnectedClientTypes.h +++ b/C/include/c4ConnectedClientTypes.h @@ -28,19 +28,26 @@ typedef struct C4DocResponse { } C4DocResponse; +typedef C4ReplicatorStatus C4ConnectedClientStatus; + +/** Callback a client can register, to get status information. + This will be called on arbitrary background threads, and should not block. */ +typedef void (*C4ConnectedClientStatusChangedCallback)(C4ConnectedClient*, + C4ConnectedClientStatus, + void * C4NULLABLE context); + + /** Parameters describing a connected client, used when creating a \ref C4ConnectedClient. */ typedef struct C4ConnectedClientParameters { C4Slice url; ///clientStatusChanged(this, status); + } } } @@ -158,7 +161,6 @@ namespace litecore::client { status.reasonName(), status.code, FMTSLICE(status.message), state); bool closedByPeer = (_activityLevel != kC4Stopping); - setStatus(kC4Stopped); _connectionClosed(); @@ -183,6 +185,7 @@ namespace litecore::client { } gotError(C4Error::make(domain, code, status.message)); } + setStatus(kC4Stopped); { LOCK(_mutex); if (_delegate) diff --git a/Replicator/ConnectedClient/ConnectedClient.hh b/Replicator/ConnectedClient/ConnectedClient.hh index f7b4e3861..11c239e94 100644 --- a/Replicator/ConnectedClient/ConnectedClient.hh +++ b/Replicator/ConnectedClient/ConnectedClient.hh @@ -68,7 +68,7 @@ namespace litecore::client { virtual void clientGotTLSCertificate(ConnectedClient* NONNULL, slice certData) =0; virtual void clientStatusChanged(ConnectedClient* NONNULL, - ActivityLevel) =0; + const Status&) =0; virtual void clientConnectionClosed(ConnectedClient* NONNULL, const CloseStatus&) { } diff --git a/Replicator/c4ConnectedClientImpl.hh b/Replicator/c4ConnectedClientImpl.hh index baa52b846..329a3a377 100644 --- a/Replicator/c4ConnectedClientImpl.hh +++ b/Replicator/c4ConnectedClientImpl.hh @@ -17,10 +17,15 @@ #include "c4ConnectedClient.hh" #include "c4Socket+Internal.hh" #include "c4Internal.hh" +#include "Headers.hh" #include "Replicator.hh" #include "RevTree.hh" #include "TreeDocument.hh" +#ifdef COUCHBASE_ENTERPRISE +#include "c4Certificate.hh" +#endif + namespace litecore::client { using namespace litecore::websocket; @@ -30,17 +35,19 @@ namespace litecore::client { struct C4ConnectedClientImpl final: public C4ConnectedClient, public ConnectedClient::Delegate { public: - C4ConnectedClientImpl(const C4ConnectedClientParameters ¶ms) { + C4ConnectedClientImpl(const C4ConnectedClientParameters ¶ms) + :_onStatusChanged(params.onStatusChanged) + ,_callbackContext(params.callbackContext) + { if (params.socketFactory) { // Keep a copy of the C4SocketFactory struct in case original is invalidated: - _customSocketFactory = *params.socketFactory; - _socketFactory = &_customSocketFactory; + _socketFactory = *params.socketFactory; } auto webSocket = repl::CreateWebSocket(effectiveURL(params.url), socketOptions(params), nullptr, - _socketFactory); + (_socketFactory ? &*_socketFactory : nullptr)); _client = new ConnectedClient(webSocket, *this, params); _client->start(); } @@ -48,25 +55,52 @@ namespace litecore::client { Async getStatus() const override { return _client->status(); } - - protected: + + alloc_slice getResponseHeaders() const noexcept override { + return _responseHeaders; + } + +#ifdef COUCHBASE_ENTERPRISE + C4Cert* getPeerTLSCertificate() const override { + LOCK(_mutex); + if (!_peerTLSCertificate && _peerTLSCertificateData) { + _peerTLSCertificate = C4Cert::fromData(_peerTLSCertificateData); + _peerTLSCertificateData = nullptr; + } + return _peerTLSCertificate; + } +#endif + + #pragma mark - ConnectedClient Delegate - virtual void clientGotHTTPResponse(ConnectedClient* C4NONNULL client, + protected: + virtual void clientGotHTTPResponse(ConnectedClient* client, int status, - const websocket::Headers &headers) override { - // TODO: implement + const websocket::Headers &headers) override + { + _responseHeaders = headers.encode(); } - virtual void clientGotTLSCertificate(ConnectedClient* C4NONNULL client, - slice certData) override { - // TODO: implement + + virtual void clientGotTLSCertificate(ConnectedClient* client, + slice certData) override + { +#ifdef COUCHBASE_ENTERPRISE + LOCK(_mutex); + _peerTLSCertificateData = certData; + _peerTLSCertificate = nullptr; +#endif } - virtual void clientStatusChanged(ConnectedClient* C4NONNULL client, - ConnectedClient::ActivityLevel level) override { - // TODO: implement + + virtual void clientStatusChanged(ConnectedClient* client, + const ConnectedClient::Status &status) override + { + if (_onStatusChanged) + _onStatusChanged(this, status, _callbackContext); } - virtual void clientConnectionClosed(ConnectedClient* C4NONNULL client, const CloseStatus& status) override { - // TODO: implement + + virtual void clientConnectionClosed(ConnectedClient*, const CloseStatus& status) override { + // (do we need to do anything here?) } #pragma mark - @@ -111,7 +145,7 @@ namespace litecore::client { _client->getAllDocIDs(collectionID, pattern, callback); } - void query(slice name, FLDict C4NULLABLE params, bool asFleece, QueryReceiver rcvr) override { + void query(slice name, FLDict params, bool asFleece, QueryReceiver rcvr) override { _client->query(name, params, asFleece, rcvr); } @@ -149,9 +183,15 @@ namespace litecore::client { return opts.properties.data(); } - mutable std::mutex _mutex; - Retained _client; - const C4SocketFactory* C4NULLABLE _socketFactory {nullptr}; - C4SocketFactory _customSocketFactory {}; // Storage for *_socketFactory if non-null + mutable std::mutex _mutex; + Retained _client; + optional _socketFactory; + C4ConnectedClientStatusChangedCallback _onStatusChanged; + void* _callbackContext; + alloc_slice _responseHeaders; +#ifdef COUCHBASE_ENTERPRISE + mutable alloc_slice _peerTLSCertificateData; + mutable Retained _peerTLSCertificate; +#endif }; } diff --git a/Replicator/tests/ConnectedClientTest.hh b/Replicator/tests/ConnectedClientTest.hh index d21523591..56b39846d 100644 --- a/Replicator/tests/ConnectedClientTest.hh +++ b/Replicator/tests/ConnectedClientTest.hh @@ -124,21 +124,25 @@ public: { Log("+++ Client got HTTP response"); } + void clientGotTLSCertificate(client::ConnectedClient* NONNULL, slice certData) override { Log("+++ Client got TLS certificate"); } + void clientStatusChanged(client::ConnectedClient* NONNULL, - C4ReplicatorActivityLevel level) override { - Log("+++ Client status changed: %d", int(level)); - if (level == kC4Stopped) { + client::ConnectedClient::Status const& status) override + { + Log("+++ Client status changed: %d", int(status.level)); + if (status.level == kC4Stopped) { std::unique_lock lock(_mutex); _clientRunning = false; if (!_clientRunning && !_serverRunning) _cond.notify_all(); } } + void clientConnectionClosed(client::ConnectedClient* NONNULL, const CloseStatus &close) override { Log("+++ Client connection closed: reason=%d, code=%d, message=%.*s",