From: Philip Peterson <philip.c.peterson@xxxxxxxxx> Introduce a promise paradigm. A promise starts off in the pending state, and represents an asynchronous (or synchronous) action that will eventually end in either a successful result or a failure result. If a failure result, an error message may be provided. This allows us to represent tasks which may fail, while deferring any control flow actions or error printing that may occur in relation to said task. Signed-off-by: Philip Peterson <philip.c.peterson@xxxxxxxxx> --- Makefile | 1 + promise.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ promise.h | 71 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 promise.c create mode 100644 promise.h diff --git a/Makefile b/Makefile index 78e874099d9..4851eb2d822 100644 --- a/Makefile +++ b/Makefile @@ -1109,6 +1109,7 @@ LIB_OBJS += preload-index.o LIB_OBJS += pretty.o LIB_OBJS += prio-queue.o LIB_OBJS += progress.o +LIB_OBJS += promise.o LIB_OBJS += promisor-remote.o LIB_OBJS += prompt.o LIB_OBJS += protocol.o diff --git a/promise.c b/promise.c new file mode 100644 index 00000000000..58ed8b67880 --- /dev/null +++ b/promise.c @@ -0,0 +1,89 @@ +/* + * Generic implementation of callbacks with await checking. + */ +#include "promise.h" + +void promise_assert_finished(struct promise_t *p) { + if (p->state == PROMISE_UNRESOLVED) { + BUG("expected promise to have been resolved/rejected"); + } +} + +void promise_assert_failure(struct promise_t *p) { + if (p->state != PROMISE_FAILURE) { + BUG("expected promise to have been rejected"); + } +} + +void promise_resolve(struct promise_t *p, int status) { + if (p->state != PROMISE_UNRESOLVED) { + BUG("promise was already resolved/rejected"); + return; + } + p->result.success_result = status; + p->state = PROMISE_SUCCESS; +} + +void promise_reject(struct promise_t *p, int status, const char* fmt, ...) { + va_list args; + if (p->state != PROMISE_UNRESOLVED) { + BUG("promise was already resolved/rejected"); + return; + } + p->result.failure_result.status = status; + + strbuf_init(&p->result.failure_result.message, 0); + + va_start(args, fmt); + strbuf_vaddf(&p->result.failure_result.message, fmt, args); + va_end(args); + + p->state = PROMISE_FAILURE; +} + +struct promise_t *promise_init(void) { + // Promises are allocated on the heap, because they represent potentially long-running tasks, + // and a stack-allocated value might not live long enough. + struct promise_t *new_promise = xmalloc(sizeof(struct promise_t)); + struct failure_result_t failure_result; + + new_promise->state = PROMISE_UNRESOLVED; + failure_result.status = 0; + new_promise->result.failure_result = failure_result; + + return new_promise; +} + +/** + * Outputs an error message and size from a failed promise. The error message must be + * free()'ed by the caller. Calling this function is not allowed if the promise is not + * failed. + * + * Argument `size` may be omitted by passing in NULL. + * + * Note that although *error_message is null-terminated, its size may be larger + * than the terminated string, and its actual size is indicated by *size. + */ +void promise_copy_error(struct promise_t *p, char **error_message, size_t *size) { + size_t local_size; + promise_assert_failure(p); + + *error_message = strbuf_detach(&p->result.failure_result.message, &local_size); + if (size) { + *size = local_size; + } + + // We are only doing a copy, not a consume, so we need to put the error message back + // the way we found it. + strbuf_add(&p->result.failure_result.message, *error_message, strlen(*error_message)); +} + +/** + * Fully deallocates the promise as well as the error message, if any. + */ +void promise_release(struct promise_t *p) { + if (p->state == PROMISE_FAILURE) { + strbuf_release(&p->result.failure_result.message); + } + free(p); +} diff --git a/promise.h b/promise.h new file mode 100644 index 00000000000..c5500eba986 --- /dev/null +++ b/promise.h @@ -0,0 +1,71 @@ +#ifndef PROMISE_H +#define PROMISE_H + +#include "git-compat-util.h" +#include "strbuf.h" + +enum promise_state { + PROMISE_UNRESOLVED = 0, + PROMISE_SUCCESS = 1, + PROMISE_FAILURE = 2, +}; + +typedef int success_result_t; + +struct failure_result_t { + int status; + struct strbuf message; +}; + +struct promise_t { + enum promise_state state; + union { + success_result_t success_result; + struct failure_result_t failure_result; + } result; +}; + +// Function to assert that a promise has been resolved +void promise_assert_finished(struct promise_t *p); + +// Function to assert that a promise has been rejected +void promise_assert_failure(struct promise_t *p); + +// Function to resolve a promise with a success result +void promise_resolve(struct promise_t *p, int status); + +// Function to reject a promise with a failure result and an optional formatted error message +void promise_reject(struct promise_t *p, int status, const char* fmt, ...); + +// Function to create a new promise +struct promise_t *promise_init(void); + +// Copies the error out of a failed promise +void promise_copy_error(struct promise_t *promise, char **error_message, size_t *size); + +// Fully deallocates the promise +void promise_release(struct promise_t *promise); + +#define PROMISE_SUCCEED(p, errcode) do { \ + promise_resolve(p, errcode); \ + return; \ +} while (0) + +#define PROMISE_THROW(p, errcode, ...) do { \ + promise_reject(p, errcode, __VA_ARGS__); \ + return; \ +} while (0) + +#define PROMISE_BUBBLE_UP(dst, src, ...) do { \ + if (strlen(src->result.failure_result.message.buf) != 0) { \ + strbuf_insertf(&src->result.failure_result.message, 0, "\n\t"); \ + strbuf_insertf(&src->result.failure_result.message, 0, _("caused by:")); \ + strbuf_insertf(&src->result.failure_result.message, 0, "\n"); \ + strbuf_insertf(&src->result.failure_result.message, 0, __VA_ARGS__); \ + } \ + promise_reject(dst, src->result.failure_result.status, "%s", src->result.failure_result.message.buf); \ + promise_release(src); \ + return; \ +} while (0) + +#endif -- gitgitgadget