From fc180354a52d788afe7cf274496732ce377784bb Mon Sep 17 00:00:00 2001 From: skoppe Date: Mon, 4 Sep 2023 21:38:37 +0200 Subject: [PATCH] Format everything using sdfmt (#78) * Reformat with sdfmt * Support reggae Unfortunately it has to be using `dub run reggae -- --all-at-once` * Format all tests using sdfmt --- .gitignore | 7 + dub.sdl | 4 +- source/concurrency/asyncscope.d | 237 +-- source/concurrency/bitfield.d | 118 +- source/concurrency/data/queue/mpsc.d | 269 +-- source/concurrency/data/queue/waitable.d | 101 +- source/concurrency/error.d | 88 +- source/concurrency/executor.d | 6 +- source/concurrency/fork.d | 294 +-- source/concurrency/nursery.d | 491 ++--- .../operations/completewithcancellation.d | 49 +- .../operations/completewitherror.d | 78 +- source/concurrency/operations/forwardon.d | 66 +- source/concurrency/operations/ignoreerror.d | 56 +- source/concurrency/operations/on.d | 4 +- source/concurrency/operations/oncompletion.d | 70 +- source/concurrency/operations/onerror.d | 85 +- source/concurrency/operations/onresult.d | 98 +- source/concurrency/operations/ontermination.d | 72 +- source/concurrency/operations/race.d | 384 ++-- source/concurrency/operations/raceall.d | 2 +- source/concurrency/operations/repeat.d | 94 +- source/concurrency/operations/retry.d | 146 +- source/concurrency/operations/retrywhen.d | 178 +- source/concurrency/operations/stopon.d | 73 +- source/concurrency/operations/stopwhen.d | 338 ++-- source/concurrency/operations/then.d | 107 +- source/concurrency/operations/toshared.d | 428 +++-- source/concurrency/operations/tosingleton.d | 6 +- source/concurrency/operations/via.d | 136 +- source/concurrency/operations/whenall.d | 439 +++-- source/concurrency/operations/withchild.d | 31 +- source/concurrency/operations/withscheduler.d | 70 +- .../concurrency/operations/withstopsource.d | 148 +- source/concurrency/operations/withstoptoken.d | 130 +- source/concurrency/receiver.d | 134 +- source/concurrency/scheduler.d | 552 +++--- source/concurrency/sender.d | 1054 ++++++----- source/concurrency/signal.d | 339 ++-- source/concurrency/slist.d | 254 +-- source/concurrency/stoptoken.d | 760 ++++---- source/concurrency/stream/cron.d | 150 +- source/concurrency/stream/cycle.d | 28 +- source/concurrency/stream/defer.d | 115 +- source/concurrency/stream/filter.d | 73 +- source/concurrency/stream/flatmapbase.d | 530 +++--- source/concurrency/stream/flatmapconcat.d | 19 +- source/concurrency/stream/flatmaplatest.d | 19 +- source/concurrency/stream/package.d | 541 +++--- source/concurrency/stream/sample.d | 118 +- source/concurrency/stream/scan.d | 81 +- source/concurrency/stream/slide.d | 114 +- source/concurrency/stream/stream.d | 226 +-- source/concurrency/stream/take.d | 143 +- source/concurrency/stream/throttling.d | 597 +++--- source/concurrency/stream/tolist.d | 102 +- source/concurrency/stream/transform.d | 87 +- source/concurrency/syncwait.d | 269 +-- source/concurrency/thread.d | 783 ++++---- source/concurrency/timingwheels.d | 1649 ++++++++--------- source/concurrency/utils.d | 174 +- tests/ut/concurrency/asyncscope.d | 360 ++-- tests/ut/concurrency/fork.d | 19 +- tests/ut/concurrency/mpsc.d | 100 +- tests/ut/concurrency/nursery.d | 270 +-- tests/ut/concurrency/operations.d | 1469 ++++++++------- tests/ut/concurrency/pressure.d | 10 +- tests/ut/concurrency/pressure2.d | 10 +- tests/ut/concurrency/scheduler.d | 154 +- tests/ut/concurrency/sender.d | 629 ++++--- tests/ut/concurrency/slist.d | 186 +- tests/ut/concurrency/stream.d | 1586 ++++++++-------- tests/ut/concurrency/thread.d | 133 +- tests/ut/concurrency/utils.d | 79 +- tests/ut/concurrency/waitable.d | 103 +- tests/ut/ut_runner.d | 37 +- 76 files changed, 10007 insertions(+), 8952 deletions(-) diff --git a/.gitignore b/.gitignore index c48b097..418acf6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,10 @@ concurrency-test-* *.obj *.lst .dsemver +/.reggae +/.ninja_* +/build.ninja +/compile_commands.json +/reggaefile.d +/rules.ninja +/ut diff --git a/dub.sdl b/dub.sdl index 6fbf8f7..41efe6e 100644 --- a/dub.sdl +++ b/dub.sdl @@ -18,8 +18,8 @@ configuration "unittest" { targetType "executable" mainSourceFile "tests/ut/ut_runner.d" dflags "-dip1000" - sourcePaths "source" "tests/ut" - importPaths "source" "tests/ut" + sourcePaths "source" "tests" + importPaths "source" "tests" } configuration "unittest-release" { dependency "unit-threaded" version="*" diff --git a/source/concurrency/asyncscope.d b/source/concurrency/asyncscope.d index 3493105..6e1e25e 100644 --- a/source/concurrency/asyncscope.d +++ b/source/concurrency/asyncscope.d @@ -4,128 +4,139 @@ import concurrency.stoptoken; import concurrency.scheduler : NullScheduler; private enum Flag { - locked = 0, - stopped = 1, - tick = 2 + locked = 0, + stopped = 1, + tick = 2 } auto asyncScope() @safe { - // ensure NRVO - auto as = shared AsyncScope(new shared StopSource()); - return as; + // ensure NRVO + auto as = shared AsyncScope(new shared StopSource()); + return as; } struct AsyncScope { private: - import concurrency.bitfield : SharedBitField; - import concurrency.sender : Promise; - - shared SharedBitField!Flag flag; - shared Promise!void completion; - shared StopSource stopSource; - Throwable throwable; - - void forward() @trusted nothrow shared { - import core.atomic : atomicLoad; - auto t = throwable.atomicLoad(); - if (t !is null) - completion.error(cast(Throwable)t); - else - completion.fulfill(); - } - - void complete() @safe nothrow shared { - auto newState = flag.sub(Flag.tick); - if (newState == 1) { - forward(); - } - } - - void setError(Throwable t) @trusted nothrow shared { - import core.atomic : cas; - cas(&throwable, cast(shared Throwable)null, cast(shared)t); - stop(); - complete(); - } + import concurrency.bitfield : SharedBitField; + import concurrency.sender : Promise; + + shared SharedBitField!Flag flag; + shared Promise!void completion; + shared StopSource stopSource; + Throwable throwable; + + void forward() @trusted nothrow shared { + import core.atomic : atomicLoad; + auto t = throwable.atomicLoad(); + if (t !is null) + completion.error(cast(Throwable) t); + else + completion.fulfill(); + } + + void complete() @safe nothrow shared { + auto newState = flag.sub(Flag.tick); + if (newState == 1) { + forward(); + } + } + + void setError(Throwable t) @trusted nothrow shared { + import core.atomic : cas; + cas(&throwable, cast(shared Throwable) null, cast(shared) t); + stop(); + complete(); + } + public: - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - @disable this(); - - ~this() @safe shared { - import concurrency : syncWait; - import core.atomic : atomicLoad; - auto t = throwable.atomicLoad(); - if (t !is null && (cast(shared(Exception))t) is null) - return; - if (!completion.isCompleted) - cleanup.syncWait(); - } - - this(shared StopSource stopSource) @safe shared { - completion = new shared Promise!void; - this.stopSource = stopSource; - } - - auto cleanup() @safe shared { - stop(); - return completion.sender(); - } - - bool stop() nothrow @trusted { - return (cast(shared)this).stop(); - } - - bool stop() nothrow @trusted shared { - import core.atomic : MemoryOrder; - if ((flag.load!(MemoryOrder.acq) & Flag.stopped) > 0) - return false; - - auto newState = flag.add(Flag.stopped); - if (newState == 1) { - forward(); - } - return stopSource.stop(); - } - - bool spawn(Sender)(Sender s) shared @trusted { - import concurrency.sender : connectHeap; - with (flag.update(0, Flag.tick)) { - if ((oldState & Flag.stopped) == 1) { - complete(); - return false; - } - try { - s.connectHeap(AsyncScopeReceiver(&this)).start(); - } catch (Throwable t) { - // we are required to catch the throwable here, otherwise - // the destructor will wait infinitely for something that - // no longer runs - // by calling setError we ensure the internal state is correct - setError(t); - throw t; - } - return true; - } - } + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + @disable + this(); + + ~this() @safe shared { + import concurrency : syncWait; + import core.atomic : atomicLoad; + auto t = throwable.atomicLoad(); + if (t !is null && (cast(shared(Exception)) t) is null) + return; + if (!completion.isCompleted) + cleanup.syncWait(); + } + + this(shared StopSource stopSource) @safe shared { + completion = new shared Promise!void; + this.stopSource = stopSource; + } + + auto cleanup() @safe shared { + stop(); + return completion.sender(); + } + + bool stop() nothrow @trusted { + return (cast(shared) this).stop(); + } + + bool stop() nothrow @trusted shared { + import core.atomic : MemoryOrder; + if ((flag.load!(MemoryOrder.acq) & Flag.stopped) > 0) + return false; + + auto newState = flag.add(Flag.stopped); + if (newState == 1) { + forward(); + } + + return stopSource.stop(); + } + + bool spawn(Sender)(Sender s) shared @trusted { + import concurrency.sender : connectHeap; + with (flag.update(0, Flag.tick)) { + if ((oldState & Flag.stopped) == 1) { + complete(); + return false; + } + + try { + s.connectHeap(AsyncScopeReceiver(&this)).start(); + } catch (Throwable t) { + // we are required to catch the throwable here, otherwise + // the destructor will wait infinitely for something that + // no longer runs + // by calling setError we ensure the internal state is correct + setError(t); + throw t; + } + + return true; + } + } } struct AsyncScopeReceiver { - private shared AsyncScope* s; - void setValue() nothrow @safe { - s.complete(); - } - void setDone() nothrow @safe { - s.complete(); - } - void setError(Throwable t) nothrow @safe { - s.setError(t); - } - auto getStopToken() nothrow @safe { - import concurrency.stoptoken : StopToken; - return StopToken(s.stopSource); - } - auto getScheduler() nothrow @safe { - return NullScheduler(); - } + private shared AsyncScope* s; + void setValue() nothrow @safe { + s.complete(); + } + + void setDone() nothrow @safe { + s.complete(); + } + + void setError(Throwable t) nothrow @safe { + s.setError(t); + } + + auto getStopToken() nothrow @safe { + import concurrency.stoptoken : StopToken; + return StopToken(s.stopSource); + } + + auto getScheduler() nothrow @safe { + return NullScheduler(); + } } diff --git a/source/concurrency/bitfield.d b/source/concurrency/bitfield.d index 5186442..b6493ab 100644 --- a/source/concurrency/bitfield.d +++ b/source/concurrency/bitfield.d @@ -3,62 +3,76 @@ module concurrency.bitfield; import core.atomic : atomicOp, atomicLoad, MemoryOrder; struct Guard(Flags) { - private SharedBitField!(Flags)* obj; - size_t oldState, newState; - ~this() { - release(); - } - void release(size_t sub = 0) { - if (obj !is null) { - // TODO: want to use atomicFetchSub but (proper) support is only recent - // obj.store.atomicFetchSub!(MemoryOrder.rel)(sub | Flags.locked); - obj.store.atomicOp!"-="(sub | Flags.locked); - } - obj = null; - } - bool was(Flags flags) { - return (oldState & flags) == flags; - } + private SharedBitField!(Flags)* obj; + size_t oldState, newState; + ~this() { + release(); + } + + void release(size_t sub = 0) { + if (obj !is null) { + // TODO: want to use atomicFetchSub but (proper) support is only recent + // obj.store.atomicFetchSub!(MemoryOrder.rel)(sub | Flags.locked); + obj.store.atomicOp!"-="(sub | Flags.locked); + } + + obj = null; + } + + bool was(Flags flags) { + return (oldState & flags) == flags; + } } shared struct SharedBitField(Flags) { - static assert(__traits(compiles, Flags.locked), "Must have a 'locked' flag"); - private shared size_t store; - static if (Flags.locked > 0) { - Guard!Flags lock(size_t or = 0, size_t add = 0, size_t sub = 0) return scope @safe @nogc nothrow { - return Guard!Flags(&this, update(Flags.locked | or, add, sub).expand); - } - } - auto update(size_t or, size_t add = 0, size_t sub = 0) nothrow { - import concurrency.utils : spin_yield, casWeak; - import std.typecons : tuple; - size_t oldState, newState; - do { - goto load_state; - do { - spin_yield(); - load_state: - oldState = store.atomicLoad!(MemoryOrder.acq); - } while ((oldState & Flags.locked) > 0); - newState = (oldState + add - sub) | or; - } while (!casWeak!(MemoryOrder.acq, MemoryOrder.acq)(&store, oldState, newState)); - return tuple!("oldState", "newState")(oldState, newState); - } - auto add(size_t add) nothrow { - return Result!Flags(store.atomicOp!"+="(add)); - } - auto sub(size_t sub) nothrow { - return Result!Flags(store.atomicOp!"-="(sub)); - } - size_t load(MemoryOrder ms)() { - return store.atomicLoad!ms; - } + static assert(__traits(compiles, Flags.locked), + "Must have a 'locked' flag"); + private shared size_t store; + static if (Flags.locked > 0) { + Guard!Flags lock(size_t or = 0, size_t add = 0, + size_t sub = 0) return scope @safe @nogc nothrow { + return + Guard!Flags(&this, update(Flags.locked | or, add, sub).expand); + } + } + + auto update(size_t or, size_t add = 0, size_t sub = 0) nothrow { + import concurrency.utils : spin_yield, casWeak; + import std.typecons : tuple; + size_t oldState, newState; + do { + goto load_state; + do { + spin_yield(); + + load_state: + oldState = store.atomicLoad!(MemoryOrder.acq); + } while ((oldState & Flags.locked) > 0); + + newState = (oldState + add - sub) | or; + } while (!casWeak!(MemoryOrder.acq, MemoryOrder.acq)(&store, oldState, + newState)); + + return tuple!("oldState", "newState")(oldState, newState); + } + + auto add(size_t add) nothrow { + return Result!Flags(store.atomicOp!"+="(add)); + } + + auto sub(size_t sub) nothrow { + return Result!Flags(store.atomicOp!"-="(sub)); + } + + size_t load(MemoryOrder ms)() { + return store.atomicLoad!ms; + } } struct Result(Flags) { - size_t state; - alias state this; - bool has(Flags flags) { - return (state & flags) == flags; - } + size_t state; + alias state this; + bool has(Flags flags) { + return (state & flags) == flags; + } } diff --git a/source/concurrency/data/queue/mpsc.d b/source/concurrency/data/queue/mpsc.d index 293a10e..c303720 100644 --- a/source/concurrency/data/queue/mpsc.d +++ b/source/concurrency/data/queue/mpsc.d @@ -1,150 +1,157 @@ module concurrency.data.queue.mpsc; struct MPSCQueueProducer(Node) { - private MPSCQueue!(Node) q; - void push(Node* node) shared @trusted { - (cast()q).push(node); - } + private MPSCQueue!(Node) q; + void push(Node* node) shared @trusted { + (cast() q).push(node); + } } final class MPSCQueue(Node) { - import std.traits : hasUnsharedAliasing; - alias ElementType = Node*; - private Node* head, tail; - private Node stub; - - static if (hasUnsharedAliasing!Node) { - // TODO: next version add deprecated("Node has unshared aliasing") - this() @safe nothrow @nogc { - head = tail = &stub; - stub.next = null; - } - } else { - this() @safe nothrow @nogc { - head = tail = &stub; - stub.next = null; - } - } - - shared(MPSCQueueProducer!Node) producer() @trusted nothrow @nogc { - return shared MPSCQueueProducer!Node(cast(shared)this); - } - - bool push(Node* n) @trusted nothrow @nogc shared { - return (cast()this).push(n); - } - - /// returns true if first to push - bool push(Node* n) @safe nothrow @nogc { - import core.atomic : atomicExchange; - n.next = null; - Node* prev = atomicExchange(&head, n); - prev.next = toShared(n); - return prev is &stub; - } - - bool empty() @safe nothrow @nogc { - import core.atomic : atomicLoad; - return head.atomicLoad is &stub; - } - - /// returns chain with head and tail - Chain!Node popAll() @safe nothrow @nogc { - import core.atomic : atomicExchange; - - Node* first = atomicExchange(&head, &stub); - Node* last = tail; - tail = &stub; - if (last is &stub) - last = last.next.toUnshared(); - if (first is &stub) - first = null; - return Chain!Node(first, last); - } - - // pushed list of items, can only be done by consumer - bool push(Chain!Node chain) @safe nothrow @nogc { - if (chain.head is null || chain.tail is null) - return false; - - import core.atomic : atomicExchange; - chain.head.next = null; - Node* prev = atomicExchange(&head, chain.head); - prev.next = toShared(chain.tail); - return prev is &stub; - } - - /// returns node or null if none - Node* pop() @safe nothrow @nogc { - import core.atomic : atomicLoad, MemoryOrder; - while(true) { - Node* end = this.tail; - shared Node* next = tail.next.atomicLoad!(MemoryOrder.raw).toShared(); - if (end is &stub) { // still pointing to stub - if (null is next) // no new nodes - return null; - this.tail = next.toUnshared(); // at least one node was added and stub.next points to the first one - end = next.toUnshared(); - next = next.next.atomicLoad!(MemoryOrder.raw); - } - if (next) { // if there is at least another node - this.tail = toUnshared(next); - return end; - } - Node* start = this.head.atomicLoad!(MemoryOrder.acq); - if (end is start) { - push(&stub); - next = end.next.atomicLoad!(MemoryOrder.raw).toShared(); - if (next) { - this.tail = toUnshared(next); - return end; - } - } - } - } - - auto opSlice() @safe nothrow @nogc { - import core.atomic : atomicLoad, MemoryOrder; - auto current = atomicLoad(tail); - if (current is &stub) - current = current.next.atomicLoad!(MemoryOrder.raw).toUnshared(); - return Iterator!(Node)(current); - } + import std.traits : hasUnsharedAliasing; + alias ElementType = Node*; + private Node* head, tail; + private Node stub; + + static if (hasUnsharedAliasing!Node) { + // TODO: next version add deprecated("Node has unshared aliasing") + this() @safe nothrow @nogc { + head = tail = &stub; + stub.next = null; + } + } else { + this() @safe nothrow @nogc { + head = tail = &stub; + stub.next = null; + } + } + + shared(MPSCQueueProducer!Node) producer() @trusted nothrow @nogc { + return shared MPSCQueueProducer!Node(cast(shared) this); + } + + bool push(Node* n) @trusted nothrow @nogc shared { + return (cast() this).push(n); + } + + /// returns true if first to push + bool push(Node* n) @safe nothrow @nogc { + import core.atomic : atomicExchange; + n.next = null; + Node* prev = atomicExchange(&head, n); + prev.next = toShared(n); + return prev is &stub; + } + + bool empty() @safe nothrow @nogc { + import core.atomic : atomicLoad; + return head.atomicLoad is &stub; + } + + /// returns chain with head and tail + Chain!Node popAll() @safe nothrow @nogc { + import core.atomic : atomicExchange; + + Node* first = atomicExchange(&head, &stub); + Node* last = tail; + tail = &stub; + if (last is &stub) + last = last.next.toUnshared(); + if (first is &stub) + first = null; + return Chain!Node(first, last); + } + + // pushed list of items, can only be done by consumer + bool push(Chain!Node chain) @safe nothrow @nogc { + if (chain.head is null || chain.tail is null) + return false; + + import core.atomic : atomicExchange; + chain.head.next = null; + Node* prev = atomicExchange(&head, chain.head); + prev.next = toShared(chain.tail); + return prev is &stub; + } + + /// returns node or null if none + Node* pop() @safe nothrow @nogc { + import core.atomic : atomicLoad, MemoryOrder; + while (true) { + Node* end = this.tail; + shared + Node* next = tail.next.atomicLoad!(MemoryOrder.raw).toShared(); + if (end is &stub) { // still pointing to stub + if (null is next) // no new nodes + return null; + this.tail = next + .toUnshared(); // at least one node was added and stub.next points to the first one + end = next.toUnshared(); + next = next.next.atomicLoad!(MemoryOrder.raw); + } + + if (next) { // if there is at least another node + this.tail = toUnshared(next); + return end; + } + + Node* start = this.head.atomicLoad!(MemoryOrder.acq); + if (end is start) { + push(&stub); + next = end.next.atomicLoad!(MemoryOrder.raw).toShared(); + if (next) { + this.tail = toUnshared(next); + return end; + } + } + } + } + + auto opSlice() @safe nothrow @nogc { + import core.atomic : atomicLoad, MemoryOrder; + auto current = atomicLoad(tail); + if (current is &stub) + current = current.next.atomicLoad!(MemoryOrder.raw).toUnshared(); + return Iterator!(Node)(current); + } } struct Chain(Node) { - Node* head; - Node* tail; + Node* head; + Node* tail; } struct Iterator(Node) { - Node* head; - this(Node* head) @safe nothrow @nogc { - this.head = head; - } - bool empty() @safe nothrow @nogc { - return head is null; - } - Node* front() @safe nothrow @nogc { - return head; - } - void popFront() @safe nothrow @nogc { - import core.atomic : atomicLoad, MemoryOrder; - head = head.next.atomicLoad!(MemoryOrder.raw).toUnshared(); - } - Iterator!(Node) safe() { - return Iterator!(Node)(head); - } + Node* head; + this(Node* head) @safe nothrow @nogc { + this.head = head; + } + + bool empty() @safe nothrow @nogc { + return head is null; + } + + Node* front() @safe nothrow @nogc { + return head; + } + + void popFront() @safe nothrow @nogc { + import core.atomic : atomicLoad, MemoryOrder; + head = head.next.atomicLoad!(MemoryOrder.raw).toUnshared(); + } + + Iterator!(Node) safe() { + return Iterator!(Node)(head); + } } - private shared(Node*) toShared(Node)(Node* node) @trusted nothrow @nogc { - return cast(shared)node; + return cast(shared) node; } private auto toUnshared(Node)(Node* node) @trusted nothrow @nogc { - static if (is(Node : shared(T), T)) - return cast(T*)node; - else - return node; + static if (is(Node : shared(T), T)) + return cast(T*) node; + else + return node; } diff --git a/source/concurrency/data/queue/waitable.d b/source/concurrency/data/queue/waitable.d index 23543d2..55b9f13 100644 --- a/source/concurrency/data/queue/waitable.d +++ b/source/concurrency/data/queue/waitable.d @@ -1,70 +1,71 @@ module concurrency.data.queue.waitable; final class WaitableQueue(Q) { - import core.sync.semaphore : Semaphore; - import core.time : Duration; - import mir.algebraic : Nullable; + import core.sync.semaphore : Semaphore; + import core.time : Duration; + import mir.algebraic : Nullable; - private Q q; - private Semaphore sema; + private Q q; + private Semaphore sema; - this() @trusted { - q = new Q(); - sema = new Semaphore(); - } + this() @trusted { + q = new Q(); + sema = new Semaphore(); + } - bool push(Q.ElementType t) @trusted { - bool r = q.push(t); - if (r) - sema.notify(); - return r; - } + bool push(Q.ElementType t) @trusted { + bool r = q.push(t); + if (r) + sema.notify(); + return r; + } - Q.ElementType pop() @trusted { - auto r = q.pop(); - if (r !is null) - return r; + Q.ElementType pop() @trusted { + auto r = q.pop(); + if (r !is null) + return r; - sema.wait(); - return q.pop(); - } + sema.wait(); + return q.pop(); + } - bool empty() @safe @nogc nothrow { - return q.empty(); - } + bool empty() @safe @nogc nothrow { + return q.empty(); + } - Q.ElementType pop(Duration max) @trusted { - auto r = q.pop(); - if (r !is null) - return r; + Q.ElementType pop(Duration max) @trusted { + auto r = q.pop(); + if (r !is null) + return r; - if (!sema.wait(max)) - return null; + if (!sema.wait(max)) + return null; - return q.pop(); - } + return q.pop(); + } - auto opSlice() @safe nothrow @nogc { - return q.opSlice(); - } + auto opSlice() @safe nothrow @nogc { + return q.opSlice(); + } - static if (__traits(compiles, q.producer)) { - shared(WaitableQueueProducer!Q) producer() @trusted nothrow @nogc { - return shared WaitableQueueProducer!Q(cast(shared)q, cast(shared)sema); - } - } + static if (__traits(compiles, q.producer)) { + shared(WaitableQueueProducer!Q) producer() @trusted nothrow @nogc { + return shared WaitableQueueProducer!Q(cast(shared) q, + cast(shared) sema); + } + } } struct WaitableQueueProducer(Q) { - import core.sync.semaphore : Semaphore; + import core.sync.semaphore : Semaphore; - private shared Q q; - private shared Semaphore sema; + private shared Q q; + private shared Semaphore sema; - bool push(Q.ElementType t) shared @trusted { - bool r = (cast()q).push(t); - if (r) - (cast()sema).notify(); - return r; - } + bool push(Q.ElementType t) shared @trusted { + bool r = (cast() q).push(t); + if (r) + (cast() sema).notify(); + return r; + } } diff --git a/source/concurrency/error.d b/source/concurrency/error.d index 9324a46..653882a 100644 --- a/source/concurrency/error.d +++ b/source/concurrency/error.d @@ -3,53 +3,57 @@ module concurrency.error; // In order to handle Errors that get thrown on non-main threads we have to make a clone. There are cases where the Error is constructed in TLS, which either might be overwritten with another Error (if the thread continues work and hits another Error), or might point to invalid memory once the thread is cleaned up. In order to be safe we have to clone the Error. class ThrowableClone(T : Throwable) : T { - this(Args...)(Throwable.TraceInfo info, Args args) @safe nothrow { - super(args); - if (info) - this.info = new ClonedTraceInfo(info); - } + this(Args...)(Throwable.TraceInfo info, Args args) @safe nothrow { + super(args); + if (info) + this.info = new ClonedTraceInfo(info); + } } // The reason it accepts a Throwable is because there might be classes that derive directly from Throwable but aren't Exceptions. We treat them as errors here. Throwable clone(Throwable t) nothrow @safe { - import core.exception; - if (auto a = cast(AssertError)t) - return new ThrowableClone!AssertError(t.info, a.msg, a.file, a.line, a.next); - if (auto r = cast(RangeError)t) - return new ThrowableClone!RangeError(t.info, r.file, r.line, r.next); - if (auto e = cast(Error)t) - return new ThrowableClone!Error(t.info, t.msg, t.file, t.line, t.next); - return new ThrowableClone!Throwable(t.info, t.msg, t.file, t.line, t.next); + import core.exception; + if (auto a = cast(AssertError) t) + return new ThrowableClone!AssertError(t.info, a.msg, a.file, a.line, + a.next); + if (auto r = cast(RangeError) t) + return new ThrowableClone!RangeError(t.info, r.file, r.line, r.next); + if (auto e = cast(Error) t) + return new ThrowableClone!Error(t.info, t.msg, t.file, t.line, t.next); + return new ThrowableClone!Throwable(t.info, t.msg, t.file, t.line, t.next); } class ClonedTraceInfo : Throwable.TraceInfo { - string[] buf; - this(Throwable.TraceInfo t) @trusted nothrow { - if (t) { - try { - foreach (i, line; t) - buf ~= line.idup(); - } catch (Throwable t) { - // alas... - } - } - } - override int opApply(scope int delegate(ref const(char[])) dg) const { - return opApply((ref size_t, ref const(char[]) buf) => dg(buf)); - } - - override int opApply(scope int delegate(ref size_t, ref const(char[])) dg) const { - foreach(i, line; buf) { - if (dg(i,line)) - return 1; - } - return 0; - } - - override string toString() const { - string buf; - foreach ( i, line; this ) - buf ~= i ? "\n" ~ line : line; - return buf; - } + string[] buf; + this(Throwable.TraceInfo t) @trusted nothrow { + if (t) { + try { + foreach (i, line; t) + buf ~= line.idup(); + } catch (Throwable t) { + // alas... + } + } + } + + override int opApply(scope int delegate(ref const(char[])) dg) const { + return opApply((ref size_t, ref const(char[]) buf) => dg(buf)); + } + + override + int opApply(scope int delegate(ref size_t, ref const(char[])) dg) const { + foreach (i, line; buf) { + if (dg(i, line)) + return 1; + } + + return 0; + } + + override string toString() const { + string buf; + foreach (i, line; this) + buf ~= i ? "\n" ~ line : line; + return buf; + } } diff --git a/source/concurrency/executor.d b/source/concurrency/executor.d index 8b5e286..98c66fb 100644 --- a/source/concurrency/executor.d +++ b/source/concurrency/executor.d @@ -4,7 +4,7 @@ alias VoidFunction = void function() @safe; alias VoidDelegate = void delegate() shared @safe; interface Executor { - void execute(VoidFunction fn) @safe; - void execute(VoidDelegate fn) @safe; - bool isInContext() @safe; + void execute(VoidFunction fn) @safe; + void execute(VoidDelegate fn) @safe; + bool isInContext() @safe; } diff --git a/source/concurrency/fork.d b/source/concurrency/fork.d index 6e9bfbf..1ea9320 100644 --- a/source/concurrency/fork.d +++ b/source/concurrency/fork.d @@ -7,139 +7,161 @@ import concepts; // TODO: fork is an scheduler :) // therefor the Function fun could be a `then` continuation -version (Posix) -struct ForkSender { - alias Value = void; - static assert(models!(typeof(this), isSender)); - alias Fun = void delegate() shared; - alias AfterFork = void delegate(int); - static struct Operation(Receiver) { - private { - Executor executor; - void delegate() shared fun; - Receiver receiver; - void delegate(int) afterFork; - void run() { - import concurrency.thread : executeInNewThread, executeAndWait; - import concurrency.stoptoken; - import core.sys.posix.sys.wait; - import core.sys.posix.sys.select; - import core.sys.posix.unistd; - import core.stdc.errno; - import core.stdc.stdlib; - import core.stdc.string : strerror; - import std.string : fromStringz; - import std.format : format; - - auto token = receiver.getStopToken(); - shared pid = executor.executeAndWait((void delegate() shared fun) { - auto r = fork(); - if (r != 0) - return r; - - reinitThreadLocks(); - detachOtherThreads(); - drainMessageBox(); - setMainThread(); - try { - (cast()fun)(); - } catch (Throwable t) { - exit(1); - } - exit(0); - assert(0); - }, fun); - - if (pid == -1) { - receiver.setError(new Exception("Failed to fork, %s".format(strerror(errno).fromStringz))); - return; - } - import core.sys.posix.signal : kill, SIGINT; - - if (afterFork) - afterFork(pid); - auto cb = token.onStop(() @trusted shared nothrow => cast(void)kill(pid, SIGINT)); - int status; - auto ret = waitpid(pid, &status, 0); - cb.dispose(); - if (ret == -1) { - receiver.setError(new Exception("Failed to wait for child, %s".format(strerror(errno).fromStringz))); - } - else if (WIFSIGNALED(status)) { - auto exitsignal = WTERMSIG(status); - receiver.setError(new Exception("Child exited by signal %d".format(exitsignal))); - } - else if (WIFEXITED(status)) { - auto exitstatus = WEXITSTATUS(status); - if (exitstatus == 0) - receiver.setValue(); - else - receiver.setError(new Exception("Child exited with %d".format(exitstatus))); - } - else { - receiver.setError(new Exception("Child unknown exit")); - } - } - } - this(Executor executor, Fun fun, Receiver receiver, AfterFork afterFork) { - this.executor = executor; - this.fun = fun; - this.receiver = receiver; - this.afterFork = afterFork; - } - void start() @trusted nothrow { - import concurrency.thread : executeInNewThread, executeAndWait; - import concurrency.utils : closure; - - executeInNewThread(cast(void delegate() shared @safe)&this.run); - } - } - private Executor executor; - private void delegate() shared fun; - private void delegate(int) afterFork; - this(Executor executor, void delegate() shared fun, void delegate(int) afterFork = null) @system { // forking is dangerous so this is @system - this.executor = executor; - this.fun = fun; - this.afterFork = afterFork; - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - return new Operation!Receiver(executor, fun, receiver, afterFork); - } - static void reinitThreadLocks() { - import core.thread : Thread; - - __traits(getMember, Thread, "initLocks")(); - } - // After forking there is only one thread left, but others (if any) are still registered, we need to detach them else the GC will fail suspending them during a GC cycle - static private void detachOtherThreads() { - import core.thread : Thread, thread_detachInstance; - import core.memory : GC; - - GC.disable(); - auto threads = Thread.getAll(); - auto thisThread = Thread.getThis(); - foreach (t; threads) { - if (t != thisThread) - thread_detachInstance(t); - } - GC.enable(); - } - static private void drainMessageBox() { - import std.concurrency : receiveTimeout; - import std.variant : Variant; - import core.time : seconds; - import std.concurrency : thisTid; - thisTid(); // need to call otherwise the messagebox might be empty and receiveTimeout will assert - - while(receiveTimeout(seconds(-1),(Variant v){})) {} - } - // after fork there is only one thread left, it is possible - // that wasn't the main thread in the 'old' program, so - // we overwrite the global to point to the only thread left - // in the (forked) process - static private void setMainThread() { - import core.thread : Thread; - - __traits(getMember, Thread, "sm_main") = Thread.getThis(); - } -} +version(Posix) + struct ForkSender { + alias Value = void; + static assert(models!(typeof(this), isSender)); + alias Fun = void delegate() shared; + alias AfterFork = void delegate(int); + static struct Operation(Receiver) { + private { + Executor executor; + void delegate() shared fun; + Receiver receiver; + void delegate(int) afterFork; + void run() { + import concurrency.thread : executeInNewThread, + executeAndWait; + import concurrency.stoptoken; + import core.sys.posix.sys.wait; + import core.sys.posix.sys.select; + import core.sys.posix.unistd; + import core.stdc.errno; + import core.stdc.stdlib; + import core.stdc.string : strerror; + import std.string : fromStringz; + import std.format : format; + + auto token = receiver.getStopToken(); + shared pid = + executor.executeAndWait((void delegate() shared fun) { + auto r = fork(); + if (r != 0) + return r; + + reinitThreadLocks(); + detachOtherThreads(); + drainMessageBox(); + setMainThread(); + try { + (cast() fun)(); + } catch (Throwable t) { + exit(1); + } + + exit(0); + assert(0); + }, fun); + + if (pid == -1) { + receiver.setError(new Exception("Failed to fork, %s" + .format(strerror(errno).fromStringz))); + return; + } + + import core.sys.posix.signal : kill, SIGINT; + + if (afterFork) + afterFork(pid); + auto cb = token.onStop(() @trusted shared nothrow => + cast(void) kill(pid, SIGINT)); + int status; + auto ret = waitpid(pid, &status, 0); + cb.dispose(); + if (ret == -1) { + receiver.setError(new Exception( + "Failed to wait for child, %s" + .format(strerror(errno).fromStringz))); + } else if (WIFSIGNALED(status)) { + auto exitsignal = WTERMSIG(status); + receiver.setError(new Exception( + "Child exited by signal %d".format(exitsignal))); + } else if (WIFEXITED(status)) { + auto exitstatus = WEXITSTATUS(status); + if (exitstatus == 0) + receiver.setValue(); + else + receiver.setError(new Exception( + "Child exited with %d".format(exitstatus))); + } else { + receiver.setError(new Exception("Child unknown exit")); + } + } + } + + this(Executor executor, Fun fun, Receiver receiver, + AfterFork afterFork) { + this.executor = executor; + this.fun = fun; + this.receiver = receiver; + this.afterFork = afterFork; + } + + void start() @trusted nothrow { + import concurrency.thread : executeInNewThread, executeAndWait; + import concurrency.utils : closure; + + executeInNewThread( + cast(void delegate() shared @safe) &this.run); + } + } + + private Executor executor; + private void delegate() shared fun; + private void delegate(int) afterFork; + this( + Executor executor, + void delegate() shared fun, + void delegate(int) afterFork = null + ) @system { // forking is dangerous so this is @system + this.executor = executor; + this.fun = fun; + this.afterFork = afterFork; + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + return new Operation!Receiver(executor, fun, receiver, afterFork); + } + + static void reinitThreadLocks() { + import core.thread : Thread; + + __traits(getMember, Thread, "initLocks")(); + } + + // After forking there is only one thread left, but others (if any) are still registered, we need to detach them else the GC will fail suspending them during a GC cycle + static private void detachOtherThreads() { + import core.thread : Thread, thread_detachInstance; + import core.memory : GC; + + GC.disable(); + auto threads = Thread.getAll(); + auto thisThread = Thread.getThis(); + foreach (t; threads) { + if (t != thisThread) + thread_detachInstance(t); + } + + GC.enable(); + } + + static private void drainMessageBox() { + import std.concurrency : receiveTimeout; + import std.variant : Variant; + import core.time : seconds; + import std.concurrency : thisTid; + thisTid(); // need to call otherwise the messagebox might be empty and receiveTimeout will assert + + while (receiveTimeout(seconds(-1), (Variant v) {})) {} + } + + // after fork there is only one thread left, it is possible + // that wasn't the main thread in the 'old' program, so + // we overwrite the global to point to the only thread left + // in the (forked) process + static private void setMainThread() { + import core.thread : Thread; + + __traits(getMember, Thread, "sm_main") = Thread.getThis(); + } + } diff --git a/source/concurrency/nursery.d b/source/concurrency/nursery.d index abc1e7a..17ff76b 100644 --- a/source/concurrency/nursery.d +++ b/source/concurrency/nursery.d @@ -15,244 +15,273 @@ import std.typecons : Nullable; /// Senders can be added to the Nursery at any time. /// Senders are only started when the Nursery itself is being awaited on. class Nursery : StopSource { - import concurrency.sender : isSender, OperationalStateBase; - import core.sync.mutex : Mutex; - import concepts; - static assert (models!(typeof(this), isSender)); - - alias Value = void; - private { - Node[] operations; - struct Node { - OperationalStateBase state; - void start() @safe nothrow { - state.start(); - } - size_t id; - } - Mutex mutex; - shared size_t busy = 1; // we start at 1 to denote the Nursery is open for tasks - shared size_t counter = 0; - Throwable throwable; // first throwable from sender, if any - ReceiverObject receiver; - StopCallback stopCallback; - Nursery assumeThreadSafe() @trusted shared nothrow { - return cast(Nursery)this; - } - } - - this() @safe shared { - import concurrency.utils : resetScheduler; - resetScheduler(); - with(assumeThreadSafe) mutex = new Mutex(); - } - - override bool stop() nothrow @trusted { - auto result = super.stop(); - - if (result) - (cast(shared)this).done(-1); - - return result; - } - - override bool stop() nothrow @trusted shared { - return (cast(Nursery)this).stop(); - } - - StopToken getStopToken() nothrow @trusted shared { - return StopToken(cast(Nursery)this); - } - - private auto getScheduler() nothrow @trusted shared { - return (cast()receiver).getScheduler(); - } - - private void setError(Throwable e, size_t id) nothrow @safe shared { - import core.atomic : cas; - with(assumeThreadSafe) cas(&throwable, cast(Throwable)null, e); // store throwable if not already - done(id); - stop(); - } - - private void done(size_t id) nothrow @trusted shared { - import std.algorithm : countUntil, remove; - import core.atomic : atomicOp; - - with (assumeThreadSafe) { - mutex.lock_nothrow(); - auto idx = operations.countUntil!(o => o.id == id); - if (idx != -1) - operations = operations.remove(idx); - bool isDone = atomicOp!"-="(busy,1) == 0 || operations.length == 0; - auto localReceiver = receiver; - auto localThrowable = throwable; - if (isDone) { - throwable = null; - receiver = null; - if (stopCallback) - stopCallback.dispose(); - stopCallback = null; - } - mutex.unlock_nothrow(); - - if (isDone && localReceiver !is null) { - if (localThrowable !is null) { - localReceiver.setError(localThrowable); - } else if (isStopRequested()) { - localReceiver.setDone(); - } else { - try { - localReceiver.setValue(); - } catch (Exception e) { - localReceiver.setError(e); - } - } - } - } - } - - void run(Sender)(Nullable!Sender sender) shared if (isSender!Sender) { - if (!sender.isNull) - run(sender.get()); - } - - void run(Sender)(Sender sender) shared @trusted { - import concepts; - static assert (models!(Sender, isSender)); - import std.typecons : Nullable; - import core.atomic : atomicOp, atomicLoad; - import concurrency.sender : connectHeap; - - static if (is(Sender == class) || is(Sender == interface)) - if (sender is null) - return; - - if (busy.atomicLoad() == 0) - throw new Exception("This nursery is already stopped, it cannot accept more work."); - - size_t id = atomicOp!"+="(counter, 1); - auto op = sender.connectHeap(NurseryReceiver!(Sender.Value)(this, id)); - - mutex.lock_nothrow(); - - operations ~= cast(shared) Node(op, id); - atomicOp!"+="(busy, 1); - bool hasStarted = this.receiver !is null; - mutex.unlock_nothrow(); - - if (hasStarted) - op.start(); - } - - auto connect(Receiver)(return Receiver receiver) @safe return scope { - return asShared.connect(receiver); - } - - private shared(Nursery) asShared() @trusted return scope { - return cast(shared)this; - } - - auto connect(Receiver)(return Receiver receiver) shared @trusted return scope { - final class ReceiverImpl : ReceiverObject { - Receiver receiver; - SchedulerObjectBase scheduler; - this(return Receiver receiver) @trusted { this.receiver = receiver; } - void setValue() @safe { receiver.setValue(); } - void setDone() nothrow @safe { receiver.setDone(); } - void setError(Throwable e) nothrow @safe { receiver.setError(e); } - SchedulerObjectBase getScheduler() nothrow @safe { - import concurrency.scheduler : toSchedulerObject; - if (scheduler is null) - scheduler = receiver.getScheduler().toSchedulerObject(); - return scheduler; - } - } - auto stopToken = receiver.getStopToken(); - auto cb = (()@trusted => stopToken.onStop(() shared nothrow @trusted => cast(void)this.stop()))(); - return NurseryOp(this, cb, new ReceiverImpl(receiver)); - } - - private void setReceiver(ReceiverObject r, StopCallback cb) nothrow @safe shared { - with(assumeThreadSafe) { - mutex.lock_nothrow(); - assert(this.receiver is null, "Cannot await a nursery twice."); - receiver = r; - stopCallback = cb; - auto ops = operations.dup(); - mutex.unlock_nothrow(); - - // start all work - foreach(op; ops) - op.start(); - } - } + import concurrency.sender : isSender, OperationalStateBase; + import core.sync.mutex : Mutex; + import concepts; + static assert(models!(typeof(this), isSender)); + + alias Value = void; + private { + Node[] operations; + struct Node { + OperationalStateBase state; + void start() @safe nothrow { + state.start(); + } + + size_t id; + } + + Mutex mutex; + shared size_t busy = + 1; // we start at 1 to denote the Nursery is open for tasks + shared size_t counter = 0; + Throwable throwable; // first throwable from sender, if any + ReceiverObject receiver; + StopCallback stopCallback; + Nursery assumeThreadSafe() @trusted shared nothrow { + return cast(Nursery) this; + } + } + + this() @safe shared { + import concurrency.utils : resetScheduler; + resetScheduler(); + with (assumeThreadSafe) mutex = new Mutex(); + } + + override bool stop() nothrow @trusted { + auto result = super.stop(); + + if (result) + (cast(shared) this).done(-1); + + return result; + } + + override bool stop() nothrow @trusted shared { + return (cast(Nursery) this).stop(); + } + + StopToken getStopToken() nothrow @trusted shared { + return StopToken(cast(Nursery) this); + } + + private auto getScheduler() nothrow @trusted shared { + return (cast() receiver).getScheduler(); + } + + private void setError(Throwable e, size_t id) nothrow @safe shared { + import core.atomic : cas; + with (assumeThreadSafe) cas(&throwable, cast(Throwable) null, + e); // store throwable if not already + done(id); + stop(); + } + + private void done(size_t id) nothrow @trusted shared { + import std.algorithm : countUntil, remove; + import core.atomic : atomicOp; + + with (assumeThreadSafe) { + mutex.lock_nothrow(); + auto idx = operations.countUntil!(o => o.id == id); + if (idx != -1) + operations = operations.remove(idx); + bool isDone = atomicOp!"-="(busy, 1) == 0 || operations.length == 0; + auto localReceiver = receiver; + auto localThrowable = throwable; + if (isDone) { + throwable = null; + receiver = null; + if (stopCallback) + stopCallback.dispose(); + stopCallback = null; + } + + mutex.unlock_nothrow(); + + if (isDone && localReceiver !is null) { + if (localThrowable !is null) { + localReceiver.setError(localThrowable); + } else if (isStopRequested()) { + localReceiver.setDone(); + } else { + try { + localReceiver.setValue(); + } catch (Exception e) { + localReceiver.setError(e); + } + } + } + } + } + + void run(Sender)(Nullable!Sender sender) shared if (isSender!Sender) { + if (!sender.isNull) + run(sender.get()); + } + + void run(Sender)(Sender sender) shared @trusted { + import concepts; + static assert(models!(Sender, isSender)); + import std.typecons : Nullable; + import core.atomic : atomicOp, atomicLoad; + import concurrency.sender : connectHeap; + + static if (is(Sender == class) || is(Sender == interface)) + if (sender is null) + return; + + if (busy.atomicLoad() == 0) + throw new Exception( + "This nursery is already stopped, it cannot accept more work."); + + size_t id = atomicOp!"+="(counter, 1); + auto op = sender.connectHeap(NurseryReceiver!(Sender.Value)(this, id)); + + mutex.lock_nothrow(); + + operations ~= cast(shared) Node(op, id); + atomicOp!"+="(busy, 1); + bool hasStarted = this.receiver !is null; + mutex.unlock_nothrow(); + + if (hasStarted) + op.start(); + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + return asShared.connect(receiver); + } + + private shared(Nursery) asShared() @trusted return scope { + return cast(shared) this; + } + + auto connect(Receiver)( + return Receiver receiver + ) shared @trusted return scope { + final class ReceiverImpl : ReceiverObject { + Receiver receiver; + SchedulerObjectBase scheduler; + this(return Receiver receiver) @trusted { + this.receiver = receiver; + } + + void setValue() @safe { + receiver.setValue(); + } + + void setDone() nothrow @safe { + receiver.setDone(); + } + + void setError(Throwable e) nothrow @safe { + receiver.setError(e); + } + + SchedulerObjectBase getScheduler() nothrow @safe { + import concurrency.scheduler : toSchedulerObject; + if (scheduler is null) + scheduler = receiver.getScheduler().toSchedulerObject(); + return scheduler; + } + } + + auto stopToken = receiver.getStopToken(); + auto cb = (() @trusted => stopToken + .onStop(() shared nothrow @trusted => cast(void) this.stop()))(); + return NurseryOp(this, cb, new ReceiverImpl(receiver)); + } + + private + void setReceiver(ReceiverObject r, StopCallback cb) nothrow @safe shared { + with (assumeThreadSafe) { + mutex.lock_nothrow(); + assert(this.receiver is null, "Cannot await a nursery twice."); + receiver = r; + stopCallback = cb; + auto ops = operations.dup(); + mutex.unlock_nothrow(); + + // start all work + foreach (op; ops) + op.start(); + } + } } private interface ReceiverObject { - void setValue() @safe; - void setDone() nothrow @safe; - void setError(Throwable e) nothrow @safe; - SchedulerObjectBase getScheduler() nothrow @safe; + void setValue() @safe; + void setDone() nothrow @safe; + void setError(Throwable e) nothrow @safe; + SchedulerObjectBase getScheduler() nothrow @safe; } private struct NurseryReceiver(Value) { - shared Nursery nursery; - size_t id; - this(shared Nursery nursery, size_t id) { - this.nursery = nursery; - this.id = id; - } - - static if (is(Value == void)) { - void setValue() shared @safe { - (cast() this).setDone(); - } - void setValue() @safe { - (cast() this).setDone(); - } - } else { - void setValue(Value val) shared @trusted { - (cast() this).setDone(); - } - void setValue(Value val) @safe { - nursery.done(id); - } - } - - void setDone() nothrow @safe { - nursery.done(id); - } - - void setError(Throwable e) nothrow @safe { - nursery.setError(e, id); - } - - auto getStopToken() @safe { - return nursery.getStopToken(); - } - - auto getScheduler() @safe { - return nursery.getScheduler(); - } + shared Nursery nursery; + size_t id; + this(shared Nursery nursery, size_t id) { + this.nursery = nursery; + this.id = id; + } + + static if (is(Value == void)) { + void setValue() shared @safe { + (cast() this).setDone(); + } + + void setValue() @safe { + (cast() this).setDone(); + } + } else { + void setValue(Value val) shared @trusted { + (cast() this).setDone(); + } + + void setValue(Value val) @safe { + nursery.done(id); + } + } + + void setDone() nothrow @safe { + nursery.done(id); + } + + void setError(Throwable e) nothrow @safe { + nursery.setError(e, id); + } + + auto getStopToken() @safe { + return nursery.getStopToken(); + } + + auto getScheduler() @safe { + return nursery.getScheduler(); + } } private struct NurseryOp { - shared Nursery nursery; - StopCallback cb; - ReceiverObject receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(return shared Nursery n, StopCallback cb, ReceiverObject r) @safe return scope { - nursery = n; - this.cb = cb; - receiver = r; - } - void start() nothrow scope @trusted { - import core.atomic : atomicLoad; - if (nursery.busy.atomicLoad == 0) - receiver.setDone(); - else - nursery.setReceiver(receiver, cb); - } + shared Nursery nursery; + StopCallback cb; + ReceiverObject receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(return shared Nursery n, StopCallback cb, + ReceiverObject r) @safe return scope { + nursery = n; + this.cb = cb; + receiver = r; + } + + void start() nothrow scope @trusted { + import core.atomic : atomicLoad; + if (nursery.busy.atomicLoad == 0) + receiver.setDone(); + else + nursery.setReceiver(receiver, cb); + } } diff --git a/source/concurrency/operations/completewithcancellation.d b/source/concurrency/operations/completewithcancellation.d index c7266a7..b470576 100644 --- a/source/concurrency/operations/completewithcancellation.d +++ b/source/concurrency/operations/completewithcancellation.d @@ -7,31 +7,38 @@ import concepts; import std.traits; auto completeWithCancellation(Sender)(Sender sender) { - return CompleteWithCancellationSender!(Sender)(sender); + return CompleteWithCancellationSender!(Sender)(sender); } private struct CompleteWithCancellationReceiver(Receiver) { - Receiver receiver; - void setValue() nothrow @safe { - receiver.setDone(); - } - void setDone() nothrow @safe { - receiver.setDone(); - } - void setError(Throwable e) nothrow @safe { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + void setValue() nothrow @safe { + receiver.setDone(); + } + + void setDone() nothrow @safe { + receiver.setDone(); + } + + void setError(Throwable e) nothrow @safe { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } struct CompleteWithCancellationSender(Sender) if (models!(Sender, isSender)) { - static assert (models!(typeof(this), isSender)); - static assert(is(Sender.Value == void), "Sender must produce void to be able to complete with cancellation."); - alias Value = void; - Sender sender; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - /// ensure NRVO - auto op = sender.connect(CompleteWithCancellationReceiver!(Receiver)(receiver)); - return op; - } + static assert(models!(typeof(this), isSender)); + static assert( + is(Sender.Value == void), + "Sender must produce void to be able to complete with cancellation." + ); + alias Value = void; + Sender sender; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + /// ensure NRVO + auto op = sender + .connect(CompleteWithCancellationReceiver!(Receiver)(receiver)); + return op; + } } diff --git a/source/concurrency/operations/completewitherror.d b/source/concurrency/operations/completewitherror.d index f036150..0718c2b 100644 --- a/source/concurrency/operations/completewitherror.d +++ b/source/concurrency/operations/completewitherror.d @@ -7,52 +7,58 @@ import concepts; import std.traits; auto completeWithError(Sender)(Sender sender, Throwable t) { - return CompleteWithErrorSender!(Sender)(sender, t); + return CompleteWithErrorSender!(Sender)(sender, t); } template isA(T) if (is(T == class)) { - bool isA(P)(auto ref P p) if (is(P == class)) { - return cast(T)p !is null; - } + bool isA(P)(auto ref P p) if (is(P == class)) { + return cast(T) p !is null; + } } template isNotA(T) if (is(T == class)) { - bool isNotA(P)(auto ref P p) { - return !p.isA!T; - } + bool isNotA(P)(auto ref P p) { + return !p.isA!T; + } } private struct CompleteWithErrorReceiver(Receiver) { - Receiver receiver; - Throwable error; - void setValue() nothrow @safe { - receiver.setError(error); - } - void setValue(T)(auto ref T t) nothrow @safe { - receiver.setError(error); - } - void setDone() nothrow @safe { - receiver.setError(error); - } - void setError(Throwable e) nothrow @safe { - if (error.isA!Exception && e.isNotA!Exception) { - // if e is a throwable and t just a regular exception, forward throwable - receiver.setError(e); - return; - } - receiver.setError(error); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + Throwable error; + void setValue() nothrow @safe { + receiver.setError(error); + } + + void setValue(T)(auto ref T t) nothrow @safe { + receiver.setError(error); + } + + void setDone() nothrow @safe { + receiver.setError(error); + } + + void setError(Throwable e) nothrow @safe { + if (error.isA!Exception && e.isNotA!Exception) { + // if e is a throwable and t just a regular exception, forward throwable + receiver.setError(e); + return; + } + + receiver.setError(error); + } + + mixin ForwardExtensionPoints!receiver; } struct CompleteWithErrorSender(Sender) if (models!(Sender, isSender)) { - static assert (models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - Throwable t; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - /// ensure NRVO - auto op = sender.connect(CompleteWithErrorReceiver!(Receiver)(receiver, t)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + Throwable t; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + /// ensure NRVO + auto op = + sender.connect(CompleteWithErrorReceiver!(Receiver)(receiver, t)); + return op; + } } diff --git a/source/concurrency/operations/forwardon.d b/source/concurrency/operations/forwardon.d index 30ddb63..c91ef2f 100644 --- a/source/concurrency/operations/forwardon.d +++ b/source/concurrency/operations/forwardon.d @@ -9,41 +9,45 @@ import concurrency.stoptoken; import concepts; auto forwardOn(Sender, Scheduler)(Sender sender, Scheduler scheduler) { - return ForwardOnSender!(Sender, Scheduler)(sender, scheduler); + return ForwardOnSender!(Sender, Scheduler)(sender, scheduler); } private struct ForwardOnReceiver(Receiver, Value, Scheduler) { - import concurrency.operations : via; - Receiver receiver; - Scheduler scheduler; - static if (is(Value == void)) { - void setValue() @safe { - VoidSender().via(scheduler.schedule()).connectHeap(receiver).start(); - } - } else { - void setValue(Value value) @safe { - just(value).via(scheduler.schedule()).connectHeap(receiver).start(); - } - } - void setDone() @safe nothrow { - DoneSender().via(scheduler.schedule()).connectHeap(receiver).start(); - } - void setError(Throwable e) @safe nothrow { - ErrorSender(e).via(scheduler.schedule()).connectHeap(receiver).start(); - } - mixin ForwardExtensionPoints!receiver; + import concurrency.operations : via; + Receiver receiver; + Scheduler scheduler; + static if (is(Value == void)) { + void setValue() @safe { + VoidSender().via(scheduler.schedule()).connectHeap(receiver) + .start(); + } + } else { + void setValue(Value value) @safe { + just(value).via(scheduler.schedule()).connectHeap(receiver).start(); + } + } + + void setDone() @safe nothrow { + DoneSender().via(scheduler.schedule()).connectHeap(receiver).start(); + } + + void setError(Throwable e) @safe nothrow { + ErrorSender(e).via(scheduler.schedule()).connectHeap(receiver).start(); + } + + mixin ForwardExtensionPoints!receiver; } struct ForwardOnSender(Sender, Scheduler) if (models!(Sender, isSender)) { - import std.traits : ReturnType; - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - Scheduler scheduler; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - alias R = ForwardOnReceiver!(Receiver, Sender.Value, Scheduler); - // ensure NRVO - auto op = sender.connect(R(receiver, scheduler)); - return op; - } + import std.traits : ReturnType; + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + Scheduler scheduler; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + alias R = ForwardOnReceiver!(Receiver, Sender.Value, Scheduler); + // ensure NRVO + auto op = sender.connect(R(receiver, scheduler)); + return op; + } } diff --git a/source/concurrency/operations/ignoreerror.d b/source/concurrency/operations/ignoreerror.d index 042e74f..7820852 100644 --- a/source/concurrency/operations/ignoreerror.d +++ b/source/concurrency/operations/ignoreerror.d @@ -8,36 +8,40 @@ import concepts; import std.traits; IESender!Sender ignoreError(Sender)(Sender sender) { - return IESender!Sender(sender); + return IESender!Sender(sender); } struct IESender(Sender) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = sender.connect(IEReceiver!(Sender.Value,Receiver)(receiver)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = sender.connect(IEReceiver!(Sender.Value, Receiver)(receiver)); + return op; + } } private struct IEReceiver(Value, Receiver) { - import concurrency.receiver : setValueOrError; - Receiver receiver; - static if (is(Value == void)) - void setValue() @safe nothrow { - receiver.setValueOrError(); - } - else - void setValue(Value value) @safe nothrow { - receiver.setValueOrError(value); - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setDone(); - } - mixin ForwardExtensionPoints!receiver; + import concurrency.receiver : setValueOrError; + Receiver receiver; + static if (is(Value == void)) + void setValue() @safe nothrow { + receiver.setValueOrError(); + } + + else + void setValue(Value value) @safe nothrow { + receiver.setValueOrError(value); + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setDone(); + } + + mixin ForwardExtensionPoints!receiver; } diff --git a/source/concurrency/operations/on.d b/source/concurrency/operations/on.d index c035bae..1b3a847 100644 --- a/source/concurrency/operations/on.d +++ b/source/concurrency/operations/on.d @@ -8,6 +8,6 @@ import concepts; import std.traits; auto on(Sender, Scheduler)(Sender sender, Scheduler scheduler) { - import concurrency.operations : via, withScheduler; - return sender.via(scheduler.schedule()).withScheduler(scheduler); + import concurrency.operations : via, withScheduler; + return sender.via(scheduler.schedule()).withScheduler(scheduler); } diff --git a/source/concurrency/operations/oncompletion.d b/source/concurrency/operations/oncompletion.d index fc600ca..339c574 100644 --- a/source/concurrency/operations/oncompletion.d +++ b/source/concurrency/operations/oncompletion.d @@ -8,42 +8,48 @@ import concepts; /// runs a side-effect whenever the underlying sender completes with value or cancellation auto onCompletion(Sender, SideEffect)(Sender sender, SideEffect effect) { - import concurrency.utils : isThreadSafeFunction; - static assert(isThreadSafeFunction!SideEffect); - return OnCompletionSender!(Sender, SideEffect)(sender, effect); + import concurrency.utils : isThreadSafeFunction; + static assert(isThreadSafeFunction!SideEffect); + return OnCompletionSender!(Sender, SideEffect)(sender, effect); } private struct OnCompletionReceiver(Value, SideEffect, Receiver) { - Receiver receiver; - SideEffect sideEffect; - static if (is(Value == void)) - void setValue() @safe { - sideEffect(); - receiver.setValue(); - } - else - void setValue(Value value) @safe { - sideEffect(); - receiver.setValue(value); - } - void setDone() @safe nothrow { - sideEffect(); - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + SideEffect sideEffect; + static if (is(Value == void)) + void setValue() @safe { + sideEffect(); + receiver.setValue(); + } + + else + void setValue(Value value) @safe { + sideEffect(); + receiver.setValue(value); + } + + void setDone() @safe nothrow { + sideEffect(); + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } struct OnCompletionSender(Sender, SideEffect) if (models!(Sender, isSender)) { - static assert (models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - SideEffect effect; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = sender.connect(OnCompletionReceiver!(Sender.Value, SideEffect, Receiver)(receiver, effect)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + SideEffect effect; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = sender.connect( + OnCompletionReceiver!(Sender.Value, SideEffect, Receiver)(receiver, + effect)); + return op; + } } diff --git a/source/concurrency/operations/onerror.d b/source/concurrency/operations/onerror.d index dfd56a4..b994c48 100644 --- a/source/concurrency/operations/onerror.d +++ b/source/concurrency/operations/onerror.d @@ -8,49 +8,56 @@ import concepts; /// runs a side-effect whenever the underlying sender errors auto onError(Sender, SideEffect)(Sender sender, SideEffect effect) { - import concurrency.utils : isThreadSafeFunction; - alias T = Exception; - static assert(isThreadSafeFunction!SideEffect); - return OnErrorSender!(Sender, SideEffect)(sender, effect); + import concurrency.utils : isThreadSafeFunction; + alias T = Exception; + static assert(isThreadSafeFunction!SideEffect); + return OnErrorSender!(Sender, SideEffect)(sender, effect); } private struct OnErrorReceiver(Value, SideEffect, Receiver) { - Receiver receiver; - SideEffect sideEffect; - static if (is(Value == void)) - void setValue() @safe { - receiver.setValue(); - } - else - void setValue(Value value) @safe { - receiver.setValue(value); - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable t) @safe nothrow { - if (auto e = cast(Exception)t) - { - try - sideEffect(e); - catch (Exception e2) { - receiver.setError(() @trusted { return Throwable.chainTogether(e2, e); } ()); - return; - } - } - receiver.setError(t); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + SideEffect sideEffect; + static if (is(Value == void)) + void setValue() @safe { + receiver.setValue(); + } + + else + void setValue(Value value) @safe { + receiver.setValue(value); + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable t) @safe nothrow { + if (auto e = cast(Exception) t) { + try + sideEffect(e); + catch (Exception e2) { + receiver.setError(() @trusted { + return Throwable.chainTogether(e2, e); + }()); + return; + } + } + + receiver.setError(t); + } + + mixin ForwardExtensionPoints!receiver; } struct OnErrorSender(Sender, SideEffect) if (models!(Sender, isSender)) { - static assert (models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - SideEffect effect; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = sender.connect(OnErrorReceiver!(Sender.Value, SideEffect, Receiver)(receiver, effect)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + SideEffect effect; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = sender.connect(OnErrorReceiver!(Sender.Value, SideEffect, + Receiver)(receiver, effect)); + return op; + } } diff --git a/source/concurrency/operations/onresult.d b/source/concurrency/operations/onresult.d index e474164..f3dec5f 100644 --- a/source/concurrency/operations/onresult.d +++ b/source/concurrency/operations/onresult.d @@ -8,62 +8,66 @@ import concepts; /// runs a side-effect whenever the underlying sender completes with value or cancellation auto onResult(Sender, SideEffect)(Sender sender, SideEffect effect) { - import concurrency.utils : isThreadSafeFunction; - static assert(isThreadSafeFunction!SideEffect); - return OnResultSender!(Sender, SideEffect)(sender, effect); + import concurrency.utils : isThreadSafeFunction; + static assert(isThreadSafeFunction!SideEffect); + return OnResultSender!(Sender, SideEffect)(sender, effect); } alias tee = onResult; private struct OnResultReceiver(Value, SideEffect, Receiver) { - Receiver receiver; - SideEffect sideEffect; - static if (is(Value == void)) - void setValue() @safe { - sideEffect(Result!void()); - receiver.setValue(); - } - else - void setValue(Value value) @safe { - sideEffect(Result!(Value)(value)); - receiver.setValue(value); - } + Receiver receiver; + SideEffect sideEffect; + static if (is(Value == void)) + void setValue() @safe { + sideEffect(Result!void()); + receiver.setValue(); + } - void setDone() @trusted nothrow { - try { - sideEffect(Result!(Value)(Cancelled())); - } catch (Throwable t) { - return receiver.setError(t); - } - receiver.setDone(); - } + else + void setValue(Value value) @safe { + sideEffect(Result!(Value)(value)); + receiver.setValue(value); + } - void setError(Throwable e) @trusted nothrow { - if (auto ex = cast(Exception) e) { - try { - sideEffect(Result!(Value)(ex)); - } catch (Exception e2) { - return receiver.setError(() @trusted { - return Throwable.chainTogether(e2, e); - }()); - } catch (Throwable t) { - return receiver.setError(t); - } - } - receiver.setError(e); - } + void setDone() @trusted nothrow { + try { + sideEffect(Result!(Value)(Cancelled())); + } catch (Throwable t) { + return receiver.setError(t); + } - mixin ForwardExtensionPoints!receiver; + receiver.setDone(); + } + + void setError(Throwable e) @trusted nothrow { + if (auto ex = cast(Exception) e) { + try { + sideEffect(Result!(Value)(ex)); + } catch (Exception e2) { + return receiver.setError(() @trusted { + return Throwable.chainTogether(e2, e); + }()); + } catch (Throwable t) { + return receiver.setError(t); + } + } + + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } struct OnResultSender(Sender, SideEffect) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - SideEffect effect; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = sender.connect(OnResultReceiver!(Sender.Value, SideEffect, Receiver)(receiver, effect)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + SideEffect effect; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = sender.connect(OnResultReceiver!(Sender.Value, SideEffect, + Receiver)(receiver, effect)); + return op; + } } diff --git a/source/concurrency/operations/ontermination.d b/source/concurrency/operations/ontermination.d index f075b20..6a728e9 100644 --- a/source/concurrency/operations/ontermination.d +++ b/source/concurrency/operations/ontermination.d @@ -8,43 +8,49 @@ import concepts; /// runs a side-effect whenever the underlying sender terminates auto onTermination(Sender, SideEffect)(Sender sender, SideEffect effect) { - import concurrency.utils : isThreadSafeFunction; - static assert(isThreadSafeFunction!SideEffect); - return OnTerminationSender!(Sender, SideEffect)(sender, effect); + import concurrency.utils : isThreadSafeFunction; + static assert(isThreadSafeFunction!SideEffect); + return OnTerminationSender!(Sender, SideEffect)(sender, effect); } private struct OnTerminationReceiver(Value, SideEffect, Receiver) { - Receiver receiver; - SideEffect sideEffect; - static if (is(Value == void)) - void setValue() @safe { - sideEffect(); - receiver.setValue(); - } - else - void setValue(Value value) @safe { - sideEffect(); - receiver.setValue(value); - } - void setDone() @safe nothrow { - sideEffect(); - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - sideEffect(); - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + SideEffect sideEffect; + static if (is(Value == void)) + void setValue() @safe { + sideEffect(); + receiver.setValue(); + } + + else + void setValue(Value value) @safe { + sideEffect(); + receiver.setValue(value); + } + + void setDone() @safe nothrow { + sideEffect(); + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + sideEffect(); + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } struct OnTerminationSender(Sender, SideEffect) if (models!(Sender, isSender)) { - static assert (models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - SideEffect effect; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = sender.connect(OnTerminationReceiver!(Sender.Value, SideEffect, Receiver)(receiver, effect)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + SideEffect effect; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = sender.connect( + OnTerminationReceiver!(Sender.Value, SideEffect, Receiver)(receiver, + effect)); + return op; + } } diff --git a/source/concurrency/operations/race.d b/source/concurrency/operations/race.d index 231c1ae..164daa5 100644 --- a/source/concurrency/operations/race.d +++ b/source/concurrency/operations/race.d @@ -12,213 +12,237 @@ import std.traits; /// if both error out the first exception is propagated, /// uses mir.algebraic if the Sender value types differ RaceSender!(Senders) race(Senders...)(Senders senders) { - return RaceSender!(Senders)(senders, false); + return RaceSender!(Senders)(senders, false); } private template Result(Senders...) if (Senders.length > 1) { - import concurrency.utils : NoVoid; - import mir.algebraic : Algebraic, Nullable; - import std.meta : staticMap, Filter, NoDuplicates; - alias getValue(Sender) = Sender.Value; - alias SenderValues = staticMap!(getValue, Senders); - alias NoVoidValueTypes = Filter!(NoVoid, SenderValues); - enum HasVoid = SenderValues.length != NoVoidValueTypes.length; - alias ValueTypes = NoDuplicates!(NoVoidValueTypes); - - static if (ValueTypes.length == 0) - alias Result = void; - else static if (ValueTypes.length == 1) - static if (HasVoid) - alias Result = Nullable!(ValueTypes[0]); - else - alias Result = ValueTypes[0]; - else { - alias Result = Algebraic!(ValueTypes); - } + import concurrency.utils : NoVoid; + import mir.algebraic : Algebraic, Nullable; + import std.meta : staticMap, Filter, NoDuplicates; + alias getValue(Sender) = Sender.Value; + alias SenderValues = staticMap!(getValue, Senders); + alias NoVoidValueTypes = Filter!(NoVoid, SenderValues); + enum HasVoid = SenderValues.length != NoVoidValueTypes.length; + alias ValueTypes = NoDuplicates!(NoVoidValueTypes); + + static if (ValueTypes.length == 0) + alias Result = void; + else static if (ValueTypes.length == 1) + static if (HasVoid) + alias Result = Nullable!(ValueTypes[0]); + else + alias Result = ValueTypes[0]; + else { + alias Result = Algebraic!(ValueTypes); + } } alias ArrayElement(T : P[], P) = P; import std.traits; private template Result(Senders...) if (Senders.length == 1) { - alias Result = ArrayElement!(Senders).Value; + alias Result = ArrayElement!(Senders).Value; } private struct RaceOp(Receiver, Senders...) { - import std.meta : staticMap; - alias R = Result!(Senders); - static if (Senders.length > 1) { - alias ElementReceiver(Sender) = RaceReceiver!(Receiver, Sender.Value, R); - alias ConnectResult(Sender) = OpType!(Sender, ElementReceiver!Sender); - alias Ops = staticMap!(ConnectResult, Senders); - } else { - alias ElementReceiver = RaceReceiver!(Receiver, R, R); - alias Ops = OpType!(ArrayElement!(Senders[0]), ElementReceiver)[]; - } - Receiver receiver; - State!R state; - Ops ops; - @disable this(this); - @disable this(ref return scope typeof(this) rhs); - this(Receiver receiver, return Senders senders, bool noDropouts) @trusted scope { - this.receiver = receiver; - state = new State!(R)(noDropouts); - static if (Senders.length > 1) { - foreach(i, Sender; Senders) { - ops[i] = senders[i].connect(ElementReceiver!(Sender)(receiver, state, Senders.length)); - } - } else { - ops.length = senders[0].length; - foreach(i; 0..senders[0].length) { - ops[i] = senders[0][i].connect(ElementReceiver(receiver, state, senders[0].length)); - } - } - } - void start() @trusted nothrow scope { - import concurrency.stoptoken : StopSource; - if (receiver.getStopToken().isStopRequested) { - receiver.setDone(); - return; - } - state.cb = receiver.getStopToken().onStop(cast(void delegate() nothrow @safe shared)&state.stop); // butt ugly cast, but it won't take the second overload - static if (Senders.length > 1) { - foreach(i, _; Senders) { - ops[i].start(); - } - } else { - foreach(i; 0..ops.length) { - ops[i].start(); - } - } - } + import std.meta : staticMap; + alias R = Result!(Senders); + static if (Senders.length > 1) { + alias ElementReceiver(Sender) = + RaceReceiver!(Receiver, Sender.Value, R); + alias ConnectResult(Sender) = OpType!(Sender, ElementReceiver!Sender); + alias Ops = staticMap!(ConnectResult, Senders); + } else { + alias ElementReceiver = RaceReceiver!(Receiver, R, R); + alias Ops = OpType!(ArrayElement!(Senders[0]), ElementReceiver)[]; + } + + Receiver receiver; + State!R state; + Ops ops; + @disable + this(this); + @disable + this(ref return scope typeof(this) rhs); + this(Receiver receiver, return Senders senders, + bool noDropouts) @trusted scope { + this.receiver = receiver; + state = new State!(R)(noDropouts); + static if (Senders.length > 1) { + foreach (i, Sender; Senders) { + ops[i] = senders[i].connect( + ElementReceiver!(Sender)(receiver, state, Senders.length)); + } + } else { + ops.length = senders[0].length; + foreach (i; 0 .. senders[0].length) { + ops[i] = senders[0][i].connect( + ElementReceiver(receiver, state, senders[0].length)); + } + } + } + + void start() @trusted nothrow scope { + import concurrency.stoptoken : StopSource; + if (receiver.getStopToken().isStopRequested) { + receiver.setDone(); + return; + } + + state.cb = receiver.getStopToken().onStop( + cast(void delegate() nothrow @safe shared) &state.stop + ); // butt ugly cast, but it won't take the second overload + static if (Senders.length > 1) { + foreach (i, _; Senders) { + ops[i].start(); + } + } else { + foreach (i; 0 .. ops.length) { + ops[i].start(); + } + } + } } import std.meta : allSatisfy, ApplyRight; struct RaceSender(Senders...) - if ((Senders.length > 1 && allSatisfy!(ApplyRight!(models, isSender), Senders)) || - (models!(ArrayElement!(Senders[0]), isSender))) { - static assert(models!(typeof(this), isSender)); - alias Value = Result!(Senders); - Senders senders; - bool noDropouts; // if true then we fail the moment one contender does, otherwise we keep running until one finishes - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = RaceOp!(Receiver, Senders)(receiver, senders, noDropouts); - return op; - } + if ((Senders.length > 1 + && allSatisfy!(ApplyRight!(models, isSender), Senders)) + || (models!(ArrayElement!(Senders[0]), isSender))) { + static assert(models!(typeof(this), isSender)); + alias Value = Result!(Senders); + Senders senders; + bool noDropouts; // if true then we fail the moment one contender does, otherwise we keep running until one finishes + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = RaceOp!(Receiver, Senders)(receiver, senders, noDropouts); + return op; + } } private class State(Value) : StopSource { - import concurrency.bitfield; - StopCallback cb; - shared SharedBitField!Flags bitfield; - static if (!is(Value == void)) - Value value; - Throwable exception; - bool noDropouts; - this(bool noDropouts) { - this.noDropouts = noDropouts; - } + import concurrency.bitfield; + StopCallback cb; + shared SharedBitField!Flags bitfield; + static if (!is(Value == void)) + Value value; + Throwable exception; + bool noDropouts; + this(bool noDropouts) { + this.noDropouts = noDropouts; + } } private enum Flags : size_t { - locked = 0x1, - value_produced = 0x2, - doneOrError_produced = 0x4 + locked = 0x1, + value_produced = 0x2, + doneOrError_produced = 0x4 } private enum Counter : size_t { - tick = 0x8, - mask = ~0x7 + tick = 0x8, + mask = ~0x7 } private struct RaceReceiver(Receiver, InnerValue, Value) { - import core.atomic : atomicOp, atomicLoad, MemoryOrder; - Receiver receiver; - State!(Value) state; - size_t senderCount; - auto getStopToken() { - return StopToken(state); - } - private bool isValueProduced(size_t state) { - return (state & Flags.value_produced) > 0; - } - private bool isDoneOrErrorProduced(size_t state) { - return (state & Flags.doneOrError_produced) > 0; - } - private bool isLast(size_t state) { - return (state >> 3) == atomicLoad(senderCount); - } - static if (!is(InnerValue == void)) - void setValue(InnerValue value) @safe nothrow { - with (state.bitfield.lock(Flags.value_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isValueProduced(oldState)) { - static if (is(InnerValue == Value)) - state.value = value; - else - state.value = Value(value); - release(); // must release before calling .stop - state.stop(); - } else - release(); - - if (last) - process(newState); - } - } - else - void setValue() @safe nothrow { - with (state.bitfield.update(Flags.value_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isValueProduced(oldState)) { - state.stop(); - } - if (last) - process(newState); - } - } - void setDone() @safe nothrow { - with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (state.noDropouts && !isDoneOrErrorProduced(oldState)) { - state.stop(); - } - if (last) - process(newState); - } - } - void setError(Throwable exception) @safe nothrow { - with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) { - state.exception = exception; - if (state.noDropouts) { - release(); // release before stop - state.stop(); - } - } - release(); - if (last) - process(newState); - } - } - private void process(size_t newState) { - import concurrency.receiver : setValueOrError; - - state.cb.dispose(); - if (receiver.getStopToken().isStopRequested) - receiver.setDone(); - else if (isValueProduced(newState)) { - static if (is(Value == void)) - receiver.setValueOrError(); - else - receiver.setValueOrError(state.value); - } else if (state.exception) - receiver.setError(state.exception); - else - receiver.setDone(); - } - mixin ForwardExtensionPoints!receiver; + import core.atomic : atomicOp, atomicLoad, MemoryOrder; + Receiver receiver; + State!(Value) state; + size_t senderCount; + auto getStopToken() { + return StopToken(state); + } + + private bool isValueProduced(size_t state) { + return (state & Flags.value_produced) > 0; + } + + private bool isDoneOrErrorProduced(size_t state) { + return (state & Flags.doneOrError_produced) > 0; + } + + private bool isLast(size_t state) { + return (state >> 3) == atomicLoad(senderCount); + } + + static if (!is(InnerValue == void)) + void setValue(InnerValue value) @safe nothrow { + with (state.bitfield.lock(Flags.value_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isValueProduced(oldState)) { + static if (is(InnerValue == Value)) + state.value = value; + else + state.value = Value(value); + release(); // must release before calling .stop + state.stop(); + } else + release(); + + if (last) + process(newState); + } + } + + else + void setValue() @safe nothrow { + with (state.bitfield.update(Flags.value_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isValueProduced(oldState)) { + state.stop(); + } + + if (last) + process(newState); + } + } + + void setDone() @safe nothrow { + with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (state.noDropouts && !isDoneOrErrorProduced(oldState)) { + state.stop(); + } + + if (last) + process(newState); + } + } + + void setError(Throwable exception) @safe nothrow { + with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) { + state.exception = exception; + if (state.noDropouts) { + release(); // release before stop + state.stop(); + } + } + + release(); + if (last) + process(newState); + } + } + + private void process(size_t newState) { + import concurrency.receiver : setValueOrError; + + state.cb.dispose(); + if (receiver.getStopToken().isStopRequested) + receiver.setDone(); + else if (isValueProduced(newState)) { + static if (is(Value == void)) + receiver.setValueOrError(); + else + receiver.setValueOrError(state.value); + } else if (state.exception) + receiver.setError(state.exception); + else + receiver.setDone(); + } + + mixin ForwardExtensionPoints!receiver; } diff --git a/source/concurrency/operations/raceall.d b/source/concurrency/operations/raceall.d index cf24c9d..18dad62 100644 --- a/source/concurrency/operations/raceall.d +++ b/source/concurrency/operations/raceall.d @@ -13,5 +13,5 @@ import std.traits; /// if both error out the first exception is propagated, /// uses mir.algebraic if the Sender value types differ RaceSender!(Senders) raceAll(Senders...)(Senders senders) { - return RaceSender!(Senders)(senders, true); + return RaceSender!(Senders)(senders, true); } diff --git a/source/concurrency/operations/repeat.d b/source/concurrency/operations/repeat.d index d42cf9a..29db82e 100644 --- a/source/concurrency/operations/repeat.d +++ b/source/concurrency/operations/repeat.d @@ -8,56 +8,64 @@ import concepts; import std.traits; auto repeat(Sender)(Sender sender) { - static assert(is(Sender.Value : void), "Can only repeat effectful Senders."); - return RepeatSender!(Sender)(sender); + static assert(is(Sender.Value : void), + "Can only repeat effectful Senders."); + return RepeatSender!(Sender)(sender); } private struct RepeatReceiver(Receiver) { - private Receiver receiver; - private void delegate() @safe scope reset; - void setValue() @safe { - reset(); - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + private Receiver receiver; + private void delegate() @safe scope reset; + void setValue() @safe { + reset(); + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } private struct RepeatOp(Receiver, Sender) { - alias Op = OpType!(Sender, RepeatReceiver!(Receiver)); - Sender sender; - Receiver receiver; - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Sender sender, Receiver receiver) @trusted scope { - this.sender = sender; - this.receiver = receiver; - } - void start() @trusted nothrow scope { - try { - reset(); - } catch (Exception e) { - receiver.setError(e); - } - } - private void reset() @trusted scope { - op = sender.connect(RepeatReceiver!(Receiver)(receiver, &reset)); - op.start(); - } + alias Op = OpType!(Sender, RepeatReceiver!(Receiver)); + Sender sender; + Receiver receiver; + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Sender sender, Receiver receiver) @trusted scope { + this.sender = sender; + this.receiver = receiver; + } + + void start() @trusted nothrow scope { + try { + reset(); + } catch (Exception e) { + receiver.setError(e); + } + } + + private void reset() @trusted scope { + op = sender.connect(RepeatReceiver!(Receiver)(receiver, &reset)); + op.start(); + } } struct RepeatSender(Sender) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = RepeatOp!(Receiver, Sender)(sender, receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = RepeatOp!(Receiver, Sender)(sender, receiver); + return op; + } } diff --git a/source/concurrency/operations/retry.d b/source/concurrency/operations/retry.d index b45d4a0..7578294 100644 --- a/source/concurrency/operations/retry.d +++ b/source/concurrency/operations/retry.d @@ -8,85 +8,103 @@ import concepts; import std.traits; struct Times { - int max = 5; - int n = 0; - bool failure(Throwable e) @safe nothrow { - n++; - return n >= max; - } + int max = 5; + int n = 0; + bool failure(Throwable e) @safe nothrow { + n++; + return n >= max; + } } - // Checks T is retry logic void checkRetryLogic(T)() { - T t = T.init; - alias Ret = typeof((() nothrow => t.failure(Throwable.init))()); - static assert(is(Ret == bool), T.stringof ~ ".failure(Throwable) should return a bool, but it returns a " ~ Ret.stringof); + T t = T.init; + alias Ret = typeof((() nothrow => t.failure(Throwable.init))()); + static assert( + is(Ret == bool), + T.stringof + ~ ".failure(Throwable) should return a bool, but it returns a " + ~ Ret.stringof + ); } enum isRetryLogic(T) = is(typeof(checkRetryLogic!T)); auto retry(Sender, Logic)(Sender sender, Logic logic) { - return RetrySender!(Sender, Logic)(sender, logic); + return RetrySender!(Sender, Logic)(sender, logic); } private struct RetryReceiver(Receiver, Sender, Logic) { - private { - Sender sender; - Receiver receiver; - Logic logic; - alias Value = Sender.Value; - } - static if (is(Value == void)) { - void setValue() @safe { - receiver.setValueOrError(); - } - } else { - void setValue(Value value) @safe { - receiver.setValueOrError(value); - } - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - if (logic.failure(e)) - receiver.setError(e); - else { - try { - // TODO: we connect on the heap here but we can probably do something smart... - // Maybe we can store the new Op in the RetryOp struct - // From what I gathered that is what libunifex does - sender.connectHeap(this).start(); - } catch (Exception e) { - receiver.setError(e); - } - } - } - mixin ForwardExtensionPoints!receiver; + private { + Sender sender; + Receiver receiver; + Logic logic; + alias Value = Sender.Value; + } + + static if (is(Value == void)) { + void setValue() @safe { + receiver.setValueOrError(); + } + } else { + void setValue(Value value) @safe { + receiver.setValueOrError(value); + } + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + if (logic.failure(e)) + receiver.setError(e); + else { + try { + // TODO: we connect on the heap here but we can probably do something smart... + // Maybe we can store the new Op in the RetryOp struct + // From what I gathered that is what libunifex does + sender.connectHeap(this).start(); + } catch (Exception e) { + receiver.setError(e); + } + } + } + + mixin ForwardExtensionPoints!receiver; } private struct RetryOp(Receiver, Sender, Logic) { - alias Op = OpType!(Sender, RetryReceiver!(Receiver, Sender, Logic)); - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Sender sender, return RetryReceiver!(Receiver, Sender, Logic) receiver) @trusted scope { - op = sender.connect(receiver); - } - void start() @trusted nothrow scope { - op.start(); - } + alias Op = OpType!(Sender, RetryReceiver!(Receiver, Sender, Logic)); + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this( + Sender sender, + return RetryReceiver!(Receiver, Sender, Logic) receiver + ) @trusted scope { + op = sender.connect(receiver); + } + + void start() @trusted nothrow scope { + op.start(); + } } -struct RetrySender(Sender, Logic) if (models!(Sender, isSender) && models!(Logic, isRetryLogic)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - Logic logic; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = RetryOp!(Receiver, Sender, Logic)(sender, RetryReceiver!(Receiver, Sender, Logic)(sender, receiver, logic)); - return op; - } +struct RetrySender(Sender, Logic) + if (models!(Sender, isSender) && models!(Logic, isRetryLogic)) { + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + Logic logic; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = RetryOp!(Receiver, Sender, Logic)( + sender, + RetryReceiver!(Receiver, Sender, Logic)(sender, receiver, logic) + ); + return op; + } } diff --git a/source/concurrency/operations/retrywhen.d b/source/concurrency/operations/retrywhen.d index b100bb7..fb48763 100644 --- a/source/concurrency/operations/retrywhen.d +++ b/source/concurrency/operations/retrywhen.d @@ -8,95 +8,117 @@ import concurrency.stoptoken; import concepts; import std.traits; -enum isRetryWhenLogic(T) = models!(typeof(T.init.failure(Exception.init)), isSender); +enum isRetryWhenLogic(T) = + models!(typeof(T.init.failure(Exception.init)), isSender); -auto retryWhen(Sender, Logic)(Sender sender, Logic logic) if (isRetryWhenLogic!Logic) { - return RetryWhenSender!(Sender, Logic)(sender, logic); +auto retryWhen(Sender, Logic)(Sender sender, Logic logic) + if (isRetryWhenLogic!Logic) { + return RetryWhenSender!(Sender, Logic)(sender, logic); } private struct TriggerReceiver(Sender, Receiver, Logic) { - alias Value = void; - private RetryWhenOp!(Sender, Receiver, Logic)* op; - void setValue() @safe { - op.sourceOp = op.sender.connect(SourceReceiver!(Sender, Receiver, Logic)(op)); - op.sourceOp.start(); - } - void setDone() @safe nothrow { - op.receiver.setDone(); - } - void setError(Throwable t) @safe nothrow { - op.receiver.setError(t); - } - private auto receiver() { - return op.receiver; - } - mixin ForwardExtensionPoints!(receiver); + alias Value = void; + private RetryWhenOp!(Sender, Receiver, Logic)* op; + void setValue() @safe { + op.sourceOp = + op.sender.connect(SourceReceiver!(Sender, Receiver, Logic)(op)); + op.sourceOp.start(); + } + + void setDone() @safe nothrow { + op.receiver.setDone(); + } + + void setError(Throwable t) @safe nothrow { + op.receiver.setError(t); + } + + private auto receiver() { + return op.receiver; + } + + mixin ForwardExtensionPoints!(receiver); } private struct SourceReceiver(Sender, Receiver, Logic) { - alias Value = Sender.Value; - private RetryWhenOp!(Sender, Receiver, Logic)* op; - static if (is(Value == void)) { - void setValue() @safe { - op.receiver.setValueOrError(); - } - } else { - void setValue(Value value) @safe { - op.receiver.setValueOrError(value); - } - } - void setDone() @safe nothrow { - op.receiver.setDone(); - } - void setError(Throwable t) @trusted nothrow { - if (auto ex = cast(Exception) t) { - try { - op.triggerOp = op.logic.failure(ex).connect(TriggerReceiver!(Sender, Receiver, Logic)(op)); - op.triggerOp.start(); - } catch (Throwable t2) { - op.receiver.setError(t2); - } - return; - } - op.receiver.setError(t); - } - private auto receiver() { - return op.receiver; - } - mixin ForwardExtensionPoints!(receiver); + alias Value = Sender.Value; + private RetryWhenOp!(Sender, Receiver, Logic)* op; + static if (is(Value == void)) { + void setValue() @safe { + op.receiver.setValueOrError(); + } + } else { + void setValue(Value value) @safe { + op.receiver.setValueOrError(value); + } + } + + void setDone() @safe nothrow { + op.receiver.setDone(); + } + + void setError(Throwable t) @trusted nothrow { + if (auto ex = cast(Exception) t) { + try { + op.triggerOp = + op.logic.failure(ex) + .connect(TriggerReceiver!(Sender, Receiver, Logic)(op)); + op.triggerOp.start(); + } catch (Throwable t2) { + op.receiver.setError(t2); + } + + return; + } + + op.receiver.setError(t); + } + + private auto receiver() { + return op.receiver; + } + + mixin ForwardExtensionPoints!(receiver); } private struct RetryWhenOp(Sender, Receiver, Logic) { - import std.traits : ReturnType; - alias SourceOp = OpType!(Sender, SourceReceiver!(Sender, Receiver, Logic)); - alias TriggerOp = OpType!(ReturnType!(Logic.failure), TriggerReceiver!(Sender, Receiver, Logic)); - Sender sender; - Receiver receiver; - Logic logic; - // TODO: this could probably be a Variant to safe some space - SourceOp sourceOp; - TriggerOp triggerOp; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(return Sender sender, Receiver receiver, Logic logic) @trusted scope { - this.sender = sender; - this.receiver = receiver; - this.logic = logic; - sourceOp = this.sender.connect(SourceReceiver!(Sender, Receiver, Logic)(&this)); - } - void start() @trusted nothrow scope { - sourceOp.start(); - } + import std.traits : ReturnType; + alias SourceOp = OpType!(Sender, SourceReceiver!(Sender, Receiver, Logic)); + alias TriggerOp = OpType!(ReturnType!(Logic.failure), + TriggerReceiver!(Sender, Receiver, Logic)); + Sender sender; + Receiver receiver; + Logic logic; + // TODO: this could probably be a Variant to safe some space + SourceOp sourceOp; + TriggerOp triggerOp; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(return Sender sender, Receiver receiver, Logic logic) @trusted scope { + this.sender = sender; + this.receiver = receiver; + this.logic = logic; + sourceOp = + this.sender + .connect(SourceReceiver!(Sender, Receiver, Logic)(&this)); + } + + void start() @trusted nothrow scope { + sourceOp.start(); + } } struct RetryWhenSender(Sender, Logic) if (isRetryWhenLogic!Logic) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - Logic logic; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = RetryWhenOp!(Sender, Receiver, Logic)(sender, receiver, logic); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + Logic logic; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = + RetryWhenOp!(Sender, Receiver, Logic)(sender, receiver, logic); + return op; + } } diff --git a/source/concurrency/operations/stopon.d b/source/concurrency/operations/stopon.d index e2be29f..8d979cd 100644 --- a/source/concurrency/operations/stopon.d +++ b/source/concurrency/operations/stopon.d @@ -8,44 +8,49 @@ import concepts; import std.traits; auto stopOn(Sender)(Sender sender, StopToken stopToken) { - return StopOn!(Sender)(sender, stopToken); + return StopOn!(Sender)(sender, stopToken); } private struct StopOnReceiver(Receiver, Value) { - private { - Receiver receiver; - StopToken stopToken; - } - static if (is(Value == void)) { - void setValue() @safe { - receiver.setValue(); - } - } else { - void setValue(Value value) @safe { - receiver.setValue(value); - } - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - auto getStopToken() nothrow @trusted { - return stopToken; - } - mixin ForwardExtensionPoints!receiver; + private { + Receiver receiver; + StopToken stopToken; + } + + static if (is(Value == void)) { + void setValue() @safe { + receiver.setValue(); + } + } else { + void setValue(Value value) @safe { + receiver.setValue(value); + } + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + auto getStopToken() nothrow @trusted { + return stopToken; + } + + mixin ForwardExtensionPoints!receiver; } struct StopOn(Sender) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - StopToken stopToken; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - alias R = StopOnReceiver!(Receiver, Sender.Value); - // ensure NRVO - auto op = sender.connect(R(receiver, stopToken)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + StopToken stopToken; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + alias R = StopOnReceiver!(Receiver, Sender.Value); + // ensure NRVO + auto op = sender.connect(R(receiver, stopToken)); + return op; + } } diff --git a/source/concurrency/operations/stopwhen.d b/source/concurrency/operations/stopwhen.d index b8fda7b..e327011 100644 --- a/source/concurrency/operations/stopwhen.d +++ b/source/concurrency/operations/stopwhen.d @@ -9,190 +9,214 @@ import concepts; import std.traits; /// stopWhen cancels the source when the trigger completes normally. If the either source or trigger completes with cancellation or with an error, the first one is propagates after both are completed. -StopWhenSender!(Sender, Trigger) stopWhen(Sender, Trigger)(Sender source, Trigger trigger) { - return StopWhenSender!(Sender, Trigger)(source, trigger); +StopWhenSender!(Sender, Trigger) stopWhen(Sender, Trigger)(Sender source, + Trigger trigger) { + return StopWhenSender!(Sender, Trigger)(source, trigger); } private struct StopWhenOp(Receiver, Sender, Trigger) { - alias SenderOp = OpType!(Sender, SourceReceiver!(Receiver, Sender.Value)); - alias TriggerOp = OpType!(Trigger, TriggerReceiver!(Receiver, Sender.Value)); - Receiver receiver; - State!(Sender.Value) state; - SenderOp sourceOp; - TriggerOp triggerOp; - @disable this(this); - @disable this(ref return scope typeof(this) rhs); - this(Receiver receiver, return Sender source, return Trigger trigger) @trusted scope { - this.receiver = receiver; - state = new State!(Sender.Value)(); - sourceOp = source.connect(SourceReceiver!(Receiver, Sender.Value)(receiver, state)); - triggerOp = trigger.connect(TriggerReceiver!(Receiver, Sender.Value)(receiver, state)); - } - void start() @trusted nothrow scope { - if (receiver.getStopToken().isStopRequested) { - receiver.setDone(); - return; - } - state.cb = receiver.getStopToken().onStop(cast(void delegate() nothrow @safe shared)&state.stop); // butt ugly cast, but it won't take the second overload - sourceOp.start; - triggerOp.start; - } + alias SenderOp = OpType!(Sender, SourceReceiver!(Receiver, Sender.Value)); + alias TriggerOp = + OpType!(Trigger, TriggerReceiver!(Receiver, Sender.Value)); + Receiver receiver; + State!(Sender.Value) state; + SenderOp sourceOp; + TriggerOp triggerOp; + @disable + this(this); + @disable + this(ref return scope typeof(this) rhs); + this(Receiver receiver, return Sender source, + return Trigger trigger) @trusted scope { + this.receiver = receiver; + state = new State!(Sender.Value)(); + sourceOp = source + .connect(SourceReceiver!(Receiver, Sender.Value)(receiver, state)); + triggerOp = trigger + .connect(TriggerReceiver!(Receiver, Sender.Value)(receiver, state)); + } + + void start() @trusted nothrow scope { + if (receiver.getStopToken().isStopRequested) { + receiver.setDone(); + return; + } + + state.cb = receiver.getStopToken().onStop( + cast(void delegate() nothrow @safe shared) &state.stop + ); // butt ugly cast, but it won't take the second overload + sourceOp.start; + triggerOp.start; + } } -struct StopWhenSender(Sender, Trigger) if (models!(Sender, isSender) && models!(Trigger, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - Trigger trigger; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = StopWhenOp!(Receiver, Sender, Trigger)(receiver, sender, trigger); - return op; - } +struct StopWhenSender(Sender, Trigger) + if (models!(Sender, isSender) && models!(Trigger, isSender)) { + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + Trigger trigger; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = + StopWhenOp!(Receiver, Sender, Trigger)(receiver, sender, trigger); + return op; + } } private class State(Value) : StopSource { - import concurrency.bitfield; - StopCallback cb; - shared SharedBitField!Flags bitfield; - static if (!is(Value == void)) - Value value; - Throwable exception; + import concurrency.bitfield; + StopCallback cb; + shared SharedBitField!Flags bitfield; + static if (!is(Value == void)) + Value value; + Throwable exception; } private enum Flags : size_t { - locked = 0x1, - value_produced = 0x2, - doneOrError_produced = 0x4, - tick = 0x8 + locked = 0x1, + value_produced = 0x2, + doneOrError_produced = 0x4, + tick = 0x8 } private enum Counter : size_t { - tick = 0x8 + tick = 0x8 } -private void process(State, Receiver)(State state, Receiver receiver, size_t newState) { - import concurrency.receiver : setValueOrError; - - state.cb.dispose(); - if (receiver.getStopToken().isStopRequested) - receiver.setDone(); - else if (isValueProduced(newState)) { - static if (__traits(compiles, state.value)) - receiver.setValueOrError(state.value); - else - receiver.setValueOrError(); - } else if (state.exception) - receiver.setError(state.exception); - else - receiver.setDone(); +private +void process(State, Receiver)(State state, Receiver receiver, size_t newState) { + import concurrency.receiver : setValueOrError; + + state.cb.dispose(); + if (receiver.getStopToken().isStopRequested) + receiver.setDone(); + else if (isValueProduced(newState)) { + static if (__traits(compiles, state.value)) + receiver.setValueOrError(state.value); + else + receiver.setValueOrError(); + } else if (state.exception) + receiver.setError(state.exception); + else + receiver.setDone(); } private bool isValueProduced(size_t state) @safe nothrow pure { - return (state & Flags.value_produced) > 0; + return (state & Flags.value_produced) > 0; } + private bool isDoneOrErrorProduced(size_t state) @safe nothrow pure { - return (state & Flags.doneOrError_produced) > 0; + return (state & Flags.doneOrError_produced) > 0; } + private bool isLast(size_t state) @safe nothrow pure { - return (state & Flags.tick) > 0; + return (state & Flags.tick) > 0; } private struct TriggerReceiver(Receiver, Value) { - Receiver receiver; - State!(Value) state; - auto getStopToken() { - return StopToken(state); - } - void setValue() @safe nothrow { - with (state.bitfield.update(Flags.tick)) { - if (!isLast(oldState)) - state.stop(); - else - state.process(receiver, newState); - } - } - void setDone() @safe nothrow { - with (state.bitfield.update(Flags.doneOrError_produced, Flags.tick)) { - if (!isLast(oldState)) - state.stop(); - else - state.process(receiver, newState); - } - } - void setError(Throwable exception) @safe nothrow { - with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) { - state.exception = exception; - release(); // release before stop - state.stop(); - } else { - release(); - if (last) - state.process(receiver, newState); - } - } - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + State!(Value) state; + auto getStopToken() { + return StopToken(state); + } + + void setValue() @safe nothrow { + with (state.bitfield.update(Flags.tick)) { + if (!isLast(oldState)) + state.stop(); + else + state.process(receiver, newState); + } + } + + void setDone() @safe nothrow { + with (state.bitfield.update(Flags.doneOrError_produced, Flags.tick)) { + if (!isLast(oldState)) + state.stop(); + else + state.process(receiver, newState); + } + } + + void setError(Throwable exception) @safe nothrow { + with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) { + state.exception = exception; + release(); // release before stop + state.stop(); + } else { + release(); + if (last) + state.process(receiver, newState); + } + } + } + + mixin ForwardExtensionPoints!receiver; } private struct SourceReceiver(Receiver, Value) { - import core.atomic : atomicOp, atomicLoad, MemoryOrder; - Receiver receiver; - State!(Value) state; - auto getStopToken() { - return StopToken(state); - } - static if (!is(Value == void)) - void setValue(Value value) @safe nothrow { - with (state.bitfield.update(Flags.value_produced | Flags.tick)) { - bool last = isLast(newState); - state.value = value; - - if (!last) - state.stop(); - else - if (isDoneOrErrorProduced(oldState)) - state.process(receiver, oldState); - else - state.process(receiver, newState); - } - } - else - void setValue() @safe nothrow { - with (state.bitfield.update(Flags.value_produced | Flags.tick)) { - bool last = isLast(newState); - if (!last) - state.stop(); - else - if (isDoneOrErrorProduced(oldState)) - state.process(receiver, oldState); - else - state.process(receiver, newState); - } - } - void setDone() @safe nothrow { - with (state.bitfield.update(Flags.doneOrError_produced | Flags.tick)) { - bool last = isLast(newState); - if (!last) - state.stop(); - else - state.process(receiver, newState); - } - } - void setError(Throwable exception) @safe nothrow { - with (state.bitfield.lock(Flags.doneOrError_produced | Flags.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) { - state.exception = exception; - } - release(); - if (!last) - state.stop(); - else - state.process(receiver, newState); - } - } - mixin ForwardExtensionPoints!receiver; + import core.atomic : atomicOp, atomicLoad, MemoryOrder; + Receiver receiver; + State!(Value) state; + auto getStopToken() { + return StopToken(state); + } + + static if (!is(Value == void)) + void setValue(Value value) @safe nothrow { + with (state.bitfield.update(Flags.value_produced | Flags.tick)) { + bool last = isLast(newState); + state.value = value; + + if (!last) + state.stop(); + else if (isDoneOrErrorProduced(oldState)) + state.process(receiver, oldState); + else + state.process(receiver, newState); + } + } + + else + void setValue() @safe nothrow { + with (state.bitfield.update(Flags.value_produced | Flags.tick)) { + bool last = isLast(newState); + if (!last) + state.stop(); + else if (isDoneOrErrorProduced(oldState)) + state.process(receiver, oldState); + else + state.process(receiver, newState); + } + } + + void setDone() @safe nothrow { + with (state.bitfield.update(Flags.doneOrError_produced | Flags.tick)) { + bool last = isLast(newState); + if (!last) + state.stop(); + else + state.process(receiver, newState); + } + } + + void setError(Throwable exception) @safe nothrow { + with (state.bitfield.lock(Flags.doneOrError_produced | Flags.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) { + state.exception = exception; + } + + release(); + if (!last) + state.stop(); + else + state.process(receiver, newState); + } + } + + mixin ForwardExtensionPoints!receiver; } diff --git a/source/concurrency/operations/then.d b/source/concurrency/operations/then.d index 9f564e9..b419228 100644 --- a/source/concurrency/operations/then.d +++ b/source/concurrency/operations/then.d @@ -7,61 +7,66 @@ import concurrency.stoptoken; import concepts; auto then(Sender, Fun)(Sender sender, Fun fun) { - import concurrency.utils; - static assert (isThreadSafeFunction!Fun); - return ThenSender!(Sender, Fun)(sender, fun); + import concurrency.utils; + static assert(isThreadSafeFunction!Fun); + return ThenSender!(Sender, Fun)(sender, fun); } private struct ThenReceiver(Receiver, Value, Fun) { - import std.traits : ReturnType; - Receiver receiver; - Fun fun; - static if (is(Value == void)) { - void setValue() @safe { - static if (is(ReturnType!Fun == void)) { - fun(); - receiver.setValue(); - } else - receiver.setValue(fun()); - } - } else { - import std.typecons : isTuple; - enum isExpandable = isTuple!Value && __traits(compiles, {fun(Value.init.expand);}); - void setValue(Value value) @safe { - static if (is(ReturnType!Fun == void)) { - static if (isExpandable) - fun(value.expand); - else - fun(value); - receiver.setValue(); - } else { - static if (isExpandable) - auto r = fun(value.expand); - else - auto r = fun(value); - receiver.setValue(r); - } - } - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + import std.traits : ReturnType; + Receiver receiver; + Fun fun; + static if (is(Value == void)) { + void setValue() @safe { + static if (is(ReturnType!Fun == void)) { + fun(); + receiver.setValue(); + } else + receiver.setValue(fun()); + } + } else { + import std.typecons : isTuple; + enum isExpandable = isTuple!Value && __traits(compiles, { + fun(Value.init.expand); + }); + void setValue(Value value) @safe { + static if (is(ReturnType!Fun == void)) { + static if (isExpandable) + fun(value.expand); + else + fun(value); + receiver.setValue(); + } else { + static if (isExpandable) + auto r = fun(value.expand); + else + auto r = fun(value); + receiver.setValue(r); + } + } + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } struct ThenSender(Sender, Fun) if (models!(Sender, isSender)) { - import std.traits : ReturnType; - static assert(models!(typeof(this), isSender)); - alias Value = ReturnType!fun; - Sender sender; - Fun fun; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - alias R = ThenReceiver!(Receiver, Sender.Value, Fun); - // ensure NRVO - auto op = sender.connect(R(receiver, fun)); - return op; - } + import std.traits : ReturnType; + static assert(models!(typeof(this), isSender)); + alias Value = ReturnType!fun; + Sender sender; + Fun fun; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + alias R = ThenReceiver!(Receiver, Sender.Value, Fun); + // ensure NRVO + auto op = sender.connect(R(receiver, fun)); + return op; + } } diff --git a/source/concurrency/operations/toshared.d b/source/concurrency/operations/toshared.d index 96e9c78..b4885d6 100644 --- a/source/concurrency/operations/toshared.d +++ b/source/concurrency/operations/toshared.d @@ -14,236 +14,270 @@ import mir.algebraic : Algebraic, Nullable, match; /// If an receiver is connected after the underlying Sender has already been completed, that receiver will have one of its termination functions called immediately. /// This operation is useful when you have multiple tasks that all depend on one shared task. It allows you to write the shared task as a regular Sender and simply apply a `.toShared`. auto toShared(Sender, Scheduler)(Sender sender, Scheduler scheduler) { - return new SharedSender!(Sender, Scheduler, ResetLogic.keepLatest)(sender, scheduler); + return new SharedSender!(Sender, Scheduler, + ResetLogic.keepLatest)(sender, scheduler); } auto toShared(Sender)(Sender sender) { - return new SharedSender!(Sender, NullScheduler, ResetLogic.keepLatest)(sender, NullScheduler()); + return new SharedSender!(Sender, NullScheduler, + ResetLogic.keepLatest)(sender, NullScheduler()); } enum ResetLogic { - keepLatest, - alwaysReset + keepLatest, + alwaysReset } -class SharedSender(Sender, Scheduler, ResetLogic resetLogic) if (models!(Sender, isSender)) { - import std.traits : ReturnType; - static assert(models!(typeof(this), isSender)); - alias Props = Properties!(Sender); - alias Value = Props.Value; - alias InternalValue = Props.InternalValue; - private { - Sender sender; - Scheduler scheduler; - SharedSenderState!(Sender) state; - void add(Props.DG dg) @safe nothrow { - with(state.counter.lock(0, Flags.tick)) { - if (was(Flags.completed)) { - InternalValue value = state.inst.value.get; - release(Flags.tick); // release early - dg(value); - } else { - if ((oldState >> 2) == 0) { - auto localState = new SharedSenderInstStateImpl!(Sender, Scheduler, resetLogic)(); - this.state.inst = localState; - release(); // release early - localState.dgs.pushBack(dg); - try { - localState.op = sender.connect(SharedSenderReceiver!(Sender, Scheduler, resetLogic)(&state, scheduler)); - } catch (Exception e) { - state.process!(resetLogic)(InternalValue(e)); - } - localState.op.start(); - } else { - auto localState = state.inst; - localState.dgs.pushBack(dg); - } - } - } - } - /// returns false if it is the last - bool remove(Props.DG dg) @safe nothrow { - with (state.counter.lock(0, 0, Flags.tick)) { - if (was(Flags.completed)) { - release(0-Flags.tick); // release early - return true; - } - if ((newState >> 2) == 0) { - auto localStopSource = state.inst; - release(); // release early - localStopSource.stop(); - return false; - } else { - auto localReceiver = state.inst; - release(); // release early - localReceiver.dgs.remove(dg); - return true; - } - } - } - } - bool isCompleted() @trusted { - import core.atomic : MemoryOrder; - return (state.counter.load!(MemoryOrder.acq) & Flags.completed) > 0; - } - void reset() @trusted { - with (state.counter.lock()) { - if (was(Flags.completed)) - release(Flags.completed); - } - } - this(Sender sender, Scheduler scheduler) { - this.sender = sender; - this.scheduler = scheduler; - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = SharedSenderOp!(Sender, Scheduler, resetLogic, Receiver)(this, receiver); - return op; - } +class SharedSender(Sender, Scheduler, ResetLogic resetLogic) + if (models!(Sender, isSender)) { + import std.traits : ReturnType; + static assert(models!(typeof(this), isSender)); + alias Props = Properties!(Sender); + alias Value = Props.Value; + alias InternalValue = Props.InternalValue; + private { + Sender sender; + Scheduler scheduler; + SharedSenderState!(Sender) state; + void add(Props.DG dg) @safe nothrow { + with (state.counter.lock(0, Flags.tick)) { + if (was(Flags.completed)) { + InternalValue value = state.inst.value.get; + release(Flags.tick); // release early + dg(value); + } else { + if ((oldState >> 2) == 0) { + auto localState = + new SharedSenderInstStateImpl!(Sender, Scheduler, + resetLogic)(); + this.state.inst = localState; + release(); // release early + localState.dgs.pushBack(dg); + try { + localState.op = sender.connect( + SharedSenderReceiver!( + Sender, Scheduler, resetLogic + )(&state, scheduler)); + } catch (Exception e) { + state.process!(resetLogic)(InternalValue(e)); + } + + localState.op.start(); + } else { + auto localState = state.inst; + localState.dgs.pushBack(dg); + } + } + } + } + + /// returns false if it is the last + bool remove(Props.DG dg) @safe nothrow { + with (state.counter.lock(0, 0, Flags.tick)) { + if (was(Flags.completed)) { + release(0 - Flags.tick); // release early + return true; + } + + if ((newState >> 2) == 0) { + auto localStopSource = state.inst; + release(); // release early + localStopSource.stop(); + return false; + } else { + auto localReceiver = state.inst; + release(); // release early + localReceiver.dgs.remove(dg); + return true; + } + } + } + } + + bool isCompleted() @trusted { + import core.atomic : MemoryOrder; + return (state.counter.load!(MemoryOrder.acq) & Flags.completed) > 0; + } + + void reset() @trusted { + with (state.counter.lock()) { + if (was(Flags.completed)) + release(Flags.completed); + } + } + + this(Sender sender, Scheduler scheduler) { + this.sender = sender; + this.scheduler = scheduler; + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = SharedSenderOp!(Sender, Scheduler, resetLogic, + Receiver)(this, receiver); + return op; + } } private enum Flags { - locked = 0x1, - completed = 0x2, - tick = 0x4 + locked = 0x1, + completed = 0x2, + tick = 0x4 } -private struct Done{} +private struct Done {} -private struct ValueRep{} +private struct ValueRep {} private template Properties(Sender) { - alias Value = Sender.Value; - static if (!is(Value == void)) - alias ValueRep = Value; - else - alias ValueRep = .ValueRep; - alias InternalValue = Algebraic!(Throwable, ValueRep, Done); - alias DG = void delegate(InternalValue) nothrow @safe shared; + alias Value = Sender.Value; + static if (!is(Value == void)) + alias ValueRep = Value; + else + alias ValueRep = .ValueRep; + alias InternalValue = Algebraic!(Throwable, ValueRep, Done); + alias DG = void delegate(InternalValue) nothrow @safe shared; } -private struct SharedSenderOp(Sender, Scheduler, ResetLogic resetLogic, Receiver) { - alias Props = Properties!(Sender); - SharedSender!(Sender, Scheduler, resetLogic) parent; - Receiver receiver; - StopCallback cb; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @trusted scope { - parent.add(&(cast(shared)this).onValue); - cb = receiver.getStopToken.onStop(&(cast(shared)this).onStop); - } - void onStop() nothrow @trusted shared { - with(unshared) { - /// If this is the last one connected, remove will return false, - /// stop the underlying sender and we will receive the setDone via - /// the onValue. - /// This is to ensure we always await the underlying sender for - /// completion. - if (parent.remove(&(cast(shared)this).onValue)) - receiver.setDone(); - } - } - void onValue(Props.InternalValue value) nothrow @safe shared { - with(unshared) { - value.match!((Props.ValueRep v){ - try { - static if (is(Props.Value == void)) - receiver.setValue(); - else - receiver.setValue(v); - } catch (Exception e) { - /// TODO: dispose needs to be called in all cases, except - /// this onValue can sometimes be called immediately, - /// leaving no room to set cb.dispose... - cb.dispose(); - receiver.setError(e); - } - }, (Throwable e){ - receiver.setError(e); - }, (Done d){ - receiver.setDone(); - }); - } - } - private auto ref unshared() @trusted nothrow shared { - return cast()this; - } +private +struct SharedSenderOp(Sender, Scheduler, ResetLogic resetLogic, Receiver) { + alias Props = Properties!(Sender); + SharedSender!(Sender, Scheduler, resetLogic) parent; + Receiver receiver; + StopCallback cb; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @trusted scope { + parent.add(&(cast(shared) this).onValue); + cb = receiver.getStopToken.onStop(&(cast(shared) this).onStop); + } + + void onStop() nothrow @trusted shared { + with (unshared) { + /// If this is the last one connected, remove will return false, + /// stop the underlying sender and we will receive the setDone via + /// the onValue. + /// This is to ensure we always await the underlying sender for + /// completion. + if (parent.remove(&(cast(shared) this).onValue)) + receiver.setDone(); + } + } + + void onValue(Props.InternalValue value) nothrow @safe shared { + with (unshared) { + value.match!((Props.ValueRep v) { + try { + static if (is(Props.Value == void)) + receiver.setValue(); + else + receiver.setValue(v); + } catch (Exception e) { + /// TODO: dispose needs to be called in all cases, except + /// this onValue can sometimes be called immediately, + /// leaving no room to set cb.dispose... + cb.dispose(); + receiver.setError(e); + } + }, (Throwable e) { + receiver.setError(e); + }, (Done d) { + receiver.setDone(); + }); + } + } + + private auto ref unshared() @trusted nothrow shared { + return cast() this; + } } private struct SharedSenderReceiver(Sender, Scheduler, ResetLogic resetLogic) { - alias InternalValue = Properties!(Sender).InternalValue; - alias ValueRep = Properties!(Sender).ValueRep; - SharedSenderState!(Sender)* state; - Scheduler scheduler; - static if (is(Sender.Value == void)) - void setValue() @safe { - process(InternalValue(ValueRep())); - } - else - void setValue(ValueRep v) @safe { - process(InternalValue(v)); - } - void setDone() @safe nothrow { - process(InternalValue(Done())); - } - void setError(Throwable e) @safe nothrow { - process(InternalValue(e)); - } - private void process(InternalValue v) @safe { - state.process!(resetLogic)(v); - } - StopToken getStopToken() @trusted nothrow { - return StopToken(state.inst); - } - Scheduler getScheduler() @safe nothrow scope { - return scheduler; - } + alias InternalValue = Properties!(Sender).InternalValue; + alias ValueRep = Properties!(Sender).ValueRep; + SharedSenderState!(Sender)* state; + Scheduler scheduler; + static if (is(Sender.Value == void)) + void setValue() @safe { + process(InternalValue(ValueRep())); + } + + else + void setValue(ValueRep v) @safe { + process(InternalValue(v)); + } + + void setDone() @safe nothrow { + process(InternalValue(Done())); + } + + void setError(Throwable e) @safe nothrow { + process(InternalValue(e)); + } + + private void process(InternalValue v) @safe { + state.process!(resetLogic)(v); + } + + StopToken getStopToken() @trusted nothrow { + return StopToken(state.inst); + } + + Scheduler getScheduler() @safe nothrow scope { + return scheduler; + } } private struct SharedSenderState(Sender) { - import concurrency.bitfield; + import concurrency.bitfield; - alias Props = Properties!(Sender); + alias Props = Properties!(Sender); - SharedSenderInstState!(Sender) inst; - SharedBitField!Flags counter; + SharedSenderInstState!(Sender) inst; + SharedBitField!Flags counter; } private template process(ResetLogic resetLogic) { - void process(State, InternalValue)(State state, InternalValue value) @safe { - state.inst.value = value; - static if (resetLogic == ResetLogic.alwaysReset) { - size_t updateFlag = 0; - } else { - size_t updateFlag = Flags.completed; - } - with(state.counter.lock(updateFlag)) { - auto localState = state.inst; - InternalValue v = localState.value.get; - release(oldState & (~0x3)); // release early and remove all ticks - if (localState.isStopRequested) - v = Done(); - foreach(dg; localState.dgs[]) - dg(v); - } - } + void process(State, InternalValue)(State state, InternalValue value) @safe { + state.inst.value = value; + static if (resetLogic == ResetLogic.alwaysReset) { + size_t updateFlag = 0; + } else { + size_t updateFlag = Flags.completed; + } + + with (state.counter.lock(updateFlag)) { + auto localState = state.inst; + InternalValue v = localState.value.get; + release(oldState & (~0x3)); // release early and remove all ticks + if (localState.isStopRequested) + v = Done(); + foreach (dg; localState.dgs[]) + dg(v); + } + } } private class SharedSenderInstState(Sender) : StopSource { - import concurrency.slist; - import std.traits : ReturnType; - alias Props = Properties!(Sender); - shared SList!(Props.DG) dgs; - Nullable!(Props.InternalValue) value; - this() { - this.dgs = new shared SList!(Props.DG); - } + import concurrency.slist; + import std.traits : ReturnType; + alias Props = Properties!(Sender); + shared SList!(Props.DG) dgs; + Nullable!(Props.InternalValue) value; + this() { + this.dgs = new shared SList!(Props.DG); + } } /// NOTE: this is a super class to break a dependency cycle of SharedSenderReceiver on itself (which it technically doesn't have but is probably too complex for the compiler) -private class SharedSenderInstStateImpl(Sender, Scheduler, ResetLogic resetLogic) : SharedSenderInstState!(Sender) { - alias Op = OpType!(Sender, SharedSenderReceiver!(Sender, Scheduler, resetLogic)); - Op op; +private class SharedSenderInstStateImpl( + Sender, + Scheduler, + ResetLogic resetLogic +) : SharedSenderInstState!(Sender) { + alias Op = + OpType!(Sender, SharedSenderReceiver!(Sender, Scheduler, resetLogic)); + Op op; } diff --git a/source/concurrency/operations/tosingleton.d b/source/concurrency/operations/tosingleton.d index bb0cde0..613438b 100644 --- a/source/concurrency/operations/tosingleton.d +++ b/source/concurrency/operations/tosingleton.d @@ -8,9 +8,11 @@ import concurrency.scheduler : NullScheduler; /// This is in contrast with `toShared` which requires an explicit `reset` before it restarts the underlying Sender, simply forwarding the last termination call until that time. /// This operation is useful if multiple things in your program depend on one single (sub)task running. auto toSingleton(Sender, Scheduler)(Sender sender, Scheduler scheduler) { - return new SharedSender!(Sender, Scheduler, ResetLogic.alwaysReset)(sender, scheduler); + return new SharedSender!(Sender, Scheduler, + ResetLogic.alwaysReset)(sender, scheduler); } auto toSingleton(Sender)(Sender sender) { - return new SharedSender!(Sender, NullScheduler, ResetLogic.alwaysReset)(sender, NullScheduler()); + return new SharedSender!(Sender, NullScheduler, + ResetLogic.alwaysReset)(sender, NullScheduler()); } diff --git a/source/concurrency/operations/via.d b/source/concurrency/operations/via.d index 279fceb..0dd128b 100644 --- a/source/concurrency/operations/via.d +++ b/source/concurrency/operations/via.d @@ -8,77 +8,87 @@ import concepts; import std.traits; auto via(SenderA, SenderB)(SenderA a, SenderB b) { - return ViaSender!(SenderA, SenderB)(a,b); + return ViaSender!(SenderA, SenderB)(a, b); } private enum NoVoid(T) = !is(T == void); private struct ViaAReceiver(ValueB, ValueA, Receiver) { - ValueB valueB; - Receiver receiver; - static if (!is(ValueA == void)) - void setValue(ValueA valueA) @safe { - import std.typecons : tuple; - receiver.setValue(tuple(valueB, valueA)); - } - else - void setValue() @safe { - receiver.setValue(valueB); - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + ValueB valueB; + Receiver receiver; + static if (!is(ValueA == void)) + void setValue(ValueA valueA) @safe { + import std.typecons : tuple; + receiver.setValue(tuple(valueB, valueA)); + } + + else + void setValue() @safe { + receiver.setValue(valueB); + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } private struct ViaBReceiver(SenderA, ValueB, Receiver) { - SenderA senderA; - Receiver receiver; - static if (!is(ValueB == void)) { - // OpType!(SenderA, ViaAReceiver!(ValueB, SenderA.Value, Receiver)) op; - void setValue(ValueB val) @safe { - // TODO: tried to allocate this on the stack, but failed... - auto op = senderA.connectHeap(ViaAReceiver!(ValueB, SenderA.Value, Receiver)(val, receiver)); - op.start(); - } - } else { - // OpType!(SenderA, Receiver) op; - void setValue() @safe { - // TODO: tried to allocate this on the stack, but failed... - auto op = senderA.connectHeap(receiver); - op.start(); - } - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; -} + SenderA senderA; + Receiver receiver; + static if (!is(ValueB == void)) { + // OpType!(SenderA, ViaAReceiver!(ValueB, SenderA.Value, Receiver)) op; + void setValue(ValueB val) @safe { + // TODO: tried to allocate this on the stack, but failed... + auto op = senderA.connectHeap( + ViaAReceiver!(ValueB, SenderA.Value, Receiver)(val, receiver)); + op.start(); + } + } else { + // OpType!(SenderA, Receiver) op; + void setValue() @safe { + // TODO: tried to allocate this on the stack, but failed... + auto op = senderA.connectHeap(receiver); + op.start(); + } + } -struct ViaSender(SenderA, SenderB) if (models!(SenderA, isSender) && models!(SenderB, isSender)) { - static assert(models!(typeof(this), isSender)); - import std.meta : Filter, AliasSeq; - SenderA senderA; - SenderB senderB; - alias Values = Filter!(NoVoid, AliasSeq!(SenderA.Value, SenderB.Value)); - static if (Values.length == 0) - alias Value = void; - else static if (Values.length == 1) - alias Value = Values[0]; - else { - import std.typecons : Tuple; - alias Value = Tuple!Values; - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = senderB.connect(ViaBReceiver!(SenderA, SenderB.Value, Receiver)(senderA, receiver)); - return op; - } + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } +struct ViaSender(SenderA, SenderB) + if (models!(SenderA, isSender) && models!(SenderB, isSender)) { + static assert(models!(typeof(this), isSender)); + import std.meta : Filter, AliasSeq; + SenderA senderA; + SenderB senderB; + alias Values = Filter!(NoVoid, AliasSeq!(SenderA.Value, SenderB.Value)); + static if (Values.length == 0) + alias Value = void; + else static if (Values.length == 1) + alias Value = Values[0]; + else { + import std.typecons : Tuple; + alias Value = Tuple!Values; + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = senderB.connect( + ViaBReceiver!(SenderA, SenderB.Value, Receiver)(senderA, receiver)); + return op; + } +} diff --git a/source/concurrency/operations/whenall.d b/source/concurrency/operations/whenall.d index 342c9b7..9e1aa4d 100644 --- a/source/concurrency/operations/whenall.d +++ b/source/concurrency/operations/whenall.d @@ -9,241 +9,268 @@ import std.traits; import concurrency.utils : spin_yield, casWeak; WhenAllSender!(Senders) whenAll(Senders...)(Senders senders) { - return WhenAllSender!(Senders)(senders); + return WhenAllSender!(Senders)(senders); } private enum Flags : size_t { - locked = 0x1, - value_produced = 0x2, - doneOrError_produced = 0x4 + locked = 0x1, + value_produced = 0x2, + doneOrError_produced = 0x4 } private enum Counter : size_t { - tick = 0x8 + tick = 0x8 } template GetSenderValues(Senders...) { - import std.meta; - alias SenderValue(T) = T.Value; - alias GetSenderValues = staticMap!(SenderValue, Senders); + import std.meta; + alias SenderValue(T) = T.Value; + alias GetSenderValues = staticMap!(SenderValue, Senders); } private template WhenAllResult(Senders...) if (Senders.length > 1) { - import std.meta; - import std.typecons; - import mir.algebraic : Algebraic, Nullable; - import concurrency.utils : NoVoid; - template Cummulative(size_t count, Ts...) { - static if (Ts.length > 0) { - enum head = count + Ts[0]; - static if (Ts.length == 1) - alias Cummulative = AliasSeq!(head); - else static if (Ts.length > 1) - alias Cummulative = AliasSeq!(head, Cummulative!(head, Ts[1..$])); - } else { - alias Cummulative = AliasSeq!(); - } - } - alias SenderValues = GetSenderValues!(Senders); - alias ValueTypes = Filter!(NoVoid, SenderValues); - static if (ValueTypes.length > 1) - alias Values = Tuple!(Filter!(NoVoid, SenderValues)); - else static if (ValueTypes.length == 1) - alias Values = ValueTypes[0]; - alias Indexes = Cummulative!(0, staticMap!(NoVoid, SenderValues)); - - static if (ValueTypes.length > 0) { - struct WhenAllResult { - Values values; - void setValue(T)(T t, size_t index) { - switch (index) { - foreach(idx, I; Indexes) { - case idx: - static if (ValueTypes.length == 1) - values = t; - else static if (is(typeof(values[I-1]) == T)) - values[I-1] = t; - return; - } - default: assert(false, "out of bounds"); - } - } - } - } else { - struct WhenAllResult { - } - } + import std.meta; + import std.typecons; + import mir.algebraic : Algebraic, Nullable; + import concurrency.utils : NoVoid; + template Cummulative(size_t count, Ts...) { + static if (Ts.length > 0) { + enum head = count + Ts[0]; + static if (Ts.length == 1) + alias Cummulative = AliasSeq!(head); + else static if (Ts.length > 1) + alias Cummulative = + AliasSeq!(head, Cummulative!(head, Ts[1 .. $])); + } else { + alias Cummulative = AliasSeq!(); + } + } + + alias SenderValues = GetSenderValues!(Senders); + alias ValueTypes = Filter!(NoVoid, SenderValues); + static if (ValueTypes.length > 1) + alias Values = Tuple!(Filter!(NoVoid, SenderValues)); + else static if (ValueTypes.length == 1) + alias Values = ValueTypes[0]; + alias Indexes = Cummulative!(0, staticMap!(NoVoid, SenderValues)); + + static if (ValueTypes.length > 0) { + struct WhenAllResult { + Values values; + void setValue(T)(T t, size_t index) { + switch (index) { + foreach (idx, I; Indexes) { + case idx: + static if (ValueTypes.length == 1) + values = t; + else static if (is(typeof(values[I - 1]) == T)) + values[I - 1] = t; + return; + } + + default: + assert(false, "out of bounds"); + } + } + } + } else { + struct WhenAllResult {} + } } alias ArrayElement(T : P[], P) = P; private template WhenAllResult(Senders...) if (Senders.length == 1) { - alias Element = ArrayElement!(Senders).Value; - static if (is(Element : void)) { - struct WhenAllResult {} - } else { - struct WhenAllResult { - Element[] values; - void setValue(Element)(Element elem, size_t index) { - values[index] = elem; - } - } - } + alias Element = ArrayElement!(Senders).Value; + static if (is(Element : void)) { + struct WhenAllResult {} + } else { + struct WhenAllResult { + Element[] values; + void setValue(Element)(Element elem, size_t index) { + values[index] = elem; + } + } + } } private struct WhenAllOp(Receiver, Senders...) { - import std.meta : staticMap; - alias R = WhenAllResult!(Senders); - static if (Senders.length > 1) { - alias ElementReceiver(Sender) = WhenAllReceiver!(Receiver, Sender.Value, R); - alias ConnectResult(Sender) = OpType!(Sender, ElementReceiver!Sender); - alias Ops = staticMap!(ConnectResult, Senders); - } else { - alias ElementReceiver = WhenAllReceiver!(Receiver, ArrayElement!(Senders).Value, R); - alias Ops = OpType!(ArrayElement!(Senders), ElementReceiver)[]; - } - Receiver receiver; - WhenAllState!R state; - Ops ops; - @disable this(this); - @disable this(ref return scope typeof(this) rhs); - this(return Receiver receiver, return Senders senders) @trusted scope return { - this.receiver = receiver; - state = new WhenAllState!R(); - static if (Senders.length > 1) { - foreach(i, Sender; Senders) { - ops[i] = senders[i].connect(WhenAllReceiver!(Receiver, Sender.Value, R)(receiver, state, i, Senders.length)); - } - } else { - static if (!is(ArrayElement!(Senders).Value : void)) - state.value.values.length = senders[0].length; - ops.length = senders[0].length; - foreach(i; 0..senders[0].length) { - ops[i] = senders[0][i].connect(WhenAllReceiver!(Receiver, ArrayElement!(Senders).Value, R)(receiver, state, i, senders[0].length)); - } - } - } - void start() @trusted nothrow scope { - import concurrency.stoptoken : StopSource; - if (receiver.getStopToken().isStopRequested) { - receiver.setDone(); - return; - } - state.cb = receiver.getStopToken().onStop(cast(void delegate() nothrow @safe shared)&state.stop); // butt ugly cast, but it won't take the second overload - static if (Senders.length > 1) { - foreach(i, _; Senders) { - ops[i].start(); - } - } else { - foreach(i; 0..ops.length) { - ops[i].start(); - } - } - } + import std.meta : staticMap; + alias R = WhenAllResult!(Senders); + static if (Senders.length > 1) { + alias ElementReceiver(Sender) = + WhenAllReceiver!(Receiver, Sender.Value, R); + alias ConnectResult(Sender) = OpType!(Sender, ElementReceiver!Sender); + alias Ops = staticMap!(ConnectResult, Senders); + } else { + alias ElementReceiver = + WhenAllReceiver!(Receiver, ArrayElement!(Senders).Value, R); + alias Ops = OpType!(ArrayElement!(Senders), ElementReceiver)[]; + } + + Receiver receiver; + WhenAllState!R state; + Ops ops; + @disable + this(this); + @disable + this(ref return scope typeof(this) rhs); + this(return Receiver receiver, + return Senders senders) @trusted scope return { + this.receiver = receiver; + state = new WhenAllState!R(); + static if (Senders.length > 1) { + foreach (i, Sender; Senders) { + ops[i] = senders[i].connect( + WhenAllReceiver!(Receiver, Sender.Value, + R)(receiver, state, i, Senders.length)); + } + } else { + static if (!is(ArrayElement!(Senders).Value : void)) + state.value.values.length = senders[0].length; + ops.length = senders[0].length; + foreach (i; 0 .. senders[0].length) { + ops[i] = senders[0][i].connect( + WhenAllReceiver!(Receiver, ArrayElement!(Senders).Value, + R)(receiver, state, i, senders[0].length)); + } + } + } + + void start() @trusted nothrow scope { + import concurrency.stoptoken : StopSource; + if (receiver.getStopToken().isStopRequested) { + receiver.setDone(); + return; + } + + state.cb = receiver.getStopToken().onStop( + cast(void delegate() nothrow @safe shared) &state.stop + ); // butt ugly cast, but it won't take the second overload + static if (Senders.length > 1) { + foreach (i, _; Senders) { + ops[i].start(); + } + } else { + foreach (i; 0 .. ops.length) { + ops[i].start(); + } + } + } } import std.meta : allSatisfy, ApplyRight; struct WhenAllSender(Senders...) - if ((Senders.length > 1 && allSatisfy!(ApplyRight!(models, isSender), Senders)) || - (models!(ArrayElement!(Senders[0]), isSender))) { - alias Result = WhenAllResult!(Senders); - static if (hasMember!(Result, "values")) - alias Value = typeof(Result.values); - else - alias Value = void; - Senders senders; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = WhenAllOp!(Receiver, Senders)(receiver, senders); - return op; - } + if ((Senders.length > 1 + && allSatisfy!(ApplyRight!(models, isSender), Senders)) + || (models!(ArrayElement!(Senders[0]), isSender))) { + alias Result = WhenAllResult!(Senders); + static if (hasMember!(Result, "values")) + alias Value = typeof(Result.values); + else + alias Value = void; + Senders senders; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = WhenAllOp!(Receiver, Senders)(receiver, senders); + return op; + } } private class WhenAllState(Value) : StopSource { - import concurrency.bitfield; - StopCallback cb; - static if (is(typeof(Value.values))) - Value value; - Throwable exception; - shared SharedBitField!Flags bitfield; + import concurrency.bitfield; + StopCallback cb; + static if (is(typeof(Value.values))) + Value value; + Throwable exception; + shared SharedBitField!Flags bitfield; } private struct WhenAllReceiver(Receiver, InnerValue, Value) { - import core.atomic : atomicOp, atomicLoad, MemoryOrder; - Receiver receiver; - WhenAllState!(Value) state; - size_t senderIndex; - size_t senderCount; - auto getStopToken() { - return StopToken(state); - } - private bool isValueProduced(size_t state) { - return (state & Flags.value_produced) > 0; - } - private bool isDoneOrErrorProduced(size_t state) { - return (state & Flags.doneOrError_produced) > 0; - } - private bool isLast(size_t state) { - return (state >> 3) == atomicLoad(senderCount); - } - static if (!is(InnerValue == void)) - void setValue(InnerValue value) @safe { - with (state.bitfield.lock(Flags.value_produced, Counter.tick)) { - bool last = isLast(newState); - state.value.setValue(value, senderIndex); - release(); - if (last) - process(newState); - } - } - else - void setValue() @safe { - with (state.bitfield.update(Flags.value_produced, Counter.tick)) { - bool last = isLast(newState); - if (last) - process(newState); - } - } - void setDone() @safe nothrow { - with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) - state.stop(); - if (last) - process(newState); - } - } - void setError(Throwable exception) @safe nothrow { - with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) { - state.exception = exception; - release(); // must release before calling .stop - state.stop(); - } else - release(); - if (last) - process(newState); - } - } - private void process(size_t newState) { - state.cb.dispose(); - - if (receiver.getStopToken().isStopRequested) - receiver.setDone(); - else if (isDoneOrErrorProduced(newState)) { - if (state.exception) - receiver.setError(state.exception); - else - receiver.setDone(); - } else { - import concurrency.receiver : setValueOrError; - static if (is(typeof(Value.values))) - receiver.setValueOrError(state.value.values); - else - receiver.setValueOrError(); - } - } - mixin ForwardExtensionPoints!receiver; + import core.atomic : atomicOp, atomicLoad, MemoryOrder; + Receiver receiver; + WhenAllState!(Value) state; + size_t senderIndex; + size_t senderCount; + auto getStopToken() { + return StopToken(state); + } + + private bool isValueProduced(size_t state) { + return (state & Flags.value_produced) > 0; + } + + private bool isDoneOrErrorProduced(size_t state) { + return (state & Flags.doneOrError_produced) > 0; + } + + private bool isLast(size_t state) { + return (state >> 3) == atomicLoad(senderCount); + } + + static if (!is(InnerValue == void)) + void setValue(InnerValue value) @safe { + with (state.bitfield.lock(Flags.value_produced, Counter.tick)) { + bool last = isLast(newState); + state.value.setValue(value, senderIndex); + release(); + if (last) + process(newState); + } + } + + else + void setValue() @safe { + with (state.bitfield.update(Flags.value_produced, Counter.tick)) { + bool last = isLast(newState); + if (last) + process(newState); + } + } + + void setDone() @safe nothrow { + with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) + state.stop(); + if (last) + process(newState); + } + } + + void setError(Throwable exception) @safe nothrow { + with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) { + state.exception = exception; + release(); // must release before calling .stop + state.stop(); + } else + release(); + if (last) + process(newState); + } + } + + private void process(size_t newState) { + state.cb.dispose(); + + if (receiver.getStopToken().isStopRequested) + receiver.setDone(); + else if (isDoneOrErrorProduced(newState)) { + if (state.exception) + receiver.setError(state.exception); + else + receiver.setDone(); + } else { + import concurrency.receiver : setValueOrError; + static if (is(typeof(Value.values))) + receiver.setValueOrError(state.value.values); + else + receiver.setValueOrError(); + } + } + + mixin ForwardExtensionPoints!receiver; } diff --git a/source/concurrency/operations/withchild.d b/source/concurrency/operations/withchild.d index aab2bef..3ee5d38 100644 --- a/source/concurrency/operations/withchild.d +++ b/source/concurrency/operations/withchild.d @@ -8,19 +8,24 @@ import concepts; import std.traits; import concurrency.utils : spin_yield, casWeak; -WithChildSender!(SenderParent, SenderChild) withChild(SenderParent, SenderChild)(SenderParent a, SenderChild b) { - return WithChildSender!(SenderParent, SenderChild)(a, b); +WithChildSender!(SenderParent, SenderChild) withChild( + SenderParent, SenderChild +)(SenderParent a, SenderChild b) { + return WithChildSender!(SenderParent, SenderChild)(a, b); } -struct WithChildSender(SenderParent, SenderChild) if (models!(SenderParent, isSender) && models!(SenderChild, isSender)) { - import concurrency.operations.whenall; - alias Value = WhenAllSender!(SenderChild, SenderParent).Value; - SenderParent a; - SenderChild b; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - import concurrency.operations.stopon; - // ensure NRVO - auto op = whenAll(b.stopOn(receiver.getStopToken), a).stopOn(StopToken()).connect(receiver); - return op; - } +struct WithChildSender(SenderParent, SenderChild) + if (models!(SenderParent, isSender) && models!(SenderChild, isSender)) { + import concurrency.operations.whenall; + alias Value = WhenAllSender!(SenderChild, SenderParent).Value; + SenderParent a; + SenderChild b; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + import concurrency.operations.stopon; + // ensure NRVO + auto op = + whenAll(b.stopOn(receiver.getStopToken), a).stopOn(StopToken()) + .connect(receiver); + return op; + } } diff --git a/source/concurrency/operations/withscheduler.d b/source/concurrency/operations/withscheduler.d index 669fe85..7895d3a 100644 --- a/source/concurrency/operations/withscheduler.d +++ b/source/concurrency/operations/withscheduler.d @@ -8,43 +8,47 @@ import concepts; import std.traits; auto withScheduler(Sender, Scheduler)(Sender sender, Scheduler scheduler) { - return WithSchedulerSender!(Sender, Scheduler)(sender, scheduler); + return WithSchedulerSender!(Sender, Scheduler)(sender, scheduler); } private struct WithSchedulerReceiver(Receiver, Value, Scheduler) { - Receiver receiver; - Scheduler scheduler; - static if (is(Value == void)) { - void setValue() @safe { - receiver.setValue(); - } - } else { - void setValue(Value value) @safe { - receiver.setValue(value); - } - } - void setDone() @safe nothrow { - receiver.setDone(); - } - void setError(Throwable e) @safe nothrow { - receiver.setError(e); - } - auto getScheduler() @safe nothrow { - import concurrency.scheduler : withBaseScheduler; - return scheduler.withBaseScheduler(receiver.getScheduler); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + Scheduler scheduler; + static if (is(Value == void)) { + void setValue() @safe { + receiver.setValue(); + } + } else { + void setValue(Value value) @safe { + receiver.setValue(value); + } + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + void setError(Throwable e) @safe nothrow { + receiver.setError(e); + } + + auto getScheduler() @safe nothrow { + import concurrency.scheduler : withBaseScheduler; + return scheduler.withBaseScheduler(receiver.getScheduler); + } + + mixin ForwardExtensionPoints!receiver; } struct WithSchedulerSender(Sender, Scheduler) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - Scheduler scheduler; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - alias R = WithSchedulerReceiver!(Receiver, Sender.Value, Scheduler); - // ensure NRVO - auto op = sender.connect(R(receiver, scheduler)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + Scheduler scheduler; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + alias R = WithSchedulerReceiver!(Receiver, Sender.Value, Scheduler); + // ensure NRVO + auto op = sender.connect(R(receiver, scheduler)); + return op; + } } diff --git a/source/concurrency/operations/withstopsource.d b/source/concurrency/operations/withstopsource.d index ca5d145..349e1f4 100644 --- a/source/concurrency/operations/withstopsource.d +++ b/source/concurrency/operations/withstopsource.d @@ -8,79 +8,89 @@ import concepts; import std.traits; template withStopSource(Sender) { - auto withStopSource(Sender sender, StopSource stopSource) { - return SSSender!(Sender)(sender, stopSource); - } - auto withStopSource(Sender sender, shared StopSource stopSource) @trusted { - return SSSender!(Sender)(sender, cast()stopSource); - } + auto withStopSource(Sender sender, StopSource stopSource) { + return SSSender!(Sender)(sender, stopSource); + } + + auto withStopSource(Sender sender, shared StopSource stopSource) @trusted { + return SSSender!(Sender)(sender, cast() stopSource); + } } private struct SSReceiver(Receiver, Value) { - private { - Receiver receiver; - StopSource stopSource; - StopSource combinedSource; - StopCallback[2] cbs; - } - static if (is(Value == void)) { - void setValue() @safe { - resetStopCallback(); - receiver.setValueOrError(); - } - } else { - void setValue(Value value) @safe { - resetStopCallback(); - receiver.setValueOrError(value); - } - } - void setDone() @safe nothrow { - resetStopCallback(); - receiver.setDone(); - } - // TODO: would be good if we only emit this function in the Sender actually could call it - void setError(Throwable e) @safe nothrow { - resetStopCallback(); - receiver.setError(e); - } - auto getStopToken() nothrow @trusted scope { - import core.atomic; - if (this.combinedSource is null) { - auto local = new StopSource(); - if (cas(&this.combinedSource, cast(StopSource)null, local)) { - auto stop = cast(void delegate() shared nothrow @safe)&local.stop; - cbs[0] = receiver.getStopToken().onStop(stop); - cbs[1] = StopToken(stopSource).onStop(stop); - if (atomicLoad(this.combinedSource) is null) { - cbs[0].dispose(); - cbs[1].dispose(); - } - } else { - cbs[0].dispose(); - cbs[1].dispose(); - } - } - return StopToken(combinedSource); - } - mixin ForwardExtensionPoints!receiver; - private void resetStopCallback() { - import core.atomic; - if (atomicExchange(&this.combinedSource, cast(StopSource)null)) { - if (cbs[0]) cbs[0].dispose(); - if (cbs[1]) cbs[1].dispose(); - } - } + private { + Receiver receiver; + StopSource stopSource; + StopSource combinedSource; + StopCallback[2] cbs; + } + + static if (is(Value == void)) { + void setValue() @safe { + resetStopCallback(); + receiver.setValueOrError(); + } + } else { + void setValue(Value value) @safe { + resetStopCallback(); + receiver.setValueOrError(value); + } + } + + void setDone() @safe nothrow { + resetStopCallback(); + receiver.setDone(); + } + + // TODO: would be good if we only emit this function in the Sender actually could call it + void setError(Throwable e) @safe nothrow { + resetStopCallback(); + receiver.setError(e); + } + + auto getStopToken() nothrow @trusted scope { + import core.atomic; + if (this.combinedSource is null) { + auto local = new StopSource(); + if (cas(&this.combinedSource, cast(StopSource) null, local)) { + auto stop = + cast(void delegate() shared nothrow @safe) &local.stop; + cbs[0] = receiver.getStopToken().onStop(stop); + cbs[1] = StopToken(stopSource).onStop(stop); + if (atomicLoad(this.combinedSource) is null) { + cbs[0].dispose(); + cbs[1].dispose(); + } + } else { + cbs[0].dispose(); + cbs[1].dispose(); + } + } + + return StopToken(combinedSource); + } + + mixin ForwardExtensionPoints!receiver; + private void resetStopCallback() { + import core.atomic; + if (atomicExchange(&this.combinedSource, cast(StopSource) null)) { + if (cbs[0]) + cbs[0].dispose(); + if (cbs[1]) + cbs[1].dispose(); + } + } } struct SSSender(Sender) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = Sender.Value; - Sender sender; - StopSource stopSource; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - alias R = SSReceiver!(Receiver, Sender.Value); - // ensure NRVO - auto op = sender.connect(R(receiver, stopSource)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = Sender.Value; + Sender sender; + StopSource stopSource; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + alias R = SSReceiver!(Receiver, Sender.Value); + // ensure NRVO + auto op = sender.connect(R(receiver, stopSource)); + return op; + } } diff --git a/source/concurrency/operations/withstoptoken.d b/source/concurrency/operations/withstoptoken.d index c561bf6..de49c45 100644 --- a/source/concurrency/operations/withstoptoken.d +++ b/source/concurrency/operations/withstoptoken.d @@ -9,72 +9,80 @@ import std.traits; import concurrency.utils : isThreadSafeFunction; deprecated("function passed is not shared @safe delegate or @safe function.") -auto withStopToken(Sender, Fun)(Sender sender, Fun fun) if (!isThreadSafeFunction!Fun) { - return STSender!(Sender, Fun)(sender, fun); - } +auto withStopToken(Sender, Fun)(Sender sender, Fun fun) + if (!isThreadSafeFunction!Fun) { + return STSender!(Sender, Fun)(sender, fun); +} -auto withStopToken(Sender, Fun)(Sender sender, Fun fun) if (isThreadSafeFunction!Fun) { - return STSender!(Sender, Fun)(sender, fun); +auto withStopToken(Sender, Fun)(Sender sender, Fun fun) + if (isThreadSafeFunction!Fun) { + return STSender!(Sender, Fun)(sender, fun); } private struct STReceiver(Receiver, Value, Fun) { - Receiver receiver; - Fun fun; - this(return scope Receiver receiver, return Fun fun) @safe return scope { - this.receiver = receiver; - this.fun = fun; - } - static if (is(Value == void)) { - void setValue() @safe { - static if (is(ReturnType!Fun == void)) { - fun(receiver.getStopToken); - if (receiver.getStopToken.isStopRequested) - receiver.setDone(); - else - receiver.setValueOrError(); - } else - receiver.setValueOrError(fun(receiver.getStopToken)); - } - } else { - import std.typecons : isTuple; - enum isExpandable = isTuple!Value && __traits(compiles, {fun(StopToken.init, Value.init.expand);}); - void setValue(Value value) @safe { - static if (is(ReturnType!Fun == void)) { - static if (isExpandable) - fun(receiver.getStopToken, value.expand); - else - fun(receiver.getStopToken, value); - if (receiver.getStopToken.isStopRequested) - receiver.setDone(); - else - receiver.setValueOrError(); - } else { - static if (isExpandable) - auto r = fun(receiver.getStopToken, value.expand); - else - auto r = fun(receiver.getStopToken, value); - receiver.setValueOrError(r); - } - } - } - void setDone() nothrow @safe { - receiver.setDone(); - } - void setError(Throwable e) nothrow @safe { - receiver.setError(e); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + Fun fun; + this(return scope Receiver receiver, return Fun fun) @safe return scope { + this.receiver = receiver; + this.fun = fun; + } + + static if (is(Value == void)) { + void setValue() @safe { + static if (is(ReturnType!Fun == void)) { + fun(receiver.getStopToken); + if (receiver.getStopToken.isStopRequested) + receiver.setDone(); + else + receiver.setValueOrError(); + } else + receiver.setValueOrError(fun(receiver.getStopToken)); + } + } else { + import std.typecons : isTuple; + enum isExpandable = isTuple!Value && __traits(compiles, { + fun(StopToken.init, Value.init.expand); + }); + void setValue(Value value) @safe { + static if (is(ReturnType!Fun == void)) { + static if (isExpandable) + fun(receiver.getStopToken, value.expand); + else + fun(receiver.getStopToken, value); + if (receiver.getStopToken.isStopRequested) + receiver.setDone(); + else + receiver.setValueOrError(); + } else { + static if (isExpandable) + auto r = fun(receiver.getStopToken, value.expand); + else + auto r = fun(receiver.getStopToken, value); + receiver.setValueOrError(r); + } + } + } + + void setDone() nothrow @safe { + receiver.setDone(); + } + + void setError(Throwable e) nothrow @safe { + receiver.setError(e); + } + + mixin ForwardExtensionPoints!receiver; } struct STSender(Sender, Fun) if (models!(Sender, isSender)) { - static assert(models!(typeof(this), isSender)); - alias Value = ReturnType!fun; - Sender sender; - Fun fun; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - alias R = STReceiver!(Receiver, Sender.Value, Fun); - // ensure NRVO - auto op = sender.connect(R(receiver, fun)); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = ReturnType!fun; + Sender sender; + Fun fun; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + alias R = STReceiver!(Receiver, Sender.Value, Fun); + // ensure NRVO + auto op = sender.connect(R(receiver, fun)); + return op; + } } diff --git a/source/concurrency/receiver.d b/source/concurrency/receiver.d index 56ed378..ced0818 100644 --- a/source/concurrency/receiver.d +++ b/source/concurrency/receiver.d @@ -4,88 +4,100 @@ import concepts; /// checks that T is a Receiver void checkReceiver(T)() { - T t = T.init; - import std.traits; - alias Params = Parameters!(T.setValue); - static if (Params.length == 0) - t.setValue(); - else - t.setValue(Params[0].init); - (() nothrow => t.setDone())(); - (() nothrow => t.setError(new Throwable("test")))(); + T t = T.init; + import std.traits; + alias Params = Parameters!(T.setValue); + static if (Params.length == 0) + t.setValue(); + else + t.setValue(Params[0].init); + (() nothrow => t.setDone())(); + (() nothrow => t.setError(new Throwable("test")))(); } enum isReceiver(T) = is(typeof(checkReceiver!T)); auto getStopToken(Receiver)(Receiver r) nothrow @safe if (isReceiver!Receiver) { - import concurrency.stoptoken : NeverStopToken; - return NeverStopToken(); + import concurrency.stoptoken : NeverStopToken; + return NeverStopToken(); } mixin template ForwardExtensionPoints(alias receiver) { - auto getStopToken() nothrow @safe { - return receiver.getStopToken(); - } - auto getScheduler() nothrow @safe { - return receiver.getScheduler(); - } + auto getStopToken() nothrow @safe { + return receiver.getStopToken(); + } + + auto getScheduler() nothrow @safe { + return receiver.getScheduler(); + } } /// A polymorphic receiver of type T interface ReceiverObjectBase(T) { - import concurrency.stoptoken : StopToken; - import concurrency.scheduler : SchedulerObjectBase; - static assert (models!(ReceiverObjectBase!T, isReceiver)); - static if (is(T == void)) - void setValue() @safe; - else - void setValue(T value = T.init) @safe; - void setDone() nothrow @safe; - void setError(Throwable e) nothrow @safe; - StopToken getStopToken() nothrow @safe; - SchedulerObjectBase getScheduler() scope nothrow @safe; + import concurrency.stoptoken : StopToken; + import concurrency.scheduler : SchedulerObjectBase; + static assert(models!(ReceiverObjectBase!T, isReceiver)); + static if (is(T == void)) + void setValue() @safe; + else + void setValue(T value = T.init) @safe; + void setDone() nothrow @safe; + void setError(Throwable e) nothrow @safe; + StopToken getStopToken() nothrow @safe; + SchedulerObjectBase getScheduler() scope nothrow @safe; } struct NullReceiver(T) { - void setDone() nothrow @safe @nogc {} - void setError(Throwable e) nothrow @safe @nogc {} - static if (is(T == void)) - void setValue() nothrow @safe @nogc {} - else - void setValue(T t) nothrow @safe @nogc {} + void setDone() nothrow @safe @nogc {} + + void setError(Throwable e) nothrow @safe @nogc {} + + static if (is(T == void)) + void setValue() nothrow @safe @nogc {} + + else + void setValue(T t) nothrow @safe @nogc {} } struct ThrowingNullReceiver(T) { - void setDone() nothrow @safe @nogc {} - void setError(Throwable e) nothrow @safe @nogc {} - static if (is(T == void)) - void setValue() @safe { throw new Exception("ThrowingNullReceiver"); } - else - void setValue(T t) @safe { throw new Exception("ThrowingNullReceiver"); } + void setDone() nothrow @safe @nogc {} + + void setError(Throwable e) nothrow @safe @nogc {} + + static if (is(T == void)) + void setValue() @safe { + throw new Exception("ThrowingNullReceiver"); + } + + else + void setValue(T t) @safe { + throw new Exception("ThrowingNullReceiver"); + } } void setValueOrError(Receiver)(auto ref Receiver receiver) @safe { - import std.traits; - static if (hasFunctionAttributes!(receiver.setValue, "nothrow")) { - receiver.setValue(); - } else { - try { - receiver.setValue(); - } catch (Exception e) { - receiver.setError(e); - } - } + import std.traits; + static if (hasFunctionAttributes!(receiver.setValue, "nothrow")) { + receiver.setValue(); + } else { + try { + receiver.setValue(); + } catch (Exception e) { + receiver.setError(e); + } + } } -void setValueOrError(Receiver, T)(auto ref Receiver receiver, auto ref T value) @safe { - import std.traits; - static if (hasFunctionAttributes!(receiver.setValue, "nothrow")) { - receiver.setValue(value); - } else { - try { - receiver.setValue(value); - } catch (Exception e) { - receiver.setError(e); - } - } +void setValueOrError(Receiver, T)(auto ref Receiver receiver, + auto ref T value) @safe { + import std.traits; + static if (hasFunctionAttributes!(receiver.setValue, "nothrow")) { + receiver.setValue(value); + } else { + try { + receiver.setValue(value); + } catch (Exception e) { + receiver.setError(e); + } + } } diff --git a/source/concurrency/scheduler.d b/source/concurrency/scheduler.d index 8687598..7892566 100644 --- a/source/concurrency/scheduler.d +++ b/source/concurrency/scheduler.d @@ -6,309 +6,357 @@ import concepts; import mir.algebraic : Nullable, nullable; void checkScheduler(T)() { - import concurrency.sender : checkSender; - import core.time : msecs; - T t = T.init; - alias Sender = typeof(t.schedule()); - checkSender!Sender(); - alias AfterSender = typeof(t.scheduleAfter(10.msecs)); - checkSender!AfterSender(); + import concurrency.sender : checkSender; + import core.time : msecs; + T t = T.init; + alias Sender = typeof(t.schedule()); + checkSender!Sender(); + alias AfterSender = typeof(t.scheduleAfter(10.msecs)); + checkSender!AfterSender(); } + enum isScheduler(T) = is(typeof(checkScheduler!T)); /// polymorphic Scheduler interface SchedulerObjectBase { - SenderObjectBase!void schedule() @safe; - SenderObjectBase!void scheduleAfter(Duration d) @safe; + SenderObjectBase!void schedule() @safe; + SenderObjectBase!void scheduleAfter(Duration d) @safe; } class SchedulerObject(S) : SchedulerObjectBase { - import concurrency.sender : toSenderObject; - S scheduler; - this(S scheduler) { - this.scheduler = scheduler; - } - SenderObjectBase!void schedule() @safe { - return scheduler.schedule().toSenderObject(); - } - SenderObjectBase!void scheduleAfter(Duration d) @safe { - return scheduler.scheduleAfter(d).toSenderObject(); - } + import concurrency.sender : toSenderObject; + S scheduler; + this(S scheduler) { + this.scheduler = scheduler; + } + + SenderObjectBase!void schedule() @safe { + return scheduler.schedule().toSenderObject(); + } + + SenderObjectBase!void scheduleAfter(Duration d) @safe { + return scheduler.scheduleAfter(d).toSenderObject(); + } } SchedulerObjectBase toSchedulerObject(S)(S scheduler) { - return new SchedulerObject!(S)(scheduler); + return new SchedulerObject!(S)(scheduler); } struct NullScheduler {} enum TimerTrigger { - trigger, - cancel + trigger, + cancel } alias TimerDelegate = void delegate(TimerTrigger) shared @safe; struct Timer { - TimerDelegate dg; - ulong id_; - ulong id() @safe nothrow @nogc { return id_; } + TimerDelegate dg; + ulong id_; + ulong id() @safe nothrow @nogc { + return id_; + } } auto localThreadScheduler() { - import concurrency.thread : LocalThreadWorker, getLocalThreadExecutor; - return SchedulerAdapter!LocalThreadWorker(LocalThreadWorker(getLocalThreadExecutor)); + import concurrency.thread : LocalThreadWorker, getLocalThreadExecutor; + return SchedulerAdapter!LocalThreadWorker( + LocalThreadWorker(getLocalThreadExecutor)); } alias LocalThreadScheduler = typeof(localThreadScheduler()); struct SchedulerAdapter(Worker) { - import concurrency.receiver : setValueOrError; - import concurrency.executor : VoidDelegate; - import core.time : Duration; - Worker worker; - auto schedule() { - static struct ScheduleOp(Receiver) { - Worker worker; - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() @trusted nothrow { - try { - worker.schedule(cast(VoidDelegate)()=>receiver.setValueOrError()); - } catch (Exception e) { - receiver.setError(e); - } - } - } - static struct ScheduleSender { - alias Value = void; - Worker worker; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = ScheduleOp!(Receiver)(worker, receiver); - return op; - } - } - return ScheduleSender(worker); - } - auto schedule() shared @trusted { - return (cast()this).schedule(); - } - auto scheduleAfter(Duration dur) @safe { - return ScheduleAfterSender!(Worker)(worker, dur); - } - auto scheduleAfter(Duration dur) shared @trusted { - return (cast()this).scheduleAfter(dur); - } + import concurrency.receiver : setValueOrError; + import concurrency.executor : VoidDelegate; + import core.time : Duration; + Worker worker; + auto schedule() { + static struct ScheduleOp(Receiver) { + Worker worker; + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() @trusted nothrow { + try { + worker.schedule( + cast(VoidDelegate) () => receiver.setValueOrError()); + } catch (Exception e) { + receiver.setError(e); + } + } + } + + static struct ScheduleSender { + alias Value = void; + Worker worker; + auto connect(Receiver)( + return Receiver receiver + ) @safe return scope { + // ensure NRVO + auto op = ScheduleOp!(Receiver)(worker, receiver); + return op; + } + } + + return ScheduleSender(worker); + } + + auto schedule() shared @trusted { + return (cast() this).schedule(); + } + + auto scheduleAfter(Duration dur) @safe { + return ScheduleAfterSender!(Worker)(worker, dur); + } + + auto scheduleAfter(Duration dur) shared @trusted { + return (cast() this).scheduleAfter(dur); + } } struct ScheduleAfterOp(Worker, Receiver) { - import std.traits : ReturnType; - import concurrency.bitfield : SharedBitField; - import concurrency.stoptoken : StopCallback, onStop; - import concurrency.receiver : setValueOrError; - - enum Flags { - locked = 0x0, - stop = 0x1, - triggered = 0x2, - setup = 0x4, - } - alias Timer = ReturnType!(Worker.addTimer); - Worker worker; - Duration dur; - Receiver receiver; - Timer timer; - StopCallback stopCb; - shared SharedBitField!Flags flags; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() @trusted scope nothrow { - if (receiver.getStopToken().isStopRequested) { - receiver.setDone(); - return; - } - - stopCb = receiver.getStopToken().onStop(cast(void delegate() nothrow @safe shared)&stop); - - try { - timer = worker.addTimer(cast(void delegate(TimerTrigger) @safe shared)&trigger, dur); - } catch (Exception e) { - receiver.setError(e); - return; - } - - with (flags.add(Flags.setup)) { - if (has(Flags.stop)) { - try { worker.cancelTimer(timer); } catch (Exception e) {} // TODO: what to do here? - } - if (has(Flags.triggered)) { - receiver.setValueOrError(); - } - } - } - private void trigger(TimerTrigger cause) @trusted nothrow { - with (flags.add(Flags.triggered)) { - if (!has(Flags.setup)) - return; - stopCb.dispose(); - final switch (cause) { - case TimerTrigger.cancel: - receiver.setDone(); - break; - case TimerTrigger.trigger: - receiver.setValueOrError(); - break; - } - } - } - private void stop() @trusted nothrow { - with (flags.add(Flags.stop)) { - if (!has(Flags.setup)) { - return; - } - if (!has(Flags.triggered)) { - try { worker.cancelTimer(timer); } catch (Exception e) {} // TODO: what to do here? - } - } - } + import std.traits : ReturnType; + import concurrency.bitfield : SharedBitField; + import concurrency.stoptoken : StopCallback, onStop; + import concurrency.receiver : setValueOrError; + + enum Flags { + locked = 0x0, + stop = 0x1, + triggered = 0x2, + setup = 0x4, + } + + alias Timer = ReturnType!(Worker.addTimer); + Worker worker; + Duration dur; + Receiver receiver; + Timer timer; + StopCallback stopCb; + shared SharedBitField!Flags flags; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() @trusted scope nothrow { + if (receiver.getStopToken().isStopRequested) { + receiver.setDone(); + return; + } + + stopCb = + receiver.getStopToken() + .onStop(cast(void delegate() nothrow @safe shared) &stop); + + try { + timer = worker.addTimer( + cast(void delegate(TimerTrigger) @safe shared) &trigger, dur); + } catch (Exception e) { + receiver.setError(e); + return; + } + + with (flags.add(Flags.setup)) { + if (has(Flags.stop)) { + try { + worker.cancelTimer(timer); + } catch (Exception e) {} // TODO: what to do here? + } + + if (has(Flags.triggered)) { + receiver.setValueOrError(); + } + } + } + + private void trigger(TimerTrigger cause) @trusted nothrow { + with (flags.add(Flags.triggered)) { + if (!has(Flags.setup)) + return; + stopCb.dispose(); + final switch (cause) { + case TimerTrigger.cancel: + receiver.setDone(); + break; + case TimerTrigger.trigger: + receiver.setValueOrError(); + break; + } + } + } + + private void stop() @trusted nothrow { + with (flags.add(Flags.stop)) { + if (!has(Flags.setup)) { + return; + } + + if (!has(Flags.triggered)) { + try { + worker.cancelTimer(timer); + } catch (Exception e) {} // TODO: what to do here? + } + } + } } struct ScheduleAfterSender(Worker) { - alias Value = void; - Worker worker; - Duration dur; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = ScheduleAfterOp!(Worker, Receiver)(worker, dur, receiver); - return op; - } + alias Value = void; + Worker worker; + Duration dur; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = ScheduleAfterOp!(Worker, Receiver)(worker, dur, receiver); + return op; + } } struct ManualTimeScheduler { - shared ManualTimeWorker worker; - auto schedule() { - import core.time : msecs; - return scheduleAfter(0.msecs); - } - auto scheduleAfter(Duration dur) { - return ScheduleAfterSender!(shared ManualTimeWorker)(worker, dur); - } + shared ManualTimeWorker worker; + auto schedule() { + import core.time : msecs; + return scheduleAfter(0.msecs); + } + + auto scheduleAfter(Duration dur) { + return ScheduleAfterSender!(shared ManualTimeWorker)(worker, dur); + } } class ManualTimeWorker { - import concurrency.timingwheels : TimingWheels; - import concurrency.executor : VoidDelegate; - import core.sync.mutex : Mutex; - import core.sync.condition : Condition; - import core.time : msecs, hnsecs; - import std.array : Appender; - private { - TimingWheels!Timer wheels; - Appender!(Timer[]) expiredTimers; - Condition condition; - size_t time = 1; - shared ulong nextTimerId; - } - auto lock() @trusted shared { - import concurrency.utils : SharedGuard; - return SharedGuard!(ManualTimeWorker).acquire(this, cast()condition.mutex); - } - this() @trusted shared { - condition = cast(shared)new Condition(new Mutex()); - (cast()wheels).init(time); - } - ManualTimeScheduler getScheduler() @safe shared { - return ManualTimeScheduler(this); - } - Timer addTimer(TimerDelegate dg, Duration dur) @trusted shared { - import core.atomic : atomicOp; - with(lock()) { - auto real_now = time; - auto tw_now = wheels.currStdTime(1.msecs); - auto delay = (real_now - tw_now).hnsecs; - auto at = (dur + delay)/1.msecs; - auto timer = Timer(dg, nextTimerId.atomicOp!("+=")(1)); - wheels.schedule(timer, at); - condition.notifyAll(); - return timer; - } - } - void wait() @trusted shared { - with(lock()) { - condition.wait(); - } - } - void cancelTimer(Timer timer) @trusted shared { - with(lock()) { - wheels.cancel(timer); - } - timer.dg(TimerTrigger.cancel); - } - Nullable!Duration timeUntilNextEvent() @trusted shared { - with(lock()) { - return wheels.timeUntilNextEvent(1.msecs, time); - } - } - void advance(Duration dur) @trusted shared { - import std.range : retro; - import core.time : msecs; - with(lock()) { - time += dur.total!"hnsecs"; - int incr = wheels.ticksToCatchUp(1.msecs, time); - if (incr > 0) { - wheels.advance(incr, expiredTimers); - // NOTE timingwheels keeps the timers in reverse order, so we iterate in reverse - foreach(t; expiredTimers.data.retro) { - t.dg(TimerTrigger.trigger); - } - expiredTimers.shrinkTo(0); - } - } - } + import concurrency.timingwheels : TimingWheels; + import concurrency.executor : VoidDelegate; + import core.sync.mutex : Mutex; + import core.sync.condition : Condition; + import core.time : msecs, hnsecs; + import std.array : Appender; + private { + TimingWheels!Timer wheels; + Appender!(Timer[]) expiredTimers; + Condition condition; + size_t time = 1; + shared ulong nextTimerId; + } + + auto lock() @trusted shared { + import concurrency.utils : SharedGuard; + return SharedGuard!(ManualTimeWorker) + .acquire(this, cast() condition.mutex); + } + + this() @trusted shared { + condition = cast(shared) new Condition(new Mutex()); + (cast() wheels).init(time); + } + + ManualTimeScheduler getScheduler() @safe shared { + return ManualTimeScheduler(this); + } + + Timer addTimer(TimerDelegate dg, Duration dur) @trusted shared { + import core.atomic : atomicOp; + with (lock()) { + auto real_now = time; + auto tw_now = wheels.currStdTime(1.msecs); + auto delay = (real_now - tw_now).hnsecs; + auto at = (dur + delay) / 1.msecs; + auto timer = Timer(dg, nextTimerId.atomicOp!("+=")(1)); + wheels.schedule(timer, at); + condition.notifyAll(); + return timer; + } + } + + void wait() @trusted shared { + with (lock()) { + condition.wait(); + } + } + + void cancelTimer(Timer timer) @trusted shared { + with (lock()) { + wheels.cancel(timer); + } + + timer.dg(TimerTrigger.cancel); + } + + Nullable!Duration timeUntilNextEvent() @trusted shared { + with (lock()) { + return wheels.timeUntilNextEvent(1.msecs, time); + } + } + + void advance(Duration dur) @trusted shared { + import std.range : retro; + import core.time : msecs; + with (lock()) { + time += dur.total!"hnsecs"; + int incr = wheels.ticksToCatchUp(1.msecs, time); + if (incr > 0) { + wheels.advance(incr, expiredTimers); + // NOTE timingwheels keeps the timers in reverse order, so we iterate in reverse + foreach (t; expiredTimers.data.retro) { + t.dg(TimerTrigger.trigger); + } + + expiredTimers.shrinkTo(0); + } + } + } } auto withBaseScheduler(T, P)(auto ref T t, auto ref P p) { - static if (isScheduler!T) - return t; - else static if (isScheduler!P) - return ProxyScheduler!(T, P)(t,p); - else - static assert(false, "Neither "~T.stringof~" nor "~P.stringof~" are full schedulers. Chain the sender with a .withScheduler and ensure the Scheduler passes the isScheduler check."); + static if (isScheduler!T) + return t; + else static if (isScheduler!P) + return ProxyScheduler!(T, P)(t, p); + else + static assert( + false, + "Neither " ~ T.stringof ~ " nor " ~ P.stringof + ~ " are full schedulers. Chain the sender with a .withScheduler and ensure the Scheduler passes the isScheduler check." + ); } private struct ProxyScheduler(T, P) { - import std.parallelism : TaskPool; - import core.time : Duration; - T front; - P back; - auto schedule() { - return front.schedule(); - } - auto scheduleAfter(Duration run) { - import concurrency.operations : via; - return schedule().via(back.scheduleAfter(run)); - } + import std.parallelism : TaskPool; + import core.time : Duration; + T front; + P back; + auto schedule() { + return front.schedule(); + } + + auto scheduleAfter(Duration run) { + import concurrency.operations : via; + return schedule().via(back.scheduleAfter(run)); + } } struct ScheduleAfter { - static assert (models!(typeof(this), isSender)); - alias Value = void; - Duration duration; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = receiver.getScheduler.scheduleAfter(duration).connect(receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = void; + Duration duration; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = + receiver.getScheduler.scheduleAfter(duration).connect(receiver); + return op; + } } struct Schedule { - static assert (models!(typeof(this), isSender)); - alias Value = void; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = receiver.getScheduler.schedule().connect(receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = void; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = receiver.getScheduler.schedule().connect(receiver); + return op; + } } diff --git a/source/concurrency/sender.d b/source/concurrency/sender.d index 0b4d35b..be0b7d1 100644 --- a/source/concurrency/sender.d +++ b/source/concurrency/sender.d @@ -42,578 +42,654 @@ import core.time : Duration; /// checks that T is a Sender void checkSender(T)() @safe { - import concurrency.scheduler : SchedulerObjectBase; - import concurrency.stoptoken : StopToken; - T t = T.init; - struct Scheduler { - import core.time : Duration; - auto schedule() @safe { return VoidSender(); } - auto scheduleAfter(Duration) @safe { return VoidSender(); } - } - static struct Receiver { - int* i; // force it scope - static if (is(T.Value == void)) - void setValue() @safe {} - else - void setValue(T.Value) @safe {} - void setDone() @safe nothrow {} - void setError(Throwable e) @safe nothrow {} - StopToken getStopToken() @safe nothrow { return StopToken.init; } - Scheduler getScheduler() @safe nothrow { return Scheduler.init; } - } - scope receiver = Receiver.init; - OpType!(T, Receiver) op = t.connect(receiver); - static if (!isValidOp!(T, Receiver)) - pragma(msg, "Warning: ", T, "'s operation state is not returned via the stack"); + import concurrency.scheduler : SchedulerObjectBase; + import concurrency.stoptoken : StopToken; + T t = T.init; + struct Scheduler { + import core.time : Duration; + auto schedule() @safe { + return VoidSender(); + } + + auto scheduleAfter(Duration) @safe { + return VoidSender(); + } + } + + static struct Receiver { + int* i; // force it scope + static if (is(T.Value == void)) + void setValue() @safe {} + + else + void setValue(T.Value) @safe {} + + void setDone() @safe nothrow {} + + void setError(Throwable e) @safe nothrow {} + + StopToken getStopToken() @safe nothrow { + return StopToken.init; + } + + Scheduler getScheduler() @safe nothrow { + return Scheduler.init; + } + } + + scope receiver = Receiver.init; + OpType!(T, Receiver) op = t.connect(receiver); + static if (!isValidOp!(T, Receiver)) + pragma(msg, "Warning: ", T, + "'s operation state is not returned via the stack"); } + enum isSender(T) = is(typeof(checkSender!T)); /// It is ok for the operation state to be on the heap, but if it is on the stack we need to ensure any copies are elided. We can't be 100% sure (the compiler may still blit), but this is the best we can do. template isValidOp(Sender, Receiver) { - import std.traits : isPointer; - import std.meta : allSatisfy; - alias overloads = __traits(getOverloads, Sender, "connect", true); - template isRVO(alias connect) { - static if (__traits(isTemplate, connect)) - enum isRVO = __traits(isReturnOnStack, connect!Receiver); - else - enum isRVO = __traits(isReturnOnStack, connect); - } - alias Op = OpType!(Sender, Receiver); - enum isValidOp = isPointer!Op || is(Op == OperationObject) || is(Op == class) || (allSatisfy!(isRVO, overloads) && !__traits(isPOD, Op)); + import std.traits : isPointer; + import std.meta : allSatisfy; + alias overloads = __traits(getOverloads, Sender, "connect", true); + template isRVO(alias connect) { + static if (__traits(isTemplate, connect)) + enum isRVO = __traits(isReturnOnStack, connect!Receiver); + else + enum isRVO = __traits(isReturnOnStack, connect); + } + + alias Op = OpType!(Sender, Receiver); + enum isValidOp = isPointer!Op || is(Op == OperationObject) + || is(Op == class) + || (allSatisfy!(isRVO, overloads) && !__traits(isPOD, Op)); } /// A Sender that sends a single value of type T struct ValueSender(T) { - static assert (models!(typeof(this), isSender)); - alias Value = T; - static struct Op(Receiver) { - Receiver receiver; - static if (!is(T == void)) - T value; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @trusted scope { - import concurrency.receiver : setValueOrError; - static if (!is(T == void)) - receiver.setValueOrError(value); - else - receiver.setValueOrError(); - } - } - static if (!is(T == void)) - T value; - Op!Receiver connect(Receiver)(return Receiver receiver) @safe { - // ensure NRVO - static if (!is(T == void)) - auto op = Op!(Receiver)(receiver, value); - else - auto op = Op!(Receiver)(receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = T; + static struct Op(Receiver) { + Receiver receiver; + static if (!is(T == void)) + T value; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @trusted scope { + import concurrency.receiver : setValueOrError; + static if (!is(T == void)) + receiver.setValueOrError(value); + else + receiver.setValueOrError(); + } + } + + static if (!is(T == void)) + T value; + Op!Receiver connect(Receiver)(return Receiver receiver) @safe { + // ensure NRVO + static if (!is(T == void)) + auto op = Op!(Receiver)(receiver, value); + else + auto op = Op!(Receiver)(receiver); + return op; + } } auto just(T...)(T t) { - import std.typecons : tuple, Tuple; - static if (T.length == 1) - return ValueSender!(T[0])(t); - else - return ValueSender!(Tuple!T)(tuple(t)); + import std.typecons : tuple, Tuple; + static if (T.length == 1) + return ValueSender!(T[0])(t); + else + return ValueSender!(Tuple!T)(tuple(t)); } struct JustFromSender(Fun) { - static assert (models!(typeof(this), isSender)); - alias Value = ReturnType!fun; - static struct Op(Receiver) { - Receiver receiver; - Fun fun; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() @trusted nothrow { - import std.traits : hasFunctionAttributes; - static if (hasFunctionAttributes!(Fun, "nothrow")) { - set(); - } else { - try { - set(); - } catch (Exception e) { - receiver.setError(e); - } - } - } - private void set() @safe { - import concurrency.receiver : setValueOrError; - static if (is(Value == void)) { - fun(); - if (receiver.getStopToken.isStopRequested) - receiver.setDone(); - else - receiver.setValue(); - } else { - auto r = fun(); - if (receiver.getStopToken.isStopRequested) - receiver.setDone(); - else - receiver.setValue(r); - } - } - } - Fun fun; - Op!Receiver connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(receiver, fun); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = ReturnType!fun; + static struct Op(Receiver) { + Receiver receiver; + Fun fun; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() @trusted nothrow { + import std.traits : hasFunctionAttributes; + static if (hasFunctionAttributes!(Fun, "nothrow")) { + set(); + } else { + try { + set(); + } catch (Exception e) { + receiver.setError(e); + } + } + } + + private void set() @safe { + import concurrency.receiver : setValueOrError; + static if (is(Value == void)) { + fun(); + if (receiver.getStopToken.isStopRequested) + receiver.setDone(); + else + receiver.setValue(); + } else { + auto r = fun(); + if (receiver.getStopToken.isStopRequested) + receiver.setDone(); + else + receiver.setValue(r); + } + } + } + + Fun fun; + Op!Receiver connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(receiver, fun); + return op; + } } JustFromSender!(Fun) justFrom(Fun)(Fun fun) if (isCallable!Fun) { - import std.traits : hasFunctionAttributes, isFunction, isFunctionPointer; - import concurrency.utils : isThreadSafeFunction; - static assert(isThreadSafeFunction!Fun); - return JustFromSender!Fun(fun); + import std.traits : hasFunctionAttributes, isFunction, isFunctionPointer; + import concurrency.utils : isThreadSafeFunction; + static assert(isThreadSafeFunction!Fun); + return JustFromSender!Fun(fun); } /// A polymorphic sender of type T interface SenderObjectBase(T) { - import concurrency.receiver; - import concurrency.scheduler : SchedulerObjectBase; - import concurrency.stoptoken : StopToken, stopTokenObject; - static assert (models!(typeof(this), isSender)); - alias Value = T; - alias Op = OperationObject; - OperationObject connect(return ReceiverObjectBase!(T) receiver) @safe scope; - OperationObject connect(Receiver)(return Receiver receiver) @trusted scope { - return connect(new class(receiver) ReceiverObjectBase!T { - Receiver receiver; - this(Receiver receiver) { - this.receiver = receiver; - } - static if (is(T == void)) { - void setValue() { - receiver.setValueOrError(); - } - } else { - void setValue(T value) { - receiver.setValueOrError(value); - } - } - void setDone() nothrow { - receiver.setDone(); - } - void setError(Throwable e) nothrow { - receiver.setError(e); - } - StopToken getStopToken() nothrow { - return stopTokenObject(receiver.getStopToken()); - } - SchedulerObjectBase getScheduler() nothrow @safe scope { - import concurrency.scheduler : toSchedulerObject; - return receiver.getScheduler().toSchedulerObject; - } - }); - } + import concurrency.receiver; + import concurrency.scheduler : SchedulerObjectBase; + import concurrency.stoptoken : StopToken, stopTokenObject; + static assert(models!(typeof(this), isSender)); + alias Value = T; + alias Op = OperationObject; + OperationObject connect(return ReceiverObjectBase!(T) receiver) @safe scope; + OperationObject connect(Receiver)(return Receiver receiver) @trusted scope { + return connect(new class(receiver) ReceiverObjectBase!T { + Receiver receiver; + this(Receiver receiver) { + this.receiver = receiver; + } + + static if (is(T == void)) { + void setValue() { + receiver.setValueOrError(); + } + } else { + void setValue(T value) { + receiver.setValueOrError(value); + } + } + + void setDone() nothrow { + receiver.setDone(); + } + + void setError(Throwable e) nothrow { + receiver.setError(e); + } + + StopToken getStopToken() nothrow { + return stopTokenObject(receiver.getStopToken()); + } + + SchedulerObjectBase getScheduler() nothrow @safe scope { + import concurrency.scheduler : toSchedulerObject; + return receiver.getScheduler().toSchedulerObject; + } + }); + } } /// Type-erased operational state object /// used in polymorphic senders struct OperationObject { - private void delegate() nothrow shared _start; - void start() scope nothrow @trusted { _start(); } + private void delegate() nothrow shared _start; + void start() scope nothrow @trusted { + _start(); + } } interface OperationalStateBase { - void start() @safe nothrow; + void start() @safe nothrow; } /// calls connect on the Sender but stores the OperationState on the heap -OperationalStateBase connectHeap(Sender, Receiver)(Sender sender, Receiver receiver) @safe { - alias State = typeof(sender.connect(receiver)); - return new class(sender, receiver) OperationalStateBase { - State state; - this(return Sender sender, return Receiver receiver) @trusted { - state = sender.connect(receiver); - } - void start() @safe nothrow { - state.start(); - } - }; +OperationalStateBase connectHeap(Sender, Receiver)(Sender sender, + Receiver receiver) @safe { + alias State = typeof(sender.connect(receiver)); + return new class(sender, receiver) OperationalStateBase { + State state; + this(return Sender sender, return Receiver receiver) @trusted { + state = sender.connect(receiver); + } + + void start() @safe nothrow { + state.start(); + } + }; } /// A class extending from SenderObjectBase that wraps any Sender class SenderObjectImpl(Sender) : SenderObjectBase!(Sender.Value) { - import concurrency.receiver : ReceiverObjectBase; - static assert (models!(typeof(this), isSender)); - private Sender sender; - this(Sender sender) { - this.sender = sender; - } - OperationObject connect(return ReceiverObjectBase!(Sender.Value) receiver) @trusted scope { - auto state = sender.connectHeap(receiver); - return OperationObject(cast(typeof(OperationObject._start))&state.start); - } - OperationObject connect(Receiver)(return Receiver receiver) @safe scope { - auto base = cast(SenderObjectBase!(Sender.Value))this; - return base.connect(receiver); - } + import concurrency.receiver : ReceiverObjectBase; + static assert(models!(typeof(this), isSender)); + private Sender sender; + this(Sender sender) { + this.sender = sender; + } + + OperationObject connect( + return ReceiverObjectBase!(Sender.Value) receiver + ) @trusted scope { + auto state = sender.connectHeap(receiver); + return + OperationObject(cast(typeof(OperationObject._start)) &state.start); + } + + OperationObject connect(Receiver)(return Receiver receiver) @safe scope { + auto base = cast(SenderObjectBase!(Sender.Value)) this; + return base.connect(receiver); + } } /// Converts any Sender to a polymorphic SenderObject auto toSenderObject(Sender)(Sender sender) { - static assert(models!(Sender, isSender)); - static if (is(Sender : SenderObjectBase!(Sender.Value))) { - return sender; - } else - return cast(SenderObjectBase!(Sender.Value))new SenderObjectImpl!(Sender)(sender); + static assert(models!(Sender, isSender)); + static if (is(Sender : SenderObjectBase!(Sender.Value))) { + return sender; + } else + return cast(SenderObjectBase!(Sender.Value)) + new SenderObjectImpl!(Sender)(sender); } /// A sender that always sets an error struct ThrowingSender { - alias Value = void; - static struct Op(Receiver) { - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() { - receiver.setError(new Exception("ThrowingSender")); - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!Receiver(receiver); - return op; - } + alias Value = void; + static struct Op(Receiver) { + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() { + receiver.setError(new Exception("ThrowingSender")); + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!Receiver(receiver); + return op; + } } /// A sender that always calls setDone struct DoneSender { - static assert (models!(typeof(this), isSender)); - alias Value = void; - static struct DoneOp(Receiver) { - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @trusted scope { - receiver.setDone(); - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = DoneOp!(Receiver)(receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = void; + static struct DoneOp(Receiver) { + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @trusted scope { + receiver.setDone(); + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = DoneOp!(Receiver)(receiver); + return op; + } } /// A sender that always calls setValue with no args struct VoidSender { - static assert (models!(typeof(this), isSender)); - alias Value = void; - struct VoidOp(Receiver) { - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @safe { - import concurrency.receiver : setValueOrError; - receiver.setValueOrError(); - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = VoidOp!Receiver(receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = void; + struct VoidOp(Receiver) { + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @safe { + import concurrency.receiver : setValueOrError; + receiver.setValueOrError(); + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = VoidOp!Receiver(receiver); + return op; + } } /// A sender that always calls setError struct ErrorSender { - static assert (models!(typeof(this), isSender)); - alias Value = void; - Throwable exception; - static struct ErrorOp(Receiver) { - Receiver receiver; - Throwable exception; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @trusted scope { - receiver.setError(exception); - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = ErrorOp!(Receiver)(receiver, exception); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = void; + Throwable exception; + static struct ErrorOp(Receiver) { + Receiver receiver; + Throwable exception; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @trusted scope { + receiver.setError(exception); + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = ErrorOp!(Receiver)(receiver, exception); + return op; + } } template OpType(Sender, Receiver) { - static if (is(Sender.Op)) { - alias OpType = Sender.Op; - } else { - import std.traits : ReturnType; - import std.meta : staticMap; - template GetOpType(alias connect) { - static if (__traits(isTemplate, connect)) { - alias GetOpType = ReturnType!(connect!Receiver);//(Receiver.init)); - } else { - alias GetOpType = ReturnType!(connect);//(Receiver.init)); - } - } - alias overloads = __traits(getOverloads, Sender, "connect", true); - alias opTypes = staticMap!(GetOpType, overloads); - alias OpType = opTypes[0]; - } + static if (is(Sender.Op)) { + alias OpType = Sender.Op; + } else { + import std.traits : ReturnType; + import std.meta : staticMap; + template GetOpType(alias connect) { + static if (__traits(isTemplate, connect)) { + alias GetOpType = + ReturnType!(connect!Receiver);//(Receiver.init)); + } else { + alias GetOpType = ReturnType!(connect);//(Receiver.init)); + } + } + + alias overloads = __traits(getOverloads, Sender, "connect", true); + alias opTypes = staticMap!(GetOpType, overloads); + alias OpType = opTypes[0]; + } } /// A sender that delays before calling setValue struct DelaySender { - alias Value = void; - Duration dur; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = receiver.getScheduler().scheduleAfter(dur).connect(receiver); - return op; - } + alias Value = void; + Duration dur; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = receiver.getScheduler().scheduleAfter(dur).connect(receiver); + return op; + } } auto delay(Duration dur) { - return DelaySender(dur); + return DelaySender(dur); } struct PromiseSenderOp(T, Receiver) { - import concurrency.stoptoken; - import concurrency.bitfield; - private enum Flags : size_t { - locked = 0x0, - setup = 0x1, - value = 0x2, - stop = 0x4 - } - alias Sender = Promise!T; - alias InternalValue = Sender.InternalValue; - shared Sender parent; - Receiver receiver; - StopCallback cb; - shared SharedBitField!Flags bitfield; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @trusted scope { - // if already completed we can optimize - if (parent.isCompleted) { - bitfield.add(Flags.setup); - parent.add(&(cast(shared)this).onValue); - return; - } - // Otherwise we have to be a bit careful here, - // both the onStop and the onValue we register - // can be called from possibly different contexts. - // We can't atomically connect both, so we have to - // devise a scheme to handle one or both being called - // before we are done here. - - // we use a simple atomic bitfield that we set after setup - // is done. If `onValue` or `onStop` trigger before setup - // is complete, they update the bitfield and return early. - // After we setup both, we flip the setup bit and check - // if any of the callbacks triggered in the meantime, - // if they did we know we have to perform some cleanup - // if they didn't the callbacks themselves will handle it - - bool triggeredInline = parent.add(&(cast(shared)this).onValue); - // if triggeredInline there is no point in setting up the stop callback - if (!triggeredInline) - cb = receiver.getStopToken.onStop(&(cast(shared)this).onStop); - - with (bitfield.add(Flags.setup)) { - if (has(Flags.stop)) { - // it stopped before we finished setup - parent.remove(&(cast(shared)this).onValue); - receiver.setDone(); - } - if (has(Flags.value)) { - // it fired before we finished setup - // just add it again, it will fire again - parent.add(&(cast(shared)this).onValue); - } - } - } - void onStop() nothrow @trusted shared { - // we toggle the stop bit and return early if setup bit isn't set - with (bitfield.add(Flags.stop)) - if (!has(Flags.setup)) - return; - with(unshared) { - // If `parent.remove` returns true, onValue will never be called, - // so we can call setDone ourselves. - // If it returns false onStop and onValue are in a race, and we - // let onValue pass. - if (parent.remove(&(cast(shared)this).onValue)) - receiver.setDone(); - } - } - void onValue(InternalValue value) nothrow @safe shared { - import mir.algebraic : match; - // we toggle the stop bit and return early if setup bit isn't set - with (bitfield.add(Flags.value)) - if (!has(Flags.setup)) - return; - with(unshared) { - // `cb.dispose` will ensure onStop will never be called - // after it returns. It will also block if it is currently - // being executed. - // This means that when it completes we are the only one - // calling the receiver's termination functions. - if (cb) - cb.dispose(); - value.match!((Sender.ValueRep v){ - try { - static if (is(Sender.Value == void)) - receiver.setValue(); - else - receiver.setValue(v); - } catch (Exception e) { - receiver.setError(e); - } - }, (Throwable e){ - receiver.setError(e); - }, (Sender.Done d){ - receiver.setDone(); - }); - } - } - private auto ref unshared() @trusted nothrow shared { - return cast()this; - } + import concurrency.stoptoken; + import concurrency.bitfield; + private enum Flags : size_t { + locked = 0x0, + setup = 0x1, + value = 0x2, + stop = 0x4 + } + + alias Sender = Promise!T; + alias InternalValue = Sender.InternalValue; + shared Sender parent; + Receiver receiver; + StopCallback cb; + shared SharedBitField!Flags bitfield; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @trusted scope { + // if already completed we can optimize + if (parent.isCompleted) { + bitfield.add(Flags.setup); + parent.add(&(cast(shared) this).onValue); + return; + } + + // Otherwise we have to be a bit careful here, + // both the onStop and the onValue we register + // can be called from possibly different contexts. + // We can't atomically connect both, so we have to + // devise a scheme to handle one or both being called + // before we are done here. + + // we use a simple atomic bitfield that we set after setup + // is done. If `onValue` or `onStop` trigger before setup + // is complete, they update the bitfield and return early. + // After we setup both, we flip the setup bit and check + // if any of the callbacks triggered in the meantime, + // if they did we know we have to perform some cleanup + // if they didn't the callbacks themselves will handle it + + bool triggeredInline = parent.add(&(cast(shared) this).onValue); + // if triggeredInline there is no point in setting up the stop callback + if (!triggeredInline) + cb = receiver.getStopToken.onStop(&(cast(shared) this).onStop); + + with (bitfield.add(Flags.setup)) { + if (has(Flags.stop)) { + // it stopped before we finished setup + parent.remove(&(cast(shared) this).onValue); + receiver.setDone(); + } + + if (has(Flags.value)) { + // it fired before we finished setup + // just add it again, it will fire again + parent.add(&(cast(shared) this).onValue); + } + } + } + + void onStop() nothrow @trusted shared { + // we toggle the stop bit and return early if setup bit isn't set + with (bitfield.add(Flags.stop)) if (!has(Flags.setup)) + return; + with (unshared) { + // If `parent.remove` returns true, onValue will never be called, + // so we can call setDone ourselves. + // If it returns false onStop and onValue are in a race, and we + // let onValue pass. + if (parent.remove(&(cast(shared) this).onValue)) + receiver.setDone(); + } + } + + void onValue(InternalValue value) nothrow @safe shared { + import mir.algebraic : match; + // we toggle the stop bit and return early if setup bit isn't set + with (bitfield.add(Flags.value)) if (!has(Flags.setup)) + return; + with (unshared) { + // `cb.dispose` will ensure onStop will never be called + // after it returns. It will also block if it is currently + // being executed. + // This means that when it completes we are the only one + // calling the receiver's termination functions. + if (cb) + cb.dispose(); + value.match!((Sender.ValueRep v) { + try { + static if (is(Sender.Value == void)) + receiver.setValue(); + else + receiver.setValue(v); + } catch (Exception e) { + receiver.setError(e); + } + }, (Throwable e) { + receiver.setError(e); + }, (Sender.Done d) { + receiver.setDone(); + }); + } + } + + private auto ref unshared() @trusted nothrow shared { + return cast() this; + } } class Promise(T) { - import std.traits : ReturnType; - import concurrency.slist; - import concurrency.bitfield; - import mir.algebraic : Algebraic, match, Nullable; - alias Value = T; - static if (is(Value == void)) { - static struct ValueRep{} - } else - alias ValueRep = Value; - static struct Done{} - alias InternalValue = Algebraic!(Throwable, ValueRep, Done); - alias DG = void delegate(InternalValue) nothrow @safe shared; - private { - shared SList!DG dgs; - Nullable!InternalValue value; - enum Flags { - locked = 0x1, - completed = 0x2 - } - SharedBitField!Flags counter; - bool add(DG dg) @safe nothrow shared { - with(unshared) { - with(counter.lock()) { - if (was(Flags.completed)) { - auto val = value.get; - release(); // release early - dg(val); - return true; - } else { - dgs.pushBack(dg); - return false; - } - } - } - } - bool remove(DG dg) @safe nothrow shared { - with (counter.lock()) { - if (was(Flags.completed)) { - release(); // release early - return false; - } else { - dgs.remove(dg); - return true; - } - } - } - private auto ref unshared() @trusted nothrow shared { - return cast()this; - } - } - private bool pushImpl(P)(P t) @safe shared nothrow { - import std.exception : enforce; - with (counter.lock(Flags.completed)) { - if (was(Flags.completed)) - return false; - InternalValue val = InternalValue(t); - (cast()value) = val; - auto localDgs = dgs.release(); - release(); - foreach(dg; localDgs) - dg(val); - return true; - } - } - bool cancel() @safe shared nothrow { - return pushImpl(Done()); - } - bool error(Throwable e) @safe shared nothrow { - return pushImpl(e); - } - static if (is(Value == void)) { - bool fulfill() @safe shared nothrow { - return pushImpl(ValueRep()); - } - } else { - bool fulfill(T t) @safe shared nothrow { - return pushImpl(t); - } - } - bool isCompleted() @trusted shared nothrow { - import core.atomic : MemoryOrder; - return (counter.load!(MemoryOrder.acq) & Flags.completed) > 0; - } - this() { - this.dgs = new shared SList!DG; - } - auto sender() shared @safe nothrow { - return shared PromiseSender!T(this); - } + import std.traits : ReturnType; + import concurrency.slist; + import concurrency.bitfield; + import mir.algebraic : Algebraic, match, Nullable; + alias Value = T; + static if (is(Value == void)) { + static struct ValueRep {} + } else + alias ValueRep = Value; + static struct Done {} + + alias InternalValue = Algebraic!(Throwable, ValueRep, Done); + alias DG = void delegate(InternalValue) nothrow @safe shared; + private { + shared SList!DG dgs; + Nullable!InternalValue value; + enum Flags { + locked = 0x1, + completed = 0x2 + } + + SharedBitField!Flags counter; + bool add(DG dg) @safe nothrow shared { + with (unshared) { + with (counter.lock()) { + if (was(Flags.completed)) { + auto val = value.get; + release(); // release early + dg(val); + return true; + } else { + dgs.pushBack(dg); + return false; + } + } + } + } + + bool remove(DG dg) @safe nothrow shared { + with (counter.lock()) { + if (was(Flags.completed)) { + release(); // release early + return false; + } else { + dgs.remove(dg); + return true; + } + } + } + + private auto ref unshared() @trusted nothrow shared { + return cast() this; + } + } + + private bool pushImpl(P)(P t) @safe shared nothrow { + import std.exception : enforce; + with (counter.lock(Flags.completed)) { + if (was(Flags.completed)) + return false; + InternalValue val = InternalValue(t); + (cast() value) = val; + auto localDgs = dgs.release(); + release(); + foreach (dg; localDgs) + dg(val); + return true; + } + } + + bool cancel() @safe shared nothrow { + return pushImpl(Done()); + } + + bool error(Throwable e) @safe shared nothrow { + return pushImpl(e); + } + + static if (is(Value == void)) { + bool fulfill() @safe shared nothrow { + return pushImpl(ValueRep()); + } + } else { + bool fulfill(T t) @safe shared nothrow { + return pushImpl(t); + } + } + + bool isCompleted() @trusted shared nothrow { + import core.atomic : MemoryOrder; + return (counter.load!(MemoryOrder.acq) & Flags.completed) > 0; + } + + this() { + this.dgs = new shared SList!DG; + } + + auto sender() shared @safe nothrow { + return shared PromiseSender!T(this); + } } shared(Promise!T) promise(T)() { - return new shared Promise!T(); + return new shared Promise!T(); } struct PromiseSender(T) { - alias Value = T; - static assert(models!(typeof(this), isSender)); - private shared Promise!T promise; - - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = asShared.connect(receiver); - return op; - } - auto connect(Receiver)(return Receiver receiver) @safe shared return scope { - // ensure NRVO - auto op = PromiseSenderOp!(T, Receiver)(promise, receiver); - return op; - } - private auto asShared() @trusted return scope { - return cast(shared)this; - } + alias Value = T; + static assert(models!(typeof(this), isSender)); + private shared Promise!T promise; + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = asShared.connect(receiver); + return op; + } + + auto connect(Receiver)(return Receiver receiver) @safe shared return scope { + // ensure NRVO + auto op = PromiseSenderOp!(T, Receiver)(promise, receiver); + return op; + } + + private auto asShared() @trusted return scope { + return cast(shared) this; + } } struct Defer(Fun) { - import concurrency.utils; - static assert (isThreadSafeCallable!Fun); - alias Sender = typeof(fun()); - static assert(models!(Sender, isSender)); - alias Value = Sender.Value; - Fun fun; - auto connect(Receiver)(return Receiver receiver) @safe { - // ensure NRVO - auto op = fun().connect(receiver); - return op; - } + import concurrency.utils; + static assert(isThreadSafeCallable!Fun); + alias Sender = typeof(fun()); + static assert(models!(Sender, isSender)); + alias Value = Sender.Value; + Fun fun; + auto connect(Receiver)(return Receiver receiver) @safe { + // ensure NRVO + auto op = fun().connect(receiver); + return op; + } } auto defer(Fun)(Fun fun) { - return Defer!(Fun)(fun); + return Defer!(Fun)(fun); } diff --git a/source/concurrency/signal.d b/source/concurrency/signal.d index 038323b..f03b597 100644 --- a/source/concurrency/signal.d +++ b/source/concurrency/signal.d @@ -3,71 +3,72 @@ module concurrency.signal; import concurrency.stoptoken; shared(StopSource) globalStopSource() @trusted { - import core.atomic : atomicLoad, cas; - - if (globalSource.atomicLoad is null) { - import concurrency.utils : dynamicLoad; - auto ptr = getGlobalStopSourcePointer(); - - if (auto source = (*ptr).atomicLoad) { - globalSource = source; - return globalSource; - } - - auto tmp = new shared StopSource(); - if (ptr.cas(cast(shared StopSource)null, tmp)) { - setupCtrlCHandler(tmp); - globalSource = tmp; - } else - globalSource = (*ptr).atomicLoad; - } - return globalSource; + import core.atomic : atomicLoad, cas; + + if (globalSource.atomicLoad is null) { + import concurrency.utils : dynamicLoad; + auto ptr = getGlobalStopSourcePointer(); + + if (auto source = (*ptr).atomicLoad) { + globalSource = source; + return globalSource; + } + + auto tmp = new shared StopSource(); + if (ptr.cas(cast(shared StopSource) null, tmp)) { + setupCtrlCHandler(tmp); + globalSource = tmp; + } else + globalSource = (*ptr).atomicLoad; + } + + return globalSource; } /// Returns true if first to set (otherwise it is ignored) bool setGlobalStopSource(shared StopSource stopSource) @safe { - import core.atomic : cas; - auto ptr = getGlobalStopSourcePointer(); - if (!ptr.cas(cast(shared StopSource)null, stopSource)) - return false; - globalSource = stopSource; - return true; + import core.atomic : cas; + auto ptr = getGlobalStopSourcePointer(); + if (!ptr.cas(cast(shared StopSource) null, stopSource)) + return false; + globalSource = stopSource; + return true; } /// Sets the stopSource to be called when receiving an interrupt void setupCtrlCHandler(shared StopSource stopSource) @trusted { - import core.atomic; - - if (stopSource is null) - return; - - auto old = atomicExchange(&SignalHandler.signalStopSource, stopSource); - if (old !is null) - return; - - SignalHandler.setup(); - SignalHandler.launchHandlerThread(); - version (Windows) { - import core.sys.windows.windows; - SetConsoleCtrlHandler(&signalHandler, true); - } else { - import core.sys.posix.signal; - - static void handleSignal(int s) @trusted { - sigaction_t old; - sigset_t sigset; - sigemptyset(&sigset); - sigaction_t siginfo; - siginfo.sa_handler = &signalHandler; - siginfo.sa_mask = sigset; - siginfo.sa_flags = SA_RESTART; - sigaction(s, &siginfo, &old); - // TODO: what to do with old? - } - - handleSignal(SIGINT); - handleSignal(SIGTERM); - } + import core.atomic; + + if (stopSource is null) + return; + + auto old = atomicExchange(&SignalHandler.signalStopSource, stopSource); + if (old !is null) + return; + + SignalHandler.setup(); + SignalHandler.launchHandlerThread(); + version(Windows) { + import core.sys.windows.windows; + SetConsoleCtrlHandler(&signalHandler, true); + } else { + import core.sys.posix.signal; + + static void handleSignal(int s) @trusted { + sigaction_t old; + sigset_t sigset; + sigemptyset(&sigset); + sigaction_t siginfo; + siginfo.sa_handler = &signalHandler; + siginfo.sa_mask = sigset; + siginfo.sa_flags = SA_RESTART; + sigaction(s, &siginfo, &old); + // TODO: what to do with old? + } + + handleSignal(SIGINT); + handleSignal(SIGTERM); + } } private static shared StopSource globalSource; @@ -76,122 +77,136 @@ private static shared StopSource globalSource; // the host's globalStopSource pointer. // Otherwise they would access their own local instance. // should not be called directly by usercode, instead use `globalStopSource`. -export extern(C) shared(StopSource*) concurrency_globalStopSourcePointer() @safe { - return &globalSource; +export extern(C) +shared(StopSource*) concurrency_globalStopSourcePointer() @safe { + return &globalSource; } private shared(StopSource*) getGlobalStopSourcePointer() @safe { - import concurrency.utils : dynamicLoad; - return dynamicLoad!concurrency_globalStopSourcePointer()(); + import concurrency.utils : dynamicLoad; + return dynamicLoad!concurrency_globalStopSourcePointer()(); } struct SignalHandler { - import core.atomic : atomicStore, atomicLoad, MemoryOrder, atomicExchange; - import core.thread : Thread; - static shared int lastSignal; // last signal received - enum int ABORT = -1; - version (Windows) { - import core.sync.event : Event; - private static shared Event event; // used to notify the dedicated thread to shutdown - static void notify(int num) nothrow @nogc @trusted { - lastSignal.atomicStore!(MemoryOrder.rel)(num); - (cast()event).set(); - } - private static int await() nothrow @nogc @trusted { - (cast()event).wait(); - return lastSignal.atomicLoad!(MemoryOrder.acq)(); - } - private static void setup() @trusted { - (cast()event).initialize(false, false); - } - } else version (linux) { - import core.sys.posix.unistd : write, read; - private static shared int event; // eventfd to notify dedicated thread - static void notify(int num) nothrow @nogc { - lastSignal.atomicStore!(MemoryOrder.rel)(num); - ulong b = 1; - write(event, &b, typeof(b).sizeof); - } - private static int await() nothrow @nogc { - ulong b; - while(read(event, &b, typeof(b).sizeof) != typeof(b).sizeof) {} - return lastSignal.atomicLoad!(MemoryOrder.acq)(); - } - private static void setup() { - import core.sys.linux.sys.eventfd; - event = eventfd(0, EFD_CLOEXEC); - } - } else version (Posix) { - import core.sys.posix.unistd : write, read, pipe; - private static shared int[2] selfPipe; // self pipe to notify dedicated thread - static void notify(int num) nothrow @nogc { - lastSignal.atomicStore!(MemoryOrder.rel)(num); - ulong b = 1; - write(selfPipe[1], &b, typeof(b).sizeof); - } - private static int await() nothrow @nogc { - ulong b; - while(read(cast()selfPipe[0], &b, typeof(b).sizeof) != typeof(b).sizeof) {} - return lastSignal.atomicLoad!(MemoryOrder.acq)(); - } - private static void setup() { - import std.exception : ErrnoException; - if (pipe(cast(int[2])selfPipe) == -1) - throw new ErrnoException("Failed to create self-pipe"); - } - } - private static void shutdown() { - if (atomicLoad!(MemoryOrder.acq)(signalStopSource) !is null) - SignalHandler.notify(ABORT); - } - private static shared StopSource signalStopSource; - private static shared Thread handlerThread; - private static void launchHandlerThread() { - if (handlerThread.atomicLoad !is null) - return; - - auto thread = new Thread((){ - for(;;) { - if (SignalHandler.await() == ABORT) { - return; - } - signalStopSource.stop(); - } - }); - // This has to be a daemon thread otherwise the runtime will wait on it before calling the shared module destructor that stops it. - thread.isDaemon = true; - - if (atomicExchange(&handlerThread, cast(shared)thread) !is null) - return; // someone beat us to it - - thread.start(); - } + import core.atomic : atomicStore, atomicLoad, MemoryOrder, atomicExchange; + import core.thread : Thread; + static shared int lastSignal; // last signal received + enum int ABORT = -1; + version(Windows) { + import core.sync.event : Event; + private static shared + Event event; // used to notify the dedicated thread to shutdown + static void notify(int num) nothrow @nogc @trusted { + lastSignal.atomicStore!(MemoryOrder.rel)(num); + (cast() event).set(); + } + + private static int await() nothrow @nogc @trusted { + (cast() event).wait(); + return lastSignal.atomicLoad!(MemoryOrder.acq)(); + } + + private static void setup() @trusted { + (cast() event).initialize(false, false); + } + } else version(linux) { + import core.sys.posix.unistd : write, read; + private static shared int event; // eventfd to notify dedicated thread + static void notify(int num) nothrow @nogc { + lastSignal.atomicStore!(MemoryOrder.rel)(num); + ulong b = 1; + write(event, &b, typeof(b).sizeof); + } + + private static int await() nothrow @nogc { + ulong b; + while (read(event, &b, typeof(b).sizeof) != typeof(b).sizeof) {} + return lastSignal.atomicLoad!(MemoryOrder.acq)(); + } + + private static void setup() { + import core.sys.linux.sys.eventfd; + event = eventfd(0, EFD_CLOEXEC); + } + } else version(Posix) { + import core.sys.posix.unistd : write, read, pipe; + private static shared + int[2] selfPipe; // self pipe to notify dedicated thread + static void notify(int num) nothrow @nogc { + lastSignal.atomicStore!(MemoryOrder.rel)(num); + ulong b = 1; + write(selfPipe[1], &b, typeof(b).sizeof); + } + + private static int await() nothrow @nogc { + ulong b; + while (read(cast() selfPipe[0], &b, typeof(b).sizeof) + != typeof(b).sizeof) {} + return lastSignal.atomicLoad!(MemoryOrder.acq)(); + } + + private static void setup() { + import std.exception : ErrnoException; + if (pipe(cast(int[2]) selfPipe) == -1) + throw new ErrnoException("Failed to create self-pipe"); + } + } + + private static void shutdown() { + if (atomicLoad!(MemoryOrder.acq)(signalStopSource) !is null) + SignalHandler.notify(ABORT); + } + + private static shared StopSource signalStopSource; + private static shared Thread handlerThread; + private static void launchHandlerThread() { + if (handlerThread.atomicLoad !is null) + return; + + auto thread = new Thread(() { + for (;;) { + if (SignalHandler.await() == ABORT) { + return; + } + + signalStopSource.stop(); + } + }); + // This has to be a daemon thread otherwise the runtime will wait on it before calling the shared module destructor that stops it. + thread.isDaemon = true; + + if (atomicExchange(&handlerThread, cast(shared) thread) !is null) + return; // someone beat us to it + + thread.start(); + } } /// This is required to properly shutdown in the presence of sanitizers shared static ~this() { - import core.atomic : atomicExchange; - import core.thread : Thread; - SignalHandler.shutdown(); - if (auto thread = atomicExchange(&SignalHandler.handlerThread, null)) - (cast()thread).join(); + import core.atomic : atomicExchange; + import core.thread : Thread; + SignalHandler.shutdown(); + if (auto thread = atomicExchange(&SignalHandler.handlerThread, null)) + (cast() thread).join(); } -version (Windows) { - import core.sys.windows.windows; - extern (Windows) static BOOL signalHandler(DWORD dwCtrlType) nothrow @system { - import core.stdc.signal; - if (dwCtrlType == CTRL_C_EVENT || - dwCtrlType == CTRL_BREAK_EVENT || - dwCtrlType == CTRL_CLOSE_EVENT || - dwCtrlType == CTRL_SHUTDOWN_EVENT) { - SignalHandler.notify(SIGINT); - return TRUE; - } - return FALSE; - } +version(Windows) { + import core.sys.windows.windows; + extern(Windows) static + BOOL signalHandler(DWORD dwCtrlType) nothrow @system { + import core.stdc.signal; + if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT + || dwCtrlType == CTRL_CLOSE_EVENT + || dwCtrlType == CTRL_SHUTDOWN_EVENT) { + SignalHandler.notify(SIGINT); + return TRUE; + } + + return FALSE; + } } else { - extern (C) static void signalHandler(int i) nothrow @nogc { - SignalHandler.notify(i); - } + extern(C) static void signalHandler(int i) nothrow @nogc { + SignalHandler.notify(i); + } } diff --git a/source/concurrency/slist.d b/source/concurrency/slist.d index 48d324a..db77249 100644 --- a/source/concurrency/slist.d +++ b/source/concurrency/slist.d @@ -5,135 +5,145 @@ import concurrency.utils : casWeak; /// A lock-free single linked list shared final class SList(T) { - struct Node { - shared(Node*) next; - T payload; - } - Node* head; - - void pushFront(T payload) @trusted { - auto node = new shared Node(atomicLoad(head), cast(shared(T))payload); - while (!casWeak(&head, node.next, node)) - node.next = atomicLoad(head); - } - - void pushBack(T payload) @trusted { - auto node = new shared Node(null, cast(shared(T))payload); - while (true) { - auto last = getLast(); - if (last is null) { - if (casWeak(&head, cast(shared(Node*))null, node)) - return; - } else { - auto lastNext = atomicLoad(last.next); - if (!isMarked(lastNext) && casWeak(&last.next, cast(shared(Node*))null, node)) - return; - } - } - } - - auto release() @safe { - auto old = atomicExchange(&head, cast(shared(Node*))null); - return Iterator!T(old); - } - - private shared(Node)* getLast() @trusted { - auto current = atomicLoad(head); - if (current is null) - return null; - while (true) { - auto next = clearMark(atomicLoad(current.next)); - if (next is null) - break; - current = next; - } - return current; - } - - auto opSlice() return @safe { - return Iterator!T(atomicLoad(head)); - } - - bool remove(T payload) @trusted { - auto iter = this[]; - while (!iter.empty) { - if (iter.front == cast(shared(T))payload) { - if (remove(iter.node)) - return true; - } - iter.popFront(); - } - return false; - } - - private bool remove(shared(Node*) node) @trusted { - // step 1, mark the next ptr to signify this one is logically deleted - shared(Node*) ptr; - shared(Node*)* currentNext; - do { - ptr = atomicLoad(node.next); - if (isMarked(ptr)) - return false; - } while (!casWeak(&node.next, ptr, mark(ptr))); - // step 2, iterate until next points to node, then cas - retry: - currentNext = &head; - while (true) { - ptr = atomicLoad(*currentNext); - if (clearMark(ptr) is null) - return false; - if (clearMark(ptr) is node) { - if (isMarked(ptr)) - goto retry; - - if (casWeak(currentNext, ptr, clearMark(node.next))) - return true; - goto retry; - } - currentNext = &clearMark(ptr).next; - } - assert(0); - } + struct Node { + shared(Node*) next; + T payload; + } + + Node* head; + + void pushFront(T payload) @trusted { + auto node = new shared Node(atomicLoad(head), cast(shared(T)) payload); + while (!casWeak(&head, node.next, node)) + node.next = atomicLoad(head); + } + + void pushBack(T payload) @trusted { + auto node = new shared Node(null, cast(shared(T)) payload); + while (true) { + auto last = getLast(); + if (last is null) { + if (casWeak(&head, cast(shared(Node*)) null, node)) + return; + } else { + auto lastNext = atomicLoad(last.next); + if (!isMarked(lastNext) + && casWeak(&last.next, cast(shared(Node*)) null, node)) + return; + } + } + } + + auto release() @safe { + auto old = atomicExchange(&head, cast(shared(Node*)) null); + return Iterator!T(old); + } + + private shared(Node)* getLast() @trusted { + auto current = atomicLoad(head); + if (current is null) + return null; + while (true) { + auto next = clearMark(atomicLoad(current.next)); + if (next is null) + break; + current = next; + } + + return current; + } + + auto opSlice() return @safe { + return Iterator!T(atomicLoad(head)); + } + + bool remove(T payload) @trusted { + auto iter = this[]; + while (!iter.empty) { + if (iter.front == cast(shared(T)) payload) { + if (remove(iter.node)) + return true; + } + + iter.popFront(); + } + + return false; + } + + private bool remove(shared(Node*) node) @trusted { + // step 1, mark the next ptr to signify this one is logically deleted + shared(Node*) ptr; + shared(Node*)* currentNext; + do { + ptr = atomicLoad(node.next); + if (isMarked(ptr)) + return false; + } while (!casWeak(&node.next, ptr, mark(ptr))); + + // step 2, iterate until next points to node, then cas + retry: + currentNext = &head; + while (true) { + ptr = atomicLoad(*currentNext); + if (clearMark(ptr) is null) + return false; + if (clearMark(ptr) is node) { + if (isMarked(ptr)) + goto retry; + + if (casWeak(currentNext, ptr, clearMark(node.next))) + return true; + goto retry; + } + + currentNext = &clearMark(ptr).next; + } + + assert(0); + } } -static bool isMarked(T)(T* p) -{ - return (cast(size_t)p & 1) != 0; +static bool isMarked(T)(T* p) { + return (cast(size_t) p & 1) != 0; } -static T* mark(T)(T* p) -{ - return cast(T*)(cast(size_t)p | 1); +static T* mark(T)(T* p) { + return cast(T*) (cast(size_t) p | 1); } -static T* clearMark(T)(T* p) -{ - return cast(T*)(cast(size_t)p & ~1); +static T* clearMark(T)(T* p) { + return cast(T*) (cast(size_t) p & ~1); } struct Iterator(T) { - alias Node = SList!T.Node; - shared(Node*) current; - this(shared(Node*) head) @safe nothrow { - current = head; - } - bool empty() @trusted nothrow { - if (current is null) - return true; - while (isMarked(current.next)) { - current = clearMark(atomicLoad(current.next)); - if (current is null) - return true; - } - return false; - } - shared(T) front() @safe nothrow { - return current.payload; - } - shared(Node*) node() @safe nothrow { - return current; - } - void popFront() @trusted nothrow { - current = clearMark(atomicLoad(current.next)); - } + alias Node = SList!T.Node; + shared(Node*) current; + this(shared(Node*) head) @safe nothrow { + current = head; + } + + bool empty() @trusted nothrow { + if (current is null) + return true; + while (isMarked(current.next)) { + current = clearMark(atomicLoad(current.next)); + if (current is null) + return true; + } + + return false; + } + + shared(T) front() @safe nothrow { + return current.payload; + } + + shared(Node*) node() @safe nothrow { + return current; + } + + void popFront() @trusted nothrow { + current = clearMark(atomicLoad(current.next)); + } } diff --git a/source/concurrency/stoptoken.d b/source/concurrency/stoptoken.d index ec98403..c0ed789 100644 --- a/source/concurrency/stoptoken.d +++ b/source/concurrency/stoptoken.d @@ -4,414 +4,444 @@ module concurrency.stoptoken; // it is licensed under the Creative Commons Attribution 4.0 Internation License http://creativecommons.org/licenses/by/4.0 class StopSource { - private stop_state state; - bool stop() nothrow @safe { - return state.request_stop(); - } - - bool stop() nothrow @trusted shared { - return (cast(StopSource)this).state.request_stop(); - } - - bool isStopRequested() nothrow @safe @nogc { - return state.is_stop_requested(); - } - - bool isStopRequested() nothrow @trusted @nogc shared { - return (cast(StopSource)this).isStopRequested(); - } - - /// resets the internal state, only do this if you are sure nothing else is looking at this... - void reset(this t)() @system @nogc { - this.state = stop_state(); - } + private stop_state state; + bool stop() nothrow @safe { + return state.request_stop(); + } + + bool stop() nothrow @trusted shared { + return (cast(StopSource) this).state.request_stop(); + } + + bool isStopRequested() nothrow @safe @nogc { + return state.is_stop_requested(); + } + + bool isStopRequested() nothrow @trusted @nogc shared { + return (cast(StopSource) this).isStopRequested(); + } + + /// resets the internal state, only do this if you are sure nothing else is looking at this... + void reset(this t)() @system @nogc { + this.state = stop_state(); + } } struct StopToken { - package(concurrency) StopSource source; - this(StopSource source) nothrow @safe @nogc { - this.source = source; - isStopPossible = source !is null; - } - - this(shared StopSource source) nothrow @trusted @nogc { - this.source = cast()source; - isStopPossible = source !is null; - } - - bool isStopRequested() nothrow @safe @nogc { - return isStopPossible && source.isStopRequested(); - } - - const bool isStopPossible; + package(concurrency) StopSource source; + this(StopSource source) nothrow @safe @nogc { + this.source = source; + isStopPossible = source !is null; + } + + this(shared StopSource source) nothrow @trusted @nogc { + this.source = cast() source; + isStopPossible = source !is null; + } + + bool isStopRequested() nothrow @safe @nogc { + return isStopPossible && source.isStopRequested(); + } + + const bool isStopPossible; } struct NeverStopToken { - enum isStopRequested = false; - enum isStopPossible = false; + enum isStopRequested = false; + enum isStopPossible = false; } -StopCallback onStop(StopSource stopSource, void delegate() nothrow @safe shared callback) nothrow @safe { - auto cb = new StopCallback(callback); - return onStop(stopSource, cb); +StopCallback onStop( + StopSource stopSource, + void delegate() nothrow @safe shared callback +) nothrow @safe { + auto cb = new StopCallback(callback); + return onStop(stopSource, cb); } -StopCallback onStop(StopSource stopSource, void function() nothrow @safe callback) nothrow @trusted { - import std.functional : toDelegate; - return stopSource.onStop(cast(void delegate() nothrow @safe shared)callback.toDelegate); +StopCallback onStop(StopSource stopSource, + void function() nothrow @safe callback) nothrow @trusted { + import std.functional : toDelegate; + return stopSource + .onStop(cast(void delegate() nothrow @safe shared) callback.toDelegate); } -StopCallback onStop(StopToken)(StopToken stopToken, void delegate() nothrow @safe shared callback) nothrow @safe { - if (stopToken.isStopPossible) { - return stopToken.source.onStop(callback); - } - return new StopCallback(callback); +StopCallback onStop(StopToken)( + StopToken stopToken, + void delegate() nothrow @safe shared callback +) nothrow @safe { + if (stopToken.isStopPossible) { + return stopToken.source.onStop(callback); + } + + return new StopCallback(callback); } -StopCallback onStop(StopToken)(StopToken stopToken, void function() nothrow @safe callback) nothrow @trusted { - import std.functional : toDelegate; - return stopToken.onStop(cast(void delegate() nothrow @safe shared)callback.toDelegate); +StopCallback onStop(StopToken)( + StopToken stopToken, + void function() nothrow @safe callback +) nothrow @trusted { + import std.functional : toDelegate; + return stopToken + .onStop(cast(void delegate() nothrow @safe shared) callback.toDelegate); } -StopCallback onStop(StopToken)(StopToken stopToken, StopCallback cb) nothrow @safe { - if (stopToken.isStopPossible) { - return stopToken.source.onStop(cb); - } - return cb; +StopCallback onStop(StopToken)(StopToken stopToken, + StopCallback cb) nothrow @safe { + if (stopToken.isStopPossible) { + return stopToken.source.onStop(cb); + } + + return cb; } StopCallback onStop(StopSource stopSource, StopCallback cb) nothrow @safe { - if (stopSource.state.try_add_callback(cb, true)) - cb.source = stopSource; - return cb; + if (stopSource.state.try_add_callback(cb, true)) + cb.source = stopSource; + return cb; } class StopCallback { - void dispose() nothrow @trusted @nogc { - import core.atomic : cas; - - if (source is null) - return; - auto local = source; - static if (__traits(compiles, cas(&source, local, null))) { - if (!cas(&source, local, null)) { - assert(source is null); - return; - } - } else { - if (!cas(cast(shared)&source, cast(shared)local, null)) { - assert(source is null); - return; - } - } - local.state.remove_callback(this); - } - void dispose() shared nothrow @trusted @nogc { - (cast()this).dispose(); - } - - this(void delegate() nothrow shared @safe callback) nothrow @safe @nogc { - this.callback = callback; - } + void dispose() nothrow @trusted @nogc { + import core.atomic : cas; + + if (source is null) + return; + auto local = source; + static if (__traits(compiles, cas(&source, local, null))) { + if (!cas(&source, local, null)) { + assert(source is null); + return; + } + } else { + if (!cas(cast(shared) &source, cast(shared) local, null)) { + assert(source is null); + return; + } + } + + local.state.remove_callback(this); + } + + void dispose() shared nothrow @trusted @nogc { + (cast() this).dispose(); + } + + this(void delegate() nothrow shared @safe callback) nothrow @safe @nogc { + this.callback = callback; + } + private: - void delegate() nothrow shared @safe callback; - StopSource source; + void delegate() nothrow shared @safe callback; + StopSource source; - StopCallback next_ = null; - StopCallback* prev_ = null; - bool* isRemoved_ = null; - shared bool callbackFinishedExecuting = false; + StopCallback next_ = null; + StopCallback* prev_ = null; + bool* isRemoved_ = null; + shared bool callbackFinishedExecuting = false; - void execute() nothrow @safe { - callback(); - } + void execute() nothrow @safe { + callback(); + } } deprecated("Use regular StopToken") alias StopTokenObject = StopToken; auto stopTokenObject(StopToken stopToken) { - return stopToken; + return stopToken; } auto stopTokenObject(NeverStopToken stopToken) { - StopSource s = null; - return StopToken(s); + StopSource s = null; + return StopToken(s); } private void spin_yield() nothrow @trusted @nogc { - // TODO: could use the pause asm instruction - // it is available in LDC as intrinsic... but not in DMD - import core.thread : Thread; + // TODO: could use the pause asm instruction + // it is available in LDC as intrinsic... but not in DMD + import core.thread : Thread; - Thread.yield(); + Thread.yield(); } private struct stop_state { - import core.thread : Thread; - import core.atomic : atomicStore, atomicLoad, MemoryOrder, atomicOp; - - static if (__traits(compiles, () { import core.atomic : casWeak; }) && __traits(compiles, () { - import core.internal.atomic : atomicCompareExchangeWeakNoResult; - })) - import core.atomic : casWeak; - else - auto casWeak(MemoryOrder M1, MemoryOrder M2, T, V1, V2)(T* here, V1 ifThis, V2 writeThis) pure nothrow @nogc @safe { - import core.atomic : cas; - - static if (__traits(compiles, cas!(M1, M2)(here, ifThis, writeThis))) - return cas!(M1, M2)(here, ifThis, writeThis); - else - return cas(here, ifThis, writeThis); - } + import core.thread : Thread; + import core.atomic : atomicStore, atomicLoad, MemoryOrder, atomicOp; + + static if (__traits(compiles, () { + import core.atomic : casWeak; + }) && __traits(compiles, () { + import core.internal.atomic + : atomicCompareExchangeWeakNoResult; + })) + import core.atomic : casWeak; + else + auto casWeak(MemoryOrder M1, MemoryOrder M2, T, V1, V2)( + T* here, + V1 ifThis, + V2 writeThis + ) pure nothrow @nogc @safe { + import core.atomic : cas; + + static if (__traits(compiles, + cas!(M1, M2)(here, ifThis, writeThis))) + return cas!(M1, M2)(here, ifThis, writeThis); + else + return cas(here, ifThis, writeThis); + } public: - void add_token_reference() nothrow @safe @nogc { - // TODO: want to use atomicFetchAdd but (proper) support is only recent - // state_.atomicFetchAdd!(MemoryOrder.raw)(token_ref_increment); - state_.atomicOp!"+="(token_ref_increment); - } - - void remove_token_reference() nothrow @safe @nogc { - // TODO: want to use atomicFetchSub but (proper) support is only recent - // state_.atomicFetchSub!(MemoryOrder.acq_rel)(token_ref_increment); - state_.atomicOp!"-="(token_ref_increment); - } - - void add_source_reference() nothrow @safe @nogc { - // TODO: want to use atomicFetchAdd but (proper) support is only recent - // state_.atomicFetchAdd!(MemoryOrder.raw)(source_ref_increment); - state_.atomicOp!"+="(source_ref_increment); - } - - void remove_source_reference() nothrow @safe @nogc { - // TODO: want to use atomicFetchSub but (proper) support is only recent - // state_.atomicFetchSub!(MemoryOrder.acq_rel)(source_ref_increment); - state_.atomicOp!"-="(source_ref_increment); - } - - bool request_stop() nothrow @safe { - - if (!try_lock_and_signal_until_signalled()) { - // Stop has already been requested. - return false; - } - - // Set the 'stop_requested' signal and acquired the lock. - - signallingThread_ = Thread.getThis(); - - while (head_ !is null) { - // Dequeue the head of the queue - auto cb = head_; - head_ = cb.next_; - const bool anyMore = head_ !is null; - if (anyMore) { - (() @trusted => head_.prev_ = &head_)(); // compiler 2.091.1 complains "address of variable this assigned to this with longer lifetime". But this is this, how can it have a longer lifetime... - } - // Mark this item as removed from the list. - cb.prev_ = null; - - // Don't hold lock while executing callback - // so we don't block other threads from deregistering callbacks. - unlock(); - - // TRICKY: Need to store a flag on the stack here that the callback - // can use to signal that the destructor was executed inline - // during the call. If the destructor was executed inline then - // it's not safe to dereference cb after execute() returns. - // If the destructor runs on some other thread then the other - // thread will block waiting for this thread to signal that the - // callback has finished executing. - bool isRemoved = false; - (() @trusted => cb.isRemoved_ = &isRemoved)(); // the pointer to the stack here is removed 3 lines down. - - cb.execute(); - - if (!isRemoved) { - cb.isRemoved_ = null; - cb.callbackFinishedExecuting.atomicStore!(MemoryOrder.rel)(true); - } - - if (!anyMore) { - // This was the last item in the queue when we dequeued it. - // No more items should be added to the queue after we have - // marked the state as interrupted, only removed from the queue. - // Avoid acquring/releasing the lock in this case. - return true; - } - - lock(); - } - - unlock(); - - return true; - } - - bool is_stop_requested() nothrow @safe @nogc { - return is_stop_requested(state_.atomicLoad!(MemoryOrder.acq)); - } - - bool is_stop_requestable() nothrow @safe @nogc { - return is_stop_requestable(state_.atomicLoad!(MemoryOrder.acq)); - } - - bool try_add_callback(StopCallback cb, bool incrementRefCountIfSuccessful) nothrow @safe { - ulong oldState; - do { - goto load_state; - do { - spin_yield(); - load_state: - oldState = state_.atomicLoad!(MemoryOrder.acq); - if (is_stop_requested(oldState)) { - cb.execute(); - return false; - } - else if (!is_stop_requestable(oldState)) { - return false; - } - } - while (is_locked(oldState)); - } - while (!casWeak!(MemoryOrder.acq, MemoryOrder.acq)(&state_, oldState, oldState | locked_flag)); - - // Push callback onto callback list. - cb.next_ = head_; - if (cb.next_ !is null) { - cb.next_.prev_ = &cb.next_; - } - () @trusted { cb.prev_ = &head_; } (); - head_ = cb; - - if (incrementRefCountIfSuccessful) { - unlock_and_increment_token_ref_count(); - } - else { - unlock(); - } - - // Successfully added the callback. - return true; - } - - void remove_callback(StopCallback cb) nothrow @safe @nogc { - lock(); - - if (cb.prev_ !is null) { - // Still registered, not yet executed - // Just remove from the list. - *cb.prev_ = cb.next_; - if (cb.next_ !is null) { - cb.next_.prev_ = cb.prev_; - } - - unlock_and_decrement_token_ref_count(); - - return; - } - - unlock(); - - // Callback has either already executed or is executing - // concurrently on another thread. - - if (signallingThread_ is Thread.getThis()) { - // Callback executed on this thread or is still currently executing - // and is deregistering itself from within the callback. - if (cb.isRemoved_ !is null) { - // Currently inside the callback, let the request_stop() method - // know the object is about to be destructed and that it should - // not try to access the object when the callback returns. - *cb.isRemoved_ = true; - } - } - else { - // Callback is currently executing on another thread, - // block until it finishes executing. - while (!cb.callbackFinishedExecuting.atomicLoad!(MemoryOrder.acq)) { - spin_yield(); - } - } - - remove_token_reference(); - } + void add_token_reference() nothrow @safe @nogc { + // TODO: want to use atomicFetchAdd but (proper) support is only recent + // state_.atomicFetchAdd!(MemoryOrder.raw)(token_ref_increment); + state_.atomicOp!"+="(token_ref_increment); + } + + void remove_token_reference() nothrow @safe @nogc { + // TODO: want to use atomicFetchSub but (proper) support is only recent + // state_.atomicFetchSub!(MemoryOrder.acq_rel)(token_ref_increment); + state_.atomicOp!"-="(token_ref_increment); + } + + void add_source_reference() nothrow @safe @nogc { + // TODO: want to use atomicFetchAdd but (proper) support is only recent + // state_.atomicFetchAdd!(MemoryOrder.raw)(source_ref_increment); + state_.atomicOp!"+="(source_ref_increment); + } + + void remove_source_reference() nothrow @safe @nogc { + // TODO: want to use atomicFetchSub but (proper) support is only recent + // state_.atomicFetchSub!(MemoryOrder.acq_rel)(source_ref_increment); + state_.atomicOp!"-="(source_ref_increment); + } + + bool request_stop() nothrow @safe { + if (!try_lock_and_signal_until_signalled()) { + // Stop has already been requested. + return false; + } + + // Set the 'stop_requested' signal and acquired the lock. + + signallingThread_ = Thread.getThis(); + + while (head_ !is null) { + // Dequeue the head of the queue + auto cb = head_; + head_ = cb.next_; + const bool anyMore = head_ !is null; + if (anyMore) { + (() @trusted => head_.prev_ = + &head_)(); // compiler 2.091.1 complains "address of variable this assigned to this with longer lifetime". But this is this, how can it have a longer lifetime... + } + + // Mark this item as removed from the list. + cb.prev_ = null; + + // Don't hold lock while executing callback + // so we don't block other threads from deregistering callbacks. + unlock(); + + // TRICKY: Need to store a flag on the stack here that the callback + // can use to signal that the destructor was executed inline + // during the call. If the destructor was executed inline then + // it's not safe to dereference cb after execute() returns. + // If the destructor runs on some other thread then the other + // thread will block waiting for this thread to signal that the + // callback has finished executing. + bool isRemoved = false; + (() @trusted => cb.isRemoved_ = + &isRemoved)(); // the pointer to the stack here is removed 3 lines down. + + cb.execute(); + + if (!isRemoved) { + cb.isRemoved_ = null; + cb.callbackFinishedExecuting + .atomicStore!(MemoryOrder.rel)(true); + } + + if (!anyMore) { + // This was the last item in the queue when we dequeued it. + // No more items should be added to the queue after we have + // marked the state as interrupted, only removed from the queue. + // Avoid acquring/releasing the lock in this case. + return true; + } + + lock(); + } + + unlock(); + + return true; + } + + bool is_stop_requested() nothrow @safe @nogc { + return is_stop_requested(state_.atomicLoad!(MemoryOrder.acq)); + } + + bool is_stop_requestable() nothrow @safe @nogc { + return is_stop_requestable(state_.atomicLoad!(MemoryOrder.acq)); + } + + bool try_add_callback(StopCallback cb, + bool incrementRefCountIfSuccessful) nothrow @safe { + ulong oldState; + do { + goto load_state; + do { + spin_yield(); + + load_state: + oldState = state_.atomicLoad!(MemoryOrder.acq); + if (is_stop_requested(oldState)) { + cb.execute(); + return false; + } else if (!is_stop_requestable(oldState)) { + return false; + } + } while (is_locked(oldState)); + } while (!casWeak!(MemoryOrder.acq, MemoryOrder.acq)( + &state_, oldState, oldState | locked_flag)); + + // Push callback onto callback list. + cb.next_ = head_; + if (cb.next_ !is null) { + cb.next_.prev_ = &cb.next_; + } + + () @trusted { + cb.prev_ = &head_; + }(); + head_ = cb; + + if (incrementRefCountIfSuccessful) { + unlock_and_increment_token_ref_count(); + } else { + unlock(); + } + + // Successfully added the callback. + return true; + } + + void remove_callback(StopCallback cb) nothrow @safe @nogc { + lock(); + + if (cb.prev_ !is null) { + // Still registered, not yet executed + // Just remove from the list. + *cb.prev_ = cb.next_; + if (cb.next_ !is null) { + cb.next_.prev_ = cb.prev_; + } + + unlock_and_decrement_token_ref_count(); + + return; + } + + unlock(); + + // Callback has either already executed or is executing + // concurrently on another thread. + + if (signallingThread_ is Thread.getThis()) { + // Callback executed on this thread or is still currently executing + // and is deregistering itself from within the callback. + if (cb.isRemoved_ !is null) { + // Currently inside the callback, let the request_stop() method + // know the object is about to be destructed and that it should + // not try to access the object when the callback returns. + *cb.isRemoved_ = true; + } + } else { + // Callback is currently executing on another thread, + // block until it finishes executing. + while (!cb.callbackFinishedExecuting.atomicLoad!(MemoryOrder.acq)) { + spin_yield(); + } + } + + remove_token_reference(); + } private: - static bool is_locked(ulong state) nothrow @safe @nogc { - return (state & locked_flag) != 0; - } - - static bool is_stop_requested(ulong state) nothrow @safe @nogc { - return (state & stop_requested_flag) != 0; - } - - static bool is_stop_requestable(ulong state) nothrow @safe @nogc { - // Interruptible if it has already been interrupted or if there are - // still interrupt_source instances in existence. - return is_stop_requested(state) || (state >= source_ref_increment); - } - - bool try_lock_and_signal_until_signalled() nothrow @safe @nogc { - ulong oldState; - do { - oldState = state_.atomicLoad!(MemoryOrder.acq); - if (is_stop_requested(oldState)) - return false; - while (is_locked(oldState)) { - spin_yield(); - oldState = state_.atomicLoad!(MemoryOrder.acq); - if (is_stop_requested(oldState)) - return false; - } - } - while (!casWeak!(MemoryOrder.seq, MemoryOrder.acq)(&state_, oldState, - oldState | stop_requested_flag | locked_flag)); - return true; - } - - void lock() nothrow @safe @nogc { - ulong oldState; - do { - oldState = state_.atomicLoad!(MemoryOrder.raw); - while (is_locked(oldState)) { - spin_yield(); - oldState = state_.atomicLoad!(MemoryOrder.raw); - } - } - while (!casWeak!(MemoryOrder.acq, MemoryOrder.raw)((&state_), oldState, - oldState | locked_flag)); - } - - void unlock() nothrow @safe @nogc { - // TODO: want to use atomicFetchSub but (proper) support is only recent - // state_.atomicFetchSub!(MemoryOrder.rel)(locked_flag); - state_.atomicOp!"-="(locked_flag); - } - - void unlock_and_increment_token_ref_count() nothrow @safe @nogc { - // TODO: want to use atomicFetchSub but (proper) support is only recent - // state_.atomicFetchSub!(MemoryOrder.rel)(locked_flag - token_ref_increment); - state_.atomicOp!"-="(locked_flag - token_ref_increment); - } - - void unlock_and_decrement_token_ref_count() nothrow @safe @nogc { - // TODO: want to use atomicFetchSub but (proper) support is only recent - // state_.atomicFetchSub!(MemoryOrder.acq_rel)(locked_flag + token_ref_increment); - state_.atomicOp!"-="(locked_flag + token_ref_increment); - } - - enum stop_requested_flag = 1L; - enum locked_flag = 2L; - enum token_ref_increment = 4L; - enum source_ref_increment = 1L << 33u; - - // bit 0 - stop-requested - // bit 1 - locked - // bits 2-32 - token ref count (31 bits) - // bits 33-63 - source ref count (31 bits) - shared ulong state_ = source_ref_increment; - StopCallback head_ = null; - Thread signallingThread_; + static bool is_locked(ulong state) nothrow @safe @nogc { + return (state & locked_flag) != 0; + } + + static bool is_stop_requested(ulong state) nothrow @safe @nogc { + return (state & stop_requested_flag) != 0; + } + + static bool is_stop_requestable(ulong state) nothrow @safe @nogc { + // Interruptible if it has already been interrupted or if there are + // still interrupt_source instances in existence. + return is_stop_requested(state) || (state >= source_ref_increment); + } + + bool try_lock_and_signal_until_signalled() nothrow @safe @nogc { + ulong oldState; + do { + oldState = state_.atomicLoad!(MemoryOrder.acq); + if (is_stop_requested(oldState)) + return false; + while (is_locked(oldState)) { + spin_yield(); + oldState = state_.atomicLoad!(MemoryOrder.acq); + if (is_stop_requested(oldState)) + return false; + } + } while (!casWeak!(MemoryOrder.seq, MemoryOrder.acq)( + &state_, oldState, + oldState | stop_requested_flag | locked_flag)); + + return true; + } + + void lock() nothrow @safe @nogc { + ulong oldState; + do { + oldState = state_.atomicLoad!(MemoryOrder.raw); + while (is_locked(oldState)) { + spin_yield(); + oldState = state_.atomicLoad!(MemoryOrder.raw); + } + } while (!casWeak!(MemoryOrder.acq, MemoryOrder.raw)( + (&state_), oldState, oldState | locked_flag)); + } + + void unlock() nothrow @safe @nogc { + // TODO: want to use atomicFetchSub but (proper) support is only recent + // state_.atomicFetchSub!(MemoryOrder.rel)(locked_flag); + state_.atomicOp!"-="(locked_flag); + } + + void unlock_and_increment_token_ref_count() nothrow @safe @nogc { + // TODO: want to use atomicFetchSub but (proper) support is only recent + // state_.atomicFetchSub!(MemoryOrder.rel)(locked_flag - token_ref_increment); + state_.atomicOp!"-="(locked_flag - token_ref_increment); + } + + void unlock_and_decrement_token_ref_count() nothrow @safe @nogc { + // TODO: want to use atomicFetchSub but (proper) support is only recent + // state_.atomicFetchSub!(MemoryOrder.acq_rel)(locked_flag + token_ref_increment); + state_.atomicOp!"-="(locked_flag + token_ref_increment); + } + + enum stop_requested_flag = 1L; + enum locked_flag = 2L; + enum token_ref_increment = 4L; + enum source_ref_increment = 1L << 33u; + + // bit 0 - stop-requested + // bit 1 - locked + // bits 2-32 - token ref count (31 bits) + // bits 33-63 - source ref count (31 bits) + shared ulong state_ = source_ref_increment; + StopCallback head_ = null; + Thread signallingThread_; } diff --git a/source/concurrency/stream/cron.d b/source/concurrency/stream/cron.d index 101f213..e5356ba 100644 --- a/source/concurrency/stream/cron.d +++ b/source/concurrency/stream/cron.d @@ -3,108 +3,116 @@ module concurrency.stream.cron; import mir.algebraic; import std.datetime.systime : SysTime; -struct Always { -} +struct Always {} struct Exact { - uint value; + uint value; } struct Every { - uint step; - uint starting; + uint step; + uint starting; } struct Each { - uint[] values; + uint[] values; } alias Spec = Algebraic!(Always, Exact, Every, Each); struct CronSpec { - import std.datetime.timezone : TimeZone, UTC; - Spec hours; - Spec minutes; - immutable(TimeZone) timezone = UTC(); + import std.datetime.timezone : TimeZone, UTC; + Spec hours; + Spec minutes; + immutable(TimeZone) timezone = UTC(); } struct Deferrer { - CronSpec schedule; - shared bool emitAtStart; - this(CronSpec schedule, bool emitAtStart) @safe shared { - this.schedule = schedule; - } - auto opCall() @safe shared { - import std.datetime.systime : Clock; - import concurrency.sender : delay; - if (emitAtStart) { - import core.time : msecs; - emitAtStart = false; - return delay(0.msecs); - } - auto sleep = timeTillNextTrigger(schedule, Clock.currTime()); - return delay(sleep); - } + CronSpec schedule; + shared bool emitAtStart; + this(CronSpec schedule, bool emitAtStart) @safe shared { + this.schedule = schedule; + } + + auto opCall() @safe shared { + import std.datetime.systime : Clock; + import concurrency.sender : delay; + if (emitAtStart) { + import core.time : msecs; + emitAtStart = false; + return delay(0.msecs); + } + + auto sleep = timeTillNextTrigger(schedule, Clock.currTime()); + return delay(sleep); + } } auto cronStream(CronSpec schedule, bool emitAtStart) @safe { - auto d = shared Deferrer(schedule, emitAtStart); - import concurrency.stream.defer; - return deferStream(d); + auto d = shared Deferrer(schedule, emitAtStart); + import concurrency.stream.defer; + return deferStream(d); } auto timeTillNextTrigger(CronSpec schedule, SysTime time) { - import core.time; - auto now = time.toOtherTZ(schedule.timezone); - Duration dur; - while (true) { - auto m = timeTillNextMinute(schedule.minutes, now); - now += m; - dur += m; - if (!now.hour.matches(schedule.hours)) { - auto h = timeTillNextHour(schedule.hours, now); - now += h; - dur += h; - continue; - } - return dur; - } + import core.time; + auto now = time.toOtherTZ(schedule.timezone); + Duration dur; + while (true) { + auto m = timeTillNextMinute(schedule.minutes, now); + now += m; + dur += m; + if (!now.hour.matches(schedule.hours)) { + auto h = timeTillNextHour(schedule.hours, now); + now += h; + dur += h; + continue; + } + + return dur; + } } auto matches(uint value, Spec spec) { - import std.algorithm; - return spec.match!((Always a) => true, - (Exact e) => value == e.value, - (Every e) => (value - e.starting) % e.step == 0, - (Each e) => e.values.any!(v => v == value)); + import std.algorithm; + return spec.match!( + (Always a) => true, + (Exact e) => value == e.value, + (Every e) => (value - e.starting) % e.step == 0, + (Each e) => e.values.any!(v => v == value) + ); } -auto timeTillNext(alias unit, uint cycle, alias selector)(Spec spec, SysTime now) { - import std.range : iota, empty, front; - import std.algorithm : find; - return spec.match!((Always a) { - return unit(1); - }, (Exact e) { - if (selector(now) >= e.value) - return unit(e.value + cycle - selector(now)); - return unit(e.value - selector(now)); - }, (Every e) { - auto next = iota(e.starting, uint.max, e.step).find!(v => v > selector(now)); - return unit(next.front() - selector(now)); - }, (Each e) { - auto next = e.values.find!(v => v > selector(now)); - if (next.empty) - return unit(e.values.front + cycle - selector(now)); - return unit(next.front() - selector(now)); - }); +auto timeTillNext(alias unit, uint cycle, alias selector)(Spec spec, + SysTime now) { + import std.range : iota, empty, front; + import std.algorithm : find; + return spec.match!((Always a) { + return unit(1); + }, (Exact e) { + if (selector(now) >= e.value) + return unit(e.value + cycle - selector(now)); + return unit(e.value - selector(now)); + }, (Every e) { + auto next = + iota(e.starting, uint.max, e.step).find!(v => v > selector(now)); + return unit(next.front() - selector(now)); + }, (Each e) { + auto next = e.values.find!(v => v > selector(now)); + if (next.empty) + return unit(e.values.front + cycle - selector(now)); + return unit(next.front() - selector(now)); + }); } auto timeTillNextMinute(Spec spec, SysTime now) { - import core.time; - return timeTillNext!(minutes, 60, i => i.minute)(spec, now) - seconds(now.second); + import core.time; + return timeTillNext!(minutes, 60, i => i.minute)(spec, now) + - seconds(now.second); } auto timeTillNextHour(Spec spec, SysTime now) { - import core.time; - return timeTillNext!(hours, 24, i => i.hour)(spec, now) - minutes(now.minute) - seconds(now.second); + import core.time; + return timeTillNext!(hours, 24, i => i.hour)(spec, now) + - minutes(now.minute) - seconds(now.second); } diff --git a/source/concurrency/stream/cycle.d b/source/concurrency/stream/cycle.d index 84bcfab..a9e0d3f 100644 --- a/source/concurrency/stream/cycle.d +++ b/source/concurrency/stream/cycle.d @@ -4,22 +4,22 @@ import concurrency.stream.stream; import std.range : ElementType; struct Cycle(Range) { - alias T = ElementType!Range; - alias DG = CollectDelegate!(T); - Range range; - void loop(StopToken)(DG emit, StopToken stopToken) @safe { - for(;!stopToken.isStopRequested;) { - foreach(item; range) { - emit(item); - if (stopToken.isStopRequested) - return; - } - } - } + alias T = ElementType!Range; + alias DG = CollectDelegate!(T); + Range range; + void loop(StopToken)(DG emit, StopToken stopToken) @safe { + for (; !stopToken.isStopRequested;) { + foreach (item; range) { + emit(item); + if (stopToken.isStopRequested) + return; + } + } + } } /// Stream that cycles through a Range until cancelled auto cycleStream(Range)(Range range) { - alias T = ElementType!Range; - return Cycle!Range(range).loopStream!T; + alias T = ElementType!Range; + return Cycle!Range(range).loopStream!T; } diff --git a/source/concurrency/stream/defer.d b/source/concurrency/stream/defer.d index 394f018..8a88d85 100644 --- a/source/concurrency/stream/defer.d +++ b/source/concurrency/stream/defer.d @@ -7,65 +7,72 @@ import std.traits : ReturnType; // Creates a stream of the values resulted by the Senders returned by Fun. auto deferStream(Fun)(Fun fun) if (models!(ReturnType!Fun, isSender)) { - import concurrency.utils : isThreadSafeCallable; - static assert(isThreadSafeCallable!Fun); - alias Sender = ReturnType!Fun; - return fromStreamOp!(Sender.Value, void, DeferStreamOp!(Fun))(fun); + import concurrency.utils : isThreadSafeCallable; + static assert(isThreadSafeCallable!Fun); + alias Sender = ReturnType!Fun; + return fromStreamOp!(Sender.Value, void, DeferStreamOp!(Fun))(fun); } template DeferStreamOp(Fun) { - import concurrency.sender : OpType; - alias Sender = ReturnType!Fun; - alias DG = CollectDelegate!(Sender.Value); + import concurrency.sender : OpType; + alias Sender = ReturnType!Fun; + alias DG = CollectDelegate!(Sender.Value); - struct DeferStreamOp(Receiver) { - alias Op = OpType!(Sender, DeferReceiver!(Sender.Value, Receiver)); - Fun fun; - DG dg; - Receiver receiver; - Op op; - @disable this(ref return scope inout typeof(this) rhs); - @disable this(this); - this(Fun fun, DG dg, return Receiver receiver) @trusted scope { - this.fun = fun; - this.dg = dg; - this.receiver = receiver; - } - void start() @trusted nothrow scope { - op = fun().connect(DeferReceiver!(Sender.Value, Receiver)(dg, receiver, &start)); - op.start(); - } - } + struct DeferStreamOp(Receiver) { + alias Op = OpType!(Sender, DeferReceiver!(Sender.Value, Receiver)); + Fun fun; + DG dg; + Receiver receiver; + Op op; + @disable + this(ref return scope inout typeof(this) rhs); + @disable + this(this); + this(Fun fun, DG dg, return Receiver receiver) @trusted scope { + this.fun = fun; + this.dg = dg; + this.receiver = receiver; + } + + void start() @trusted nothrow scope { + op = fun().connect( + DeferReceiver!(Sender.Value, Receiver)(dg, receiver, &start)); + op.start(); + } + } } struct DeferReceiver(Value, Receiver) { - import concurrency.receiver; - alias DG = CollectDelegate!(Value); - DG dg; - Receiver receiver; - void delegate() @safe nothrow reset; - static if (!is(Value : void)) { - void setValue(Value value) @safe { - dg(value); - if (!receiver.getStopToken.isStopRequested) - reset(); - else - setDone(); - } - } else { - void setValue() @safe { - dg(); - if (!receiver.getStopToken.isStopRequested) - reset(); - else - setDone(); - } - } - void setError(Throwable t) @safe nothrow { - receiver.setError(t); - } - void setDone() @safe nothrow { - receiver.setDone(); - } - mixin ForwardExtensionPoints!(receiver); + import concurrency.receiver; + alias DG = CollectDelegate!(Value); + DG dg; + Receiver receiver; + void delegate() @safe nothrow reset; + static if (!is(Value : void)) { + void setValue(Value value) @safe { + dg(value); + if (!receiver.getStopToken.isStopRequested) + reset(); + else + setDone(); + } + } else { + void setValue() @safe { + dg(); + if (!receiver.getStopToken.isStopRequested) + reset(); + else + setDone(); + } + } + + void setError(Throwable t) @safe nothrow { + receiver.setError(t); + } + + void setDone() @safe nothrow { + receiver.setDone(); + } + + mixin ForwardExtensionPoints!(receiver); } diff --git a/source/concurrency/stream/filter.d b/source/concurrency/stream/filter.d index 6e1d138..0617694 100644 --- a/source/concurrency/stream/filter.d +++ b/source/concurrency/stream/filter.d @@ -4,40 +4,47 @@ import concurrency.stream.stream; import concurrency.sender : OpType; import concepts; -auto filter(Stream, Fun)(Stream stream, Fun fun) if (models!(Stream, isStream)) { - alias Properties = StreamProperties!Stream; - return fromStreamOp!(Properties.ElementType, Properties.Value, FilterStreamOp!(Stream, Fun))(stream, fun); +auto filter(Stream, Fun)(Stream stream, Fun fun) + if (models!(Stream, isStream)) { + alias Properties = StreamProperties!Stream; + return fromStreamOp!(Properties.ElementType, Properties.Value, + FilterStreamOp!(Stream, Fun))(stream, fun); } template FilterStreamOp(Stream, Fun) { - import concurrency.utils : isThreadSafeFunction; - static assert(isThreadSafeFunction!Fun); - struct FilterStreamOp(Receiver) { - alias Properties = StreamProperties!Stream; - alias DG = Properties.DG; - alias Op = OpType!(Properties.Sender, Receiver); - Fun fun; - DG dg; - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Stream stream, Fun fun, DG dg, Receiver receiver) @trusted { - this.fun = fun; - this.dg = dg; - op = stream.collect(cast(Properties.DG)&item).connect(receiver); - } - static if (is(Properties.ElementType == void)) - void item() { - if (fun()) - dg(); - } - else - void item(Properties.ElementType t) { - if (fun(t)) - dg(t); - } - void start() nothrow @safe { - op.start(); - } - } + import concurrency.utils : isThreadSafeFunction; + static assert(isThreadSafeFunction!Fun); + struct FilterStreamOp(Receiver) { + alias Properties = StreamProperties!Stream; + alias DG = Properties.DG; + alias Op = OpType!(Properties.Sender, Receiver); + Fun fun; + DG dg; + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Stream stream, Fun fun, DG dg, Receiver receiver) @trusted { + this.fun = fun; + this.dg = dg; + op = stream.collect(cast(Properties.DG) &item).connect(receiver); + } + + static if (is(Properties.ElementType == void)) + void item() { + if (fun()) + dg(); + } + + else + void item(Properties.ElementType t) { + if (fun(t)) + dg(t); + } + + void start() nothrow @safe { + op.start(); + } + } } diff --git a/source/concurrency/stream/flatmapbase.d b/source/concurrency/stream/flatmapbase.d index 1bb2bdc..dba68a2 100644 --- a/source/concurrency/stream/flatmapbase.d +++ b/source/concurrency/stream/flatmapbase.d @@ -10,276 +10,308 @@ import concepts; import core.sync.semaphore : Semaphore; enum OnOverlap { - wait, - latest + wait, + latest } template FlatMapBaseStreamOp(Stream, Fun, OnOverlap overlap) { - static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; - alias InnerSender = ReturnType!Fun; - static assert(models!(InnerSender, isSender), "Fun must produce a Sender"); - alias DG = CollectDelegate!(InnerSender.Value); - struct FlatMapBaseStreamOp(Receiver) { - alias State = .State!(Properties.Sender.Value, InnerSender.Value, Receiver, overlap); - alias Op = OpType!(Properties.Sender, StreamReceiver!State); - Fun fun; - Op op; - State state; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Stream stream, Fun fun, return DG dg, return Receiver receiver) @trusted return scope { - this.fun = fun; - state = new State(dg, receiver); - // TODO: would it be good to do the fun in a transform operation? - op = stream.collect(cast(Properties.DG)&item).connect(StreamReceiver!State(state)); - } - static if (is(Properties.ElementType == void)) - void item() { - if (state.isStopRequested) - return; - state.onItem(); - with (state.bitfield.lock()) { - if (isDoneOrErrorProduced(oldState)) { - return; - } - auto sender = fun(); - release(Counter.tick); // release early - runInnerSender(sender); - } - } - else - void item(Properties.ElementType t) { - if (state.isStopRequested) - return; - state.onItem(); - with (state.bitfield.lock()) { - if (isDoneOrErrorProduced(oldState)) { - return; - } - auto sender = fun(t); - release(Counter.tick); // release early - runInnerSender(sender); - } - } - private void runInnerSender(ref InnerSender sender) { - import concurrency.sender : connectHeap; - auto innerOp = sender.connectHeap(InnerSenderReceiver!(State)(state)); - innerOp.start(); - } - void start() nothrow @safe scope { - op.start(); - } - } + static assert(isThreadSafeFunction!Fun); + alias Properties = StreamProperties!Stream; + alias InnerSender = ReturnType!Fun; + static assert(models!(InnerSender, isSender), "Fun must produce a Sender"); + alias DG = CollectDelegate!(InnerSender.Value); + struct FlatMapBaseStreamOp(Receiver) { + alias State = .State!(Properties.Sender.Value, InnerSender.Value, + Receiver, overlap); + alias Op = OpType!(Properties.Sender, StreamReceiver!State); + Fun fun; + Op op; + State state; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Stream stream, Fun fun, return DG dg, + return Receiver receiver) @trusted return scope { + this.fun = fun; + state = new State(dg, receiver); + // TODO: would it be good to do the fun in a transform operation? + op = stream.collect(cast(Properties.DG) &item) + .connect(StreamReceiver!State(state)); + } + + static if (is(Properties.ElementType == void)) + void item() { + if (state.isStopRequested) + return; + state.onItem(); + with (state.bitfield.lock()) { + if (isDoneOrErrorProduced(oldState)) { + return; + } + + auto sender = fun(); + release(Counter.tick); // release early + runInnerSender(sender); + } + } + + else + void item(Properties.ElementType t) { + if (state.isStopRequested) + return; + state.onItem(); + with (state.bitfield.lock()) { + if (isDoneOrErrorProduced(oldState)) { + return; + } + + auto sender = fun(t); + release(Counter.tick); // release early + runInnerSender(sender); + } + } + + private void runInnerSender(ref InnerSender sender) { + import concurrency.sender : connectHeap; + auto innerOp = + sender.connectHeap(InnerSenderReceiver!(State)(state)); + innerOp.start(); + } + + void start() nothrow @safe scope { + op.start(); + } + } } private enum Flags : size_t { - locked = 0x1, - value_produced = 0x2, - doneOrError_produced = 0x4 + locked = 0x1, + value_produced = 0x2, + doneOrError_produced = 0x4 } private enum Counter : size_t { - tick = 0x8 + tick = 0x8 } private bool isLast(size_t state) @safe @nogc nothrow pure { - return (state >> 3) == 2; + return (state >> 3) == 2; } private bool isDoneOrErrorProduced(size_t state) @safe @nogc nothrow pure { - return (state & Flags.doneOrError_produced) > 0; + return (state & Flags.doneOrError_produced) > 0; } -final class State(TStreamSenderValue, TSenderValue, Receiver, OnOverlap overlap) : StopSource { - import concurrency.bitfield; - import concurrency.stoptoken; - import std.exception : assumeWontThrow; - alias DG = CollectDelegate!(SenderValue); - alias StreamSenderValue = TStreamSenderValue; - alias SenderValue = TSenderValue; - alias onOverlap = overlap; - DG dg; - Receiver receiver; - static if (!is(StreamSenderValue == void)) - StreamSenderValue value; - Throwable throwable; - Semaphore semaphore; - StopCallback cb; - static if (overlap == OnOverlap.latest) - StopSource innerStopSource; - shared SharedBitField!Flags bitfield; - this(DG dg, Receiver receiver) { - this.dg = dg; - this.receiver = receiver; - semaphore = new Semaphore(1); - static if (overlap == OnOverlap.latest) - innerStopSource = new StopSource(); - bitfield = SharedBitField!Flags(Counter.tick); - cb = receiver.getStopToken.onStop(cast(void delegate() nothrow @safe shared)&stop); - } - override bool stop() nothrow @trusted { - return (cast(shared)this).stop(); - } - override bool stop() nothrow @trusted shared { - static if (overlap == OnOverlap.latest) { - auto r = super.stop(); - innerStopSource.stop(); - return r; - } else { - return super.stop(); - } - } - private void onItem() @trusted { - static if (overlap == OnOverlap.latest) { - innerStopSource.stop(); - semaphore.wait(); - innerStopSource.reset(); - } else { - semaphore.wait(); - } - } - private void process(size_t newState) { - cb.dispose(); - - if (receiver.getStopToken().isStopRequested) - receiver.setDone(); - else if (isDoneOrErrorProduced(newState)) { - if (throwable) - receiver.setError(throwable); - else - receiver.setDone(); - } else { - import concurrency.receiver : setValueOrError; - static if (is(typeof(Value.values))) - receiver.setValueOrError(state.value.values); - else - receiver.setValueOrError(); - } - } - private StopToken getSenderStopToken() @safe nothrow { - static if (overlap == OnOverlap.latest) { - return StopToken(innerStopSource); - } else { - return StopToken(this); - } - } +final class State(TStreamSenderValue, TSenderValue, Receiver, + OnOverlap overlap) : StopSource { + import concurrency.bitfield; + import concurrency.stoptoken; + import std.exception : assumeWontThrow; + alias DG = CollectDelegate!(SenderValue); + alias StreamSenderValue = TStreamSenderValue; + alias SenderValue = TSenderValue; + alias onOverlap = overlap; + DG dg; + Receiver receiver; + static if (!is(StreamSenderValue == void)) + StreamSenderValue value; + Throwable throwable; + Semaphore semaphore; + StopCallback cb; + static if (overlap == OnOverlap.latest) + StopSource innerStopSource; + shared SharedBitField!Flags bitfield; + this(DG dg, Receiver receiver) { + this.dg = dg; + this.receiver = receiver; + semaphore = new Semaphore(1); + static if (overlap == OnOverlap.latest) + innerStopSource = new StopSource(); + bitfield = SharedBitField!Flags(Counter.tick); + cb = receiver.getStopToken + .onStop(cast(void delegate() nothrow @safe shared) &stop); + } + + override bool stop() nothrow @trusted { + return (cast(shared) this).stop(); + } + + override bool stop() nothrow @trusted shared { + static if (overlap == OnOverlap.latest) { + auto r = super.stop(); + innerStopSource.stop(); + return r; + } else { + return super.stop(); + } + } + + private void onItem() @trusted { + static if (overlap == OnOverlap.latest) { + innerStopSource.stop(); + semaphore.wait(); + innerStopSource.reset(); + } else { + semaphore.wait(); + } + } + + private void process(size_t newState) { + cb.dispose(); + + if (receiver.getStopToken().isStopRequested) + receiver.setDone(); + else if (isDoneOrErrorProduced(newState)) { + if (throwable) + receiver.setError(throwable); + else + receiver.setDone(); + } else { + import concurrency.receiver : setValueOrError; + static if (is(typeof(Value.values))) + receiver.setValueOrError(state.value.values); + else + receiver.setValueOrError(); + } + } + + private StopToken getSenderStopToken() @safe nothrow { + static if (overlap == OnOverlap.latest) { + return StopToken(innerStopSource); + } else { + return StopToken(this); + } + } } struct StreamReceiver(State) { - State state; - static if (is(State.StreamSenderValue == void)) { - void setValue() @safe { - with (state.bitfield.update(Flags.value_produced, Counter.tick)) { - if (isLast(newState)) - state.process(newState); - } - } - } else { - void setValue(State.StreamSenderValue value) @safe { - with (state.bitfield.lock(Flags.value_produced, Counter.tick)) { - bool last = isLast(newState); - state.value = value; - release(); - if (last) - state.process(newState); - } - } - } - void setError(Throwable t) @safe nothrow { - with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) { - state.throwable = t; - release(); // must release before calling .stop - state.stop(); - } else - release(); - if (last) - state.process(newState); - } - } - void setDone() @safe nothrow { - with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) - state.stop(); - if (last) - state.process(newState); - } - } - StopToken getStopToken() @safe nothrow { - return StopToken(state); - } - private auto receiver() { - return state.receiver; - } - mixin ForwardExtensionPoints!(receiver); + State state; + static if (is(State.StreamSenderValue == void)) { + void setValue() @safe { + with (state.bitfield.update(Flags.value_produced, Counter.tick)) { + if (isLast(newState)) + state.process(newState); + } + } + } else { + void setValue(State.StreamSenderValue value) @safe { + with (state.bitfield.lock(Flags.value_produced, Counter.tick)) { + bool last = isLast(newState); + state.value = value; + release(); + if (last) + state.process(newState); + } + } + } + + void setError(Throwable t) @safe nothrow { + with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) { + state.throwable = t; + release(); // must release before calling .stop + state.stop(); + } else + release(); + if (last) + state.process(newState); + } + } + + void setDone() @safe nothrow { + with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) + state.stop(); + if (last) + state.process(newState); + } + } + + StopToken getStopToken() @safe nothrow { + return StopToken(state); + } + + private auto receiver() { + return state.receiver; + } + + mixin ForwardExtensionPoints!(receiver); } struct InnerSenderReceiver(State) { - State state; - static if (is(State.SenderValue == void)) { - void setValue() @safe { - state.dg(); - onSenderValue(); - } - } else { - void setValue(State.SenderValue value) @safe { - state.dg(value); - onSenderValue(); - } - } - void setError(Throwable t) @safe nothrow { - with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) { - state.throwable = t; - release(); // must release before calling .stop - state.stop(); - } else - release(); - if (last) - state.process(newState); - else - notify(); - } - } - void setDone() @safe nothrow { - static if (State.onOverlap == OnOverlap.latest) { - if (!state.isStopRequested) { - state.bitfield.add(Counter.tick); - notify(); - return; - } - } - with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { - bool last = isLast(newState); - if (!isDoneOrErrorProduced(oldState)) - state.stop(); - if (last) - state.process(newState); - else - notify(); - } - } - auto getStopToken() @safe nothrow { - return state.getSenderStopToken; - } - private auto receiver() { - return state.receiver; - } - private void onSenderValue() @trusted { - with (state.bitfield.update(0, Counter.tick)) { - if (isLast(newState)) - state.process(newState); - else - notify(); - } - } - private void notify() @trusted { - import std.exception : assumeWontThrow; - state.semaphore.notify().assumeWontThrow; - } - mixin ForwardExtensionPoints!(receiver); + State state; + static if (is(State.SenderValue == void)) { + void setValue() @safe { + state.dg(); + onSenderValue(); + } + } else { + void setValue(State.SenderValue value) @safe { + state.dg(value); + onSenderValue(); + } + } + + void setError(Throwable t) @safe nothrow { + with (state.bitfield.lock(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) { + state.throwable = t; + release(); // must release before calling .stop + state.stop(); + } else + release(); + if (last) + state.process(newState); + else + notify(); + } + } + + void setDone() @safe nothrow { + static if (State.onOverlap == OnOverlap.latest) { + if (!state.isStopRequested) { + state.bitfield.add(Counter.tick); + notify(); + return; + } + } + + with (state.bitfield.update(Flags.doneOrError_produced, Counter.tick)) { + bool last = isLast(newState); + if (!isDoneOrErrorProduced(oldState)) + state.stop(); + if (last) + state.process(newState); + else + notify(); + } + } + + auto getStopToken() @safe nothrow { + return state.getSenderStopToken; + } + + private auto receiver() { + return state.receiver; + } + + private void onSenderValue() @trusted { + with (state.bitfield.update(0, Counter.tick)) { + if (isLast(newState)) + state.process(newState); + else + notify(); + } + } + + private void notify() @trusted { + import std.exception : assumeWontThrow; + state.semaphore.notify().assumeWontThrow; + } + + mixin ForwardExtensionPoints!(receiver); } diff --git a/source/concurrency/stream/flatmapconcat.d b/source/concurrency/stream/flatmapconcat.d index af2788e..b58e0bf 100644 --- a/source/concurrency/stream/flatmapconcat.d +++ b/source/concurrency/stream/flatmapconcat.d @@ -3,15 +3,18 @@ module concurrency.stream.flatmapconcat; import concurrency.stream.stream : isStream; import concepts; -auto flatMapConcat(Stream, Fun)(Stream stream, Fun fun) if (models!(Stream, isStream)) { - import concurrency.stream.stream : fromStreamOp, StreamProperties; - import concurrency.utils : isThreadSafeFunction; - import concurrency.stream.flatmapbase; - import std.traits : ReturnType; +auto flatMapConcat(Stream, Fun)(Stream stream, Fun fun) + if (models!(Stream, isStream)) { + import concurrency.stream.stream : fromStreamOp, StreamProperties; + import concurrency.utils : isThreadSafeFunction; + import concurrency.stream.flatmapbase; + import std.traits : ReturnType; - static assert(isThreadSafeFunction!Fun); + static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; + alias Properties = StreamProperties!Stream; - return fromStreamOp!(ReturnType!Fun.Value, Properties.Value, FlatMapBaseStreamOp!(Stream, Fun, OnOverlap.wait))(stream, fun); + return fromStreamOp!( + ReturnType!Fun.Value, Properties.Value, + FlatMapBaseStreamOp!(Stream, Fun, OnOverlap.wait))(stream, fun); } diff --git a/source/concurrency/stream/flatmaplatest.d b/source/concurrency/stream/flatmaplatest.d index bbe74c6..37db66a 100644 --- a/source/concurrency/stream/flatmaplatest.d +++ b/source/concurrency/stream/flatmaplatest.d @@ -3,15 +3,18 @@ module concurrency.stream.flatmaplatest; import concurrency.stream.stream : isStream; import concepts; -auto flatMapLatest(Stream, Fun)(Stream stream, Fun fun) if (models!(Stream, isStream)) { - import concurrency.stream.stream : fromStreamOp, StreamProperties; - import concurrency.utils : isThreadSafeFunction; - import concurrency.stream.flatmapbase; - import std.traits : ReturnType; +auto flatMapLatest(Stream, Fun)(Stream stream, Fun fun) + if (models!(Stream, isStream)) { + import concurrency.stream.stream : fromStreamOp, StreamProperties; + import concurrency.utils : isThreadSafeFunction; + import concurrency.stream.flatmapbase; + import std.traits : ReturnType; - static assert(isThreadSafeFunction!Fun); + static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; + alias Properties = StreamProperties!Stream; - return fromStreamOp!(ReturnType!Fun.Value, Properties.Value, FlatMapBaseStreamOp!(Stream, Fun, OnOverlap.latest))(stream, fun); + return fromStreamOp!( + ReturnType!Fun.Value, Properties.Value, + FlatMapBaseStreamOp!(Stream, Fun, OnOverlap.latest))(stream, fun); } diff --git a/source/concurrency/stream/package.d b/source/concurrency/stream/package.d index fb0d51d..f789c74 100644 --- a/source/concurrency/stream/package.d +++ b/source/concurrency/stream/package.d @@ -58,278 +58,327 @@ import std.traits : hasFunctionAttributes; /// Stream that emit the same value until cancelled auto infiniteStream(T)(T t) { - alias DG = CollectDelegate!(T); - struct Loop { - T val; - void loop(StopToken)(DG emit, StopToken stopToken) { - while(!stopToken.isStopRequested) - emit(val); - } - } - return Loop(t).loopStream!T; + alias DG = CollectDelegate!(T); + struct Loop { + T val; + void loop(StopToken)(DG emit, StopToken stopToken) { + while (!stopToken.isStopRequested) + emit(val); + } + } + + return Loop(t).loopStream!T; } /// Stream that emits from start..end or until cancelled auto iotaStream(T)(T start, T end) { - alias DG = CollectDelegate!(T); - struct Loop { - T b,e; - void loop(StopToken)(DG emit, StopToken stopToken) { - foreach(i; b..e) { - emit(i); - if (stopToken.isStopRequested) - break; - } - } - } - return Loop(start, end).loopStream!T; + alias DG = CollectDelegate!(T); + struct Loop { + T b, e; + void loop(StopToken)(DG emit, StopToken stopToken) { + foreach (i; b .. e) { + emit(i); + if (stopToken.isStopRequested) + break; + } + } + } + + return Loop(start, end).loopStream!T; } /// Stream that emits each value from the array or until cancelled auto arrayStream(T)(T[] arr) { - alias DG = CollectDelegate!(T); - struct Loop { - T[] arr; - void loop(StopToken)(DG emit, StopToken stopToken) @safe { - foreach(item; arr) { - emit(item); - if (stopToken.isStopRequested) - break; - } - } - } - return Loop(arr).loopStream!T; + alias DG = CollectDelegate!(T); + struct Loop { + T[] arr; + void loop(StopToken)(DG emit, StopToken stopToken) @safe { + foreach (item; arr) { + emit(item); + if (stopToken.isStopRequested) + break; + } + } + } + + return Loop(arr).loopStream!T; } import core.time : Duration; auto intervalStream(Duration duration, bool emitAtStart = false) { - alias DG = CollectDelegate!(void); - static struct ItemReceiver(Op) { - Op* op; - void setValue() @safe { - if (op.receiver.getStopToken.isStopRequested) { - op.receiver.setDone(); - return; - } - try { - op.dg(); - if (op.receiver.getStopToken.isStopRequested) { - op.receiver.setDone(); - return; - } - op.load(); - } catch (Exception e) { - op.receiver.setError(e); - } - } - void setDone() @safe nothrow { - op.receiver.setDone(); - } - void setError(Throwable t) @safe nothrow { - op.receiver.setError(t); - } - auto getStopToken() @safe { - return op.receiver.getStopToken(); - } - auto getScheduler() @safe { - return op.receiver.getScheduler(); - } - } - static struct Op(Receiver) { - import std.traits : ReturnType; - Duration duration; - DG dg; - Receiver receiver; - alias SchedulerAfterSender = ReturnType!(SchedulerType!(Receiver).scheduleAfter); - alias Op = OpType!(SchedulerAfterSender, ItemReceiver!(typeof(this))); - Op op; - bool emitAtStart; - @disable this(this); - @disable this(ref return scope typeof(this) rhs); - this(Duration duration, DG dg, Receiver receiver, bool emitAtStart) { - this.duration = duration; - this.dg = dg; - this.receiver = receiver; - this.emitAtStart = emitAtStart; - } - void start() @trusted nothrow scope { - try { - if (emitAtStart) { - emitAtStart = false; - dg(); - if (receiver.getStopToken.isStopRequested) { - receiver.setDone(); - return; - } - } - load(); - } catch (Exception e) { - receiver.setError(e); - } - } - private void load() @trusted nothrow { - try { - op = receiver.getScheduler().scheduleAfter(duration).connect(ItemReceiver!(typeof(this))(&this)); - op.start(); - } catch (Exception e) { - receiver.setError(e); - } - } - } - static struct Sender { - alias Value = void; - Duration duration; - DG dg; - bool emitAtStart; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(duration, dg, receiver, emitAtStart); - return op; - } - } - static struct IntervalStream { - alias ElementType = void; - Duration duration; - bool emitAtStart = false; - auto collect(DG dg) @safe { - return Sender(duration, dg, emitAtStart); - } - } - return IntervalStream(duration, emitAtStart); + alias DG = CollectDelegate!(void); + static struct ItemReceiver(Op) { + Op* op; + void setValue() @safe { + if (op.receiver.getStopToken.isStopRequested) { + op.receiver.setDone(); + return; + } + + try { + op.dg(); + if (op.receiver.getStopToken.isStopRequested) { + op.receiver.setDone(); + return; + } + + op.load(); + } catch (Exception e) { + op.receiver.setError(e); + } + } + + void setDone() @safe nothrow { + op.receiver.setDone(); + } + + void setError(Throwable t) @safe nothrow { + op.receiver.setError(t); + } + + auto getStopToken() @safe { + return op.receiver.getStopToken(); + } + + auto getScheduler() @safe { + return op.receiver.getScheduler(); + } + } + + static struct Op(Receiver) { + import std.traits : ReturnType; + Duration duration; + DG dg; + Receiver receiver; + alias SchedulerAfterSender = + ReturnType!(SchedulerType!(Receiver).scheduleAfter); + alias Op = OpType!(SchedulerAfterSender, ItemReceiver!(typeof(this))); + Op op; + bool emitAtStart; + @disable + this(this); + @disable + this(ref return scope typeof(this) rhs); + this(Duration duration, DG dg, Receiver receiver, bool emitAtStart) { + this.duration = duration; + this.dg = dg; + this.receiver = receiver; + this.emitAtStart = emitAtStart; + } + + void start() @trusted nothrow scope { + try { + if (emitAtStart) { + emitAtStart = false; + dg(); + if (receiver.getStopToken.isStopRequested) { + receiver.setDone(); + return; + } + } + + load(); + } catch (Exception e) { + receiver.setError(e); + } + } + + private void load() @trusted nothrow { + try { + op = receiver.getScheduler().scheduleAfter(duration) + .connect(ItemReceiver!(typeof(this))(&this)); + op.start(); + } catch (Exception e) { + receiver.setError(e); + } + } + } + + static struct Sender { + alias Value = void; + Duration duration; + DG dg; + bool emitAtStart; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(duration, dg, receiver, emitAtStart); + return op; + } + } + + static struct IntervalStream { + alias ElementType = void; + Duration duration; + bool emitAtStart = false; + auto collect(DG dg) @safe { + return Sender(duration, dg, emitAtStart); + } + } + + return IntervalStream(duration, emitAtStart); } -auto via(Stream, Sender)(Stream stream, Sender sender) if (models!(Sender, isSender) && models!(Stream, isStream)) { - alias Properties = StreamProperties!Stream; - alias DG = Properties.DG; - static struct ViaStreamOp(Receiver) { - import std.traits : ReturnType; - import concurrency.operations.via : senderVia = via; - alias Op = OpType!(ReturnType!(senderVia!(Properties.Sender, Sender)), Receiver); - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Stream stream, Sender sender, DG dg, Receiver receiver) @trusted { - op = stream.collect(dg).senderVia(sender).connect(receiver); - } - void start() nothrow @safe { - op.start(); - } - } - return fromStreamOp!(Properties.ElementType, Properties.Value, ViaStreamOp)(stream, sender); +auto via(Stream, Sender)(Stream stream, Sender sender) + if (models!(Sender, isSender) && models!(Stream, isStream)) { + alias Properties = StreamProperties!Stream; + alias DG = Properties.DG; + static struct ViaStreamOp(Receiver) { + import std.traits : ReturnType; + import concurrency.operations.via : senderVia = via; + alias Op = OpType!(ReturnType!(senderVia!(Properties.Sender, Sender)), + Receiver); + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Stream stream, Sender sender, DG dg, Receiver receiver) @trusted { + op = stream.collect(dg).senderVia(sender).connect(receiver); + } + + void start() nothrow @safe { + op.start(); + } + } + + return fromStreamOp!(Properties.ElementType, Properties.Value, + ViaStreamOp)(stream, sender); } auto doneStream() { - alias DG = CollectDelegate!void; - static struct DoneStreamOp(Receiver) { - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(DG dg, Receiver receiver) { - this.receiver = receiver; - } - void start() nothrow @safe { - receiver.setDone(); - } - } - return fromStreamOp!(void, void, DoneStreamOp)(); + alias DG = CollectDelegate!void; + static struct DoneStreamOp(Receiver) { + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(DG dg, Receiver receiver) { + this.receiver = receiver; + } + + void start() nothrow @safe { + receiver.setDone(); + } + } + + return fromStreamOp!(void, void, DoneStreamOp)(); } auto errorStream(Exception e) { - alias DG = CollectDelegate!void; - static struct ErrorStreamOp(Receiver) { - Exception e; - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Exception e, DG dg, Receiver receiver) { - this.e = e; - this.receiver = receiver; - } - void start() nothrow @safe { - receiver.setError(e); - } - } - return fromStreamOp!(void, void, ErrorStreamOp)(e); + alias DG = CollectDelegate!void; + static struct ErrorStreamOp(Receiver) { + Exception e; + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Exception e, DG dg, Receiver receiver) { + this.e = e; + this.receiver = receiver; + } + + void start() nothrow @safe { + receiver.setError(e); + } + } + + return fromStreamOp!(void, void, ErrorStreamOp)(e); } /// A SharedStream is used for broadcasting values to zero or more receivers. Receivers can be added and removed at any time. The stream itself never completes, so receivers should themselves terminate their connection. auto sharedStream(T)() { - import concurrency.slist; - alias DG = CollectDelegate!(T); - return SharedStream!(T)(new shared SList!(SharedStream!(T).SubscriberDG)); + import concurrency.slist; + alias DG = CollectDelegate!(T); + return SharedStream!(T)(new shared SList!(SharedStream!(T).SubscriberDG)); } shared struct SharedStream(T) { - alias ElementType = T; - alias SubscriberDG = void delegate(T) nothrow @safe shared; - import concurrency.slist; - private { - alias DG = CollectDelegate!T; - static struct Op(Receiver) { - shared SharedStream!T source; - DG dg; - Receiver receiver; - StopCallback cb; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() nothrow @trusted { - auto stopToken = receiver.getStopToken(); - cb = stopToken.onStop(&(cast(shared)this).onStop); - if (stopToken.isStopRequested) { - cb.dispose(); - receiver.setDone(); - } else { - source.add(&(cast(shared)this).onItem); - } - } - void onStop() nothrow @safe shared { - with(unshared) { - source.remove(&this.onItem); - receiver.setDone(); - } - } - void onItem(T element) nothrow @safe shared { - with(unshared) { - try { - dg(element); - } catch (Exception e) { - source.remove(&this.onItem); - cb.dispose(); - receiver.setError(e); - } - } - } - private auto ref unshared() nothrow @trusted shared { - return cast()this; - } - } - static struct SharedStreamSender { - alias Value = void; - shared SharedStream!T source; - DG dg; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(source, dg, receiver); - return op; - } - } - shared SList!SubscriberDG dgs; - } - this(shared SList!SubscriberDG dgs) { - this.dgs = dgs; - } - void emit(T t) nothrow @trusted { - foreach(dg; dgs[]) - dg(t); - } - private void remove(SubscriberDG dg) nothrow @trusted { - dgs.remove(dg); - } - private void add(SubscriberDG dg) nothrow @trusted { - dgs.pushBack(dg); - } - auto collect(DG dg) @safe { - return SharedStreamSender(this, dg); - } + alias ElementType = T; + alias SubscriberDG = void delegate(T) nothrow @safe shared; + import concurrency.slist; + private { + alias DG = CollectDelegate!T; + static struct Op(Receiver) { + shared SharedStream!T source; + DG dg; + Receiver receiver; + StopCallback cb; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() nothrow @trusted { + auto stopToken = receiver.getStopToken(); + cb = stopToken.onStop(&(cast(shared) this).onStop); + if (stopToken.isStopRequested) { + cb.dispose(); + receiver.setDone(); + } else { + source.add(&(cast(shared) this).onItem); + } + } + + void onStop() nothrow @safe shared { + with (unshared) { + source.remove(&this.onItem); + receiver.setDone(); + } + } + + void onItem(T element) nothrow @safe shared { + with (unshared) { + try { + dg(element); + } catch (Exception e) { + source.remove(&this.onItem); + cb.dispose(); + receiver.setError(e); + } + } + } + + private auto ref unshared() nothrow @trusted shared { + return cast() this; + } + } + + static struct SharedStreamSender { + alias Value = void; + shared SharedStream!T source; + DG dg; + auto connect(Receiver)( + return Receiver receiver + ) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(source, dg, receiver); + return op; + } + } + + shared SList!SubscriberDG dgs; + } + + this(shared SList!SubscriberDG dgs) { + this.dgs = dgs; + } + + void emit(T t) nothrow @trusted { + foreach (dg; dgs[]) + dg(t); + } + + private void remove(SubscriberDG dg) nothrow @trusted { + dgs.remove(dg); + } + + private void add(SubscriberDG dg) nothrow @trusted { + dgs.pushBack(dg); + } + + auto collect(DG dg) @safe { + return SharedStreamSender(this, dg); + } } diff --git a/source/concurrency/stream/sample.d b/source/concurrency/stream/sample.d index 3549304..200d76f 100644 --- a/source/concurrency/stream/sample.d +++ b/source/concurrency/stream/sample.d @@ -5,58 +5,74 @@ import concurrency.sender : OpType; import concepts; /// Forwards the latest value from the base stream every time the trigger stream produces a value. If the base stream hasn't produces a (new) value the trigger is ignored -auto sample(StreamBase, StreamTrigger)(StreamBase base, StreamTrigger trigger) if (models!(StreamBase, isStream) && models!(StreamTrigger, isStream)) { - alias PropertiesBase = StreamProperties!StreamBase; - alias PropertiesTrigger = StreamProperties!StreamTrigger; - static assert(!is(PropertiesBase.ElementType == void), "No point in sampling a stream that procudes no values. Might as well use trigger directly"); - alias DG = PropertiesBase.DG; - return fromStreamOp!(PropertiesBase.ElementType, PropertiesBase.Value, SampleStreamOp!(StreamBase, StreamTrigger))(base, trigger); +auto sample(StreamBase, StreamTrigger)(StreamBase base, StreamTrigger trigger) + if (models!(StreamBase, isStream) && models!(StreamTrigger, isStream)) { + alias PropertiesBase = StreamProperties!StreamBase; + alias PropertiesTrigger = StreamProperties!StreamTrigger; + static assert( + !is(PropertiesBase.ElementType == void), + "No point in sampling a stream that procudes no values. Might as well use trigger directly" + ); + alias DG = PropertiesBase.DG; + return fromStreamOp!( + PropertiesBase.ElementType, PropertiesBase.Value, + SampleStreamOp!(StreamBase, StreamTrigger))(base, trigger); } template SampleStreamOp(StreamBase, StreamTrigger) { - import concurrency.operations.raceall; - import concurrency.bitfield : SharedBitField; - enum Flags : size_t { - locked = 0x1, - valid = 0x2 - } - alias PropertiesBase = StreamProperties!StreamBase; - alias PropertiesTrigger = StreamProperties!StreamTrigger; - alias DG = PropertiesBase.DG; - struct SampleStreamOp(Receiver) { - import std.traits : ReturnType; - alias RaceAllSender = ReturnType!(raceAll!(PropertiesBase.Sender, PropertiesTrigger.Sender)); - alias Op = OpType!(RaceAllSender, Receiver); - DG dg; - Op op; - PropertiesBase.ElementType element; - shared SharedBitField!Flags state; - shared size_t sampleState; - @disable this(ref return scope inout typeof(this) rhs); - @disable this(this); - this(StreamBase base, StreamTrigger trigger, DG dg, return Receiver receiver) @trusted scope { - this.dg = dg; - op = raceAll(base.collect(cast(PropertiesBase.DG)&item), - trigger.collect(cast(PropertiesTrigger.DG)&this.trigger)).connect(receiver); - } - void item(PropertiesBase.ElementType t) { - import core.atomic : atomicOp; - with(state.lock(Flags.valid)) { - element = t; - } - } - void trigger() { - import core.atomic : atomicOp; - with(state.lock()) { - if (was(Flags.valid)) { - auto localElement = element; - release(Flags.valid); - dg(localElement); - } - } - } - void start() { - op.start(); - } - } + import concurrency.operations.raceall; + import concurrency.bitfield : SharedBitField; + enum Flags : size_t { + locked = 0x1, + valid = 0x2 + } + + alias PropertiesBase = StreamProperties!StreamBase; + alias PropertiesTrigger = StreamProperties!StreamTrigger; + alias DG = PropertiesBase.DG; + struct SampleStreamOp(Receiver) { + import std.traits : ReturnType; + alias RaceAllSender = ReturnType!( + raceAll!(PropertiesBase.Sender, PropertiesTrigger.Sender)); + alias Op = OpType!(RaceAllSender, Receiver); + DG dg; + Op op; + PropertiesBase.ElementType element; + shared SharedBitField!Flags state; + shared size_t sampleState; + @disable + this(ref return scope inout typeof(this) rhs); + @disable + this(this); + this(StreamBase base, StreamTrigger trigger, DG dg, + return Receiver receiver) @trusted scope { + this.dg = dg; + op = raceAll( + base.collect(cast(PropertiesBase.DG) &item), + trigger.collect(cast(PropertiesTrigger.DG) &this.trigger) + ).connect(receiver); + } + + void item(PropertiesBase.ElementType t) { + import core.atomic : atomicOp; + with (state.lock(Flags.valid)) { + element = t; + } + } + + void trigger() { + import core.atomic : atomicOp; + with (state.lock()) { + if (was(Flags.valid)) { + auto localElement = element; + release(Flags.valid); + dg(localElement); + } + } + } + + void start() { + op.start(); + } + } } diff --git a/source/concurrency/stream/scan.d b/source/concurrency/stream/scan.d index 4d5ff0b..b167a27 100644 --- a/source/concurrency/stream/scan.d +++ b/source/concurrency/stream/scan.d @@ -6,42 +6,51 @@ import concepts; import concurrency.utils : isThreadSafeFunction; /// Applies an accumulator to each value from the source -auto scan(Stream, Fun, Seed)(Stream stream, scope Fun scanFn, Seed seed) if (models!(Stream, isStream)) { - static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; - return fromStreamOp!(Seed, Properties.Value, ScanStreamOp!(Stream, Fun, Seed))(stream, scanFn, seed); - } +auto scan(Stream, Fun, Seed)(Stream stream, scope Fun scanFn, Seed seed) + if (models!(Stream, isStream)) { + static assert(isThreadSafeFunction!Fun); + alias Properties = StreamProperties!Stream; + return fromStreamOp!( + Seed, Properties.Value, + ScanStreamOp!(Stream, Fun, Seed))(stream, scanFn, seed); +} template ScanStreamOp(Stream, Fun, Seed) { - static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; - alias DG = CollectDelegate!(Seed); - struct ScanStreamOp(Receiver) { - alias Op = OpType!(Properties.Sender, Receiver); - Fun scanFn; - Seed acc; - DG dg; - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Stream stream, Fun scanFn, Seed seed, DG dg, Receiver receiver) @trusted { - this.scanFn = scanFn; - this.acc = seed; - this.dg = dg; - op = stream.collect(cast(Properties.DG)&item).connect(receiver); - } - static if (is(Properties.ElementType == void)) - void item() { - acc = scanFn(acc); - dg(acc); - } - else - void item(Properties.ElementType t) { - acc = scanFn(acc, t); - dg(acc); - } - void start() nothrow @safe { - op.start(); - } - } + static assert(isThreadSafeFunction!Fun); + alias Properties = StreamProperties!Stream; + alias DG = CollectDelegate!(Seed); + struct ScanStreamOp(Receiver) { + alias Op = OpType!(Properties.Sender, Receiver); + Fun scanFn; + Seed acc; + DG dg; + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Stream stream, Fun scanFn, Seed seed, DG dg, + Receiver receiver) @trusted { + this.scanFn = scanFn; + this.acc = seed; + this.dg = dg; + op = stream.collect(cast(Properties.DG) &item).connect(receiver); + } + + static if (is(Properties.ElementType == void)) + void item() { + acc = scanFn(acc); + dg(acc); + } + + else + void item(Properties.ElementType t) { + acc = scanFn(acc, t); + dg(acc); + } + + void start() nothrow @safe { + op.start(); + } + } } diff --git a/source/concurrency/stream/slide.d b/source/concurrency/stream/slide.d index 2ea329f..61b5d32 100644 --- a/source/concurrency/stream/slide.d +++ b/source/concurrency/stream/slide.d @@ -5,59 +5,69 @@ import concurrency.sender : OpType; import concepts; /// slides a window over a stream, emitting all items in the window as an array. The array is reused so you must duplicate if you want to access it beyond the stream. -auto slide(Stream)(Stream stream, size_t window, size_t step = 1) if (models!(Stream, isStream)) { - import std.traits : ReturnType; - alias Properties = StreamProperties!Stream; - static assert(!is(Properties.ElementType == void), "Need ElementType to be able to slide, void wont do."); - import std.exception : enforce; - enforce(window > 0, "window must be greater than 0."); - enforce(step > 0, "step must be greated than 0."); - return fromStreamOp!(Properties.ElementType[], Properties.Value, SlideStreamOp!Stream)(stream, window, step); +auto slide(Stream)(Stream stream, size_t window, size_t step = 1) + if (models!(Stream, isStream)) { + import std.traits : ReturnType; + alias Properties = StreamProperties!Stream; + static assert(!is(Properties.ElementType == void), + "Need ElementType to be able to slide, void wont do."); + import std.exception : enforce; + enforce(window > 0, "window must be greater than 0."); + enforce(step > 0, "step must be greated than 0."); + return fromStreamOp!(Properties.ElementType[], Properties.Value, + SlideStreamOp!Stream)(stream, window, step); } template SlideStreamOp(Stream) { - alias Properties = StreamProperties!Stream; - alias DG = CollectDelegate!(Properties.ElementType[]); - struct SlideStreamOp(Receiver) { - alias Op = OpType!(Properties.Sender, Receiver); - size_t window, step, skip; - Properties.ElementType[] arr; - DG dg; - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Stream stream, size_t window, size_t step, DG dg, Receiver receiver) @trusted { - this.window = window; - this.step = step; - this.arr.reserve(window); - this.dg = dg; - op = stream.collect(cast(Properties.DG)&item).connect(receiver); - } - void item(Properties.ElementType t) { - if (skip > 0) { - skip--; - return; - } - import std.algorithm : moveAll; - if (arr.length == window) { - arr[window-1] = t; - } else { - arr ~= t; - if (arr.length < window) - return; - } - dg(arr); - if (step < window) { - moveAll(arr[step .. $], arr[0..$-step]); - if (step > 1) - arr.length -= step; - } else { - arr.length = 0; - skip = step - window; - } - } - void start() nothrow @safe { - op.start(); - } - } + alias Properties = StreamProperties!Stream; + alias DG = CollectDelegate!(Properties.ElementType[]); + struct SlideStreamOp(Receiver) { + alias Op = OpType!(Properties.Sender, Receiver); + size_t window, step, skip; + Properties.ElementType[] arr; + DG dg; + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Stream stream, size_t window, size_t step, DG dg, + Receiver receiver) @trusted { + this.window = window; + this.step = step; + this.arr.reserve(window); + this.dg = dg; + op = stream.collect(cast(Properties.DG) &item).connect(receiver); + } + + void item(Properties.ElementType t) { + if (skip > 0) { + skip--; + return; + } + + import std.algorithm : moveAll; + if (arr.length == window) { + arr[window - 1] = t; + } else { + arr ~= t; + if (arr.length < window) + return; + } + + dg(arr); + if (step < window) { + moveAll(arr[step .. $], arr[0 .. $ - step]); + if (step > 1) + arr.length -= step; + } else { + arr.length = 0; + skip = step - window; + } + } + + void start() nothrow @safe { + op.start(); + } + } } diff --git a/source/concurrency/stream/stream.d b/source/concurrency/stream/stream.d index fb98f54..2920218 100644 --- a/source/concurrency/stream/stream.d +++ b/source/concurrency/stream/stream.d @@ -10,137 +10,153 @@ import std.traits : hasFunctionAttributes; /// Once the Sender is connected and started the Stream will call the callable zero or more times before one of the three terminal functions of the Receiver is called. template CollectDelegate(ElementType) { - static if (is(ElementType == void)) { - alias CollectDelegate = void delegate() @safe shared; - } else { - alias CollectDelegate = void delegate(ElementType) @safe shared; - } + static if (is(ElementType == void)) { + alias CollectDelegate = void delegate() @safe shared; + } else { + alias CollectDelegate = void delegate(ElementType) @safe shared; + } } /// checks that T is a Stream void checkStream(T)() { - import std.traits : ReturnType; - alias DG = CollectDelegate!(T.ElementType); - static if (is(typeof(T.collect!DG))) - alias Sender = ReturnType!(T.collect!(DG)); - else - alias Sender = ReturnType!(T.collect); - static assert (models!(Sender, isSender)); + import std.traits : ReturnType; + alias DG = CollectDelegate!(T.ElementType); + static if (is(typeof(T.collect!DG))) + alias Sender = ReturnType!(T.collect!(DG)); + else + alias Sender = ReturnType!(T.collect); + static assert(models!(Sender, isSender)); } + enum isStream(T) = is(typeof(checkStream!T)); /// A polymorphic stream with elements of type T interface StreamObjectBase(T) { - import concurrency.sender : SenderObjectBase; - alias ElementType = T; - static assert (models!(typeof(this), isStream)); - alias DG = CollectDelegate!(ElementType); + import concurrency.sender : SenderObjectBase; + alias ElementType = T; + static assert(models!(typeof(this), isStream)); + alias DG = CollectDelegate!(ElementType); - SenderObjectBase!void collect(DG dg) @safe; + SenderObjectBase!void collect(DG dg) @safe; } /// A class extending from StreamObjectBase that wraps any Stream -class StreamObjectImpl(Stream) : StreamObjectBase!(Stream.ElementType) if (models!(Stream, isStream)) { - import concurrency.receiver : ReceiverObjectBase; - static assert (models!(typeof(this), isStream)); - private Stream stream; - this(Stream stream) { - this.stream = stream; - } - alias DG = CollectDelegate!(Stream.ElementType); - - SenderObjectBase!void collect(DG dg) @safe { - import concurrency.sender : toSenderObject; - return stream.collect(dg).toSenderObject(); - } +class StreamObjectImpl(Stream) : StreamObjectBase!(Stream.ElementType) + if (models!(Stream, isStream)) { + import concurrency.receiver : ReceiverObjectBase; + static assert(models!(typeof(this), isStream)); + private Stream stream; + this(Stream stream) { + this.stream = stream; + } + + alias DG = CollectDelegate!(Stream.ElementType); + + SenderObjectBase!void collect(DG dg) @safe { + import concurrency.sender : toSenderObject; + return stream.collect(dg).toSenderObject(); + } } /// Converts any Stream to a polymorphic StreamObject -StreamObjectBase!(Stream.ElementType) toStreamObject(Stream)(Stream stream) if (models!(Stream, isStream)) { - return new StreamObjectImpl!(Stream)(stream); +StreamObjectBase!(Stream.ElementType) toStreamObject(Stream)(Stream stream) + if (models!(Stream, isStream)) { + return new StreamObjectImpl!(Stream)(stream); } template StreamProperties(Stream) { - import std.traits : ReturnType; - alias ElementType = Stream.ElementType; - alias DG = CollectDelegate!(ElementType); - alias Sender = ReturnType!(Stream.collect); - alias Value = Sender.Value; + import std.traits : ReturnType; + alias ElementType = Stream.ElementType; + alias DG = CollectDelegate!(ElementType); + alias Sender = ReturnType!(Stream.collect); + alias Value = Sender.Value; } -auto fromStreamOp(StreamElementType, SenderValue, alias Op, Args...)(Args args) { - alias DG = CollectDelegate!(StreamElementType); - static struct FromStreamSender { - alias Value = SenderValue; - Args args; - DG dg; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(args, dg, receiver); - return op; - } - } - static struct FromStream { - static assert(models!(typeof(this), isStream)); - alias ElementType = StreamElementType; - Args args; - auto collect(DG dg) @safe { - return FromStreamSender(args, dg); - } - } - return FromStream(args); +auto fromStreamOp(StreamElementType, SenderValue, alias Op, + Args...)(Args args) { + alias DG = CollectDelegate!(StreamElementType); + static struct FromStreamSender { + alias Value = SenderValue; + Args args; + DG dg; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(args, dg, receiver); + return op; + } + } + + static struct FromStream { + static assert(models!(typeof(this), isStream)); + alias ElementType = StreamElementType; + Args args; + auto collect(DG dg) @safe { + return FromStreamSender(args, dg); + } + } + + return FromStream(args); } template SchedulerType(Receiver) { - import std.traits : ReturnType; - alias SchedulerType = ReturnType!(Receiver.getScheduler); + import std.traits : ReturnType; + alias SchedulerType = ReturnType!(Receiver.getScheduler); } /// Helper to construct a Stream, useful if the Stream you are modeling has a blocking loop template loopStream(E) { - alias DG = CollectDelegate!(E); - auto loopStream(T)(T t) { - static struct LoopStream { - static assert(models!(typeof(this), isStream)); - alias ElementType = E; - static struct LoopOp(Receiver) { - T t; - DG dg; - Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(T t, DG dg, Receiver receiver) { - this.t = t; - this.dg = dg; - this.receiver = receiver; - } - void start() @trusted nothrow scope { - try { - t.loop(dg, receiver.getStopToken); - } catch (Exception e) { - receiver.setError(e); - } - if (receiver.getStopToken().isStopRequested) - receiver.setDone(); - else - receiver.setValueOrError(); - } - } - static struct LoopSender { - alias Value = void; - T t; - DG dg; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = LoopOp!(Receiver)(t, dg, receiver); - return op; - } - } - T t; - auto collect(DG dg) @safe { - return LoopSender(t, dg); - } - } - return LoopStream(t); - } + alias DG = CollectDelegate!(E); + auto loopStream(T)(T t) { + static struct LoopStream { + static assert(models!(typeof(this), isStream)); + alias ElementType = E; + static struct LoopOp(Receiver) { + T t; + DG dg; + Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(T t, DG dg, Receiver receiver) { + this.t = t; + this.dg = dg; + this.receiver = receiver; + } + + void start() @trusted nothrow scope { + try { + t.loop(dg, receiver.getStopToken); + } catch (Exception e) { + receiver.setError(e); + } + + if (receiver.getStopToken().isStopRequested) + receiver.setDone(); + else + receiver.setValueOrError(); + } + } + + static struct LoopSender { + alias Value = void; + T t; + DG dg; + auto connect(Receiver)( + return Receiver receiver + ) @safe return scope { + // ensure NRVO + auto op = LoopOp!(Receiver)(t, dg, receiver); + return op; + } + } + + T t; + auto collect(DG dg) @safe { + return LoopSender(t, dg); + } + } + + return LoopStream(t); + } } diff --git a/source/concurrency/stream/take.d b/source/concurrency/stream/take.d index 67f489b..9372a9a 100644 --- a/source/concurrency/stream/take.d +++ b/source/concurrency/stream/take.d @@ -8,72 +8,91 @@ import concepts; /// takes the first n values from a stream or until cancelled auto take(Stream)(Stream stream, size_t n) if (models!(Stream, isStream)) { - alias Properties = StreamProperties!Stream; - import std.exception : enforce; - enforce(n > 0, "cannot take 0"); - return fromStreamOp!(Properties.ElementType, Properties.Value, TakeOp!Stream)(stream, n); + alias Properties = StreamProperties!Stream; + import std.exception : enforce; + enforce(n > 0, "cannot take 0"); + return fromStreamOp!(Properties.ElementType, Properties.Value, + TakeOp!Stream)(stream, n); } struct TakeReceiver(Receiver, Value) { - Receiver receiver; - StopSource stopSource; - static if (is(Value == void)) - void setValue() @safe { receiver.setValue(); } - else - void setValue(Value e) @safe { receiver.setValue(e); } - void setDone() nothrow @safe { - import concurrency.receiver : setValueOrError; - static if (is(Value == void)) { - if (stopSource.isStopRequested) - receiver.setValueOrError(); - else - receiver.setDone(); - } else - receiver.setDone(); - } - void setError(Throwable t) nothrow @safe { - receiver.setError(t); - } - mixin ForwardExtensionPoints!receiver; + Receiver receiver; + StopSource stopSource; + static if (is(Value == void)) + void setValue() @safe { + receiver.setValue(); + } + + else + void setValue(Value e) @safe { + receiver.setValue(e); + } + + void setDone() nothrow @safe { + import concurrency.receiver : setValueOrError; + static if (is(Value == void)) { + if (stopSource.isStopRequested) + receiver.setValueOrError(); + else + receiver.setDone(); + } else + receiver.setDone(); + } + + void setError(Throwable t) nothrow @safe { + receiver.setError(t); + } + + mixin ForwardExtensionPoints!receiver; } template TakeOp(Stream) { - alias Properties = StreamProperties!Stream; - struct TakeOp(Receiver) { - import concurrency.operations : withStopSource; - import std.traits : ReturnType; - alias SS = ReturnType!(withStopSource!(Properties.Sender)); - alias Op = OpType!(SS, TakeReceiver!(Receiver, Properties.Sender.Value)); - size_t n; - Properties.DG dg; - StopSource stopSource; - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(return Stream stream, size_t n, Properties.DG dg, return Receiver receiver) @trusted scope { - stopSource = new StopSource(); - this.dg = dg; - this.n = n; - op = stream.collect(cast(Properties.DG)&item).withStopSource(stopSource).connect(TakeReceiver!(Receiver,Properties.Sender.Value)(receiver, stopSource)); - } - static if (is(Properties.ElementType == void)) { - private void item() { - dg(); - /// TODO: this implies the stream will only call emit from a single execution context, we might need to enforce that - n--; - if (n == 0) - stopSource.stop(); - } - } else { - private void item(Properties.ElementType t) { - dg(t); - n--; - if (n == 0) - stopSource.stop(); - } - } - void start() nothrow @trusted scope { - op.start(); - } - } + alias Properties = StreamProperties!Stream; + struct TakeOp(Receiver) { + import concurrency.operations : withStopSource; + import std.traits : ReturnType; + alias SS = ReturnType!(withStopSource!(Properties.Sender)); + alias Op = + OpType!(SS, TakeReceiver!(Receiver, Properties.Sender.Value)); + size_t n; + Properties.DG dg; + StopSource stopSource; + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(return Stream stream, size_t n, Properties.DG dg, + return Receiver receiver) @trusted scope { + stopSource = new StopSource(); + this.dg = dg; + this.n = n; + op = stream.collect(cast(Properties.DG) &item) + .withStopSource(stopSource).connect(TakeReceiver!( + Receiver, + Properties.Sender.Value + )(receiver, stopSource)); + } + + static if (is(Properties.ElementType == void)) { + private void item() { + dg(); + /// TODO: this implies the stream will only call emit from a single execution context, we might need to enforce that + n--; + if (n == 0) + stopSource.stop(); + } + } else { + private void item(Properties.ElementType t) { + dg(t); + n--; + if (n == 0) + stopSource.stop(); + } + } + + void start() nothrow @trusted scope { + op.start(); + } + } } diff --git a/source/concurrency/stream/throttling.d b/source/concurrency/stream/throttling.d index ab0e38f..c6bad3d 100644 --- a/source/concurrency/stream/throttling.d +++ b/source/concurrency/stream/throttling.d @@ -8,292 +8,357 @@ import core.time : Duration; import core.atomic : MemoryOrder; private enum ThrottleFlags : size_t { - locked = 0x1, - value_produced = 0x2, - doneOrError_produced = 0x4, - timerArmed = 0x8, - timerRearming = 0x10, - counter = 0x20 + locked = 0x1, + value_produced = 0x2, + doneOrError_produced = 0x4, + timerArmed = 0x8, + timerRearming = 0x10, + counter = 0x20 } -enum ThrottleEmitLogic: uint { - first, // emit the first item in the window - last // emit the last item in the window -}; -enum ThrottleTimerLogic: uint { - noop, // don't reset the timer on new items - rearm // reset the timer on new items -}; +enum ThrottleEmitLogic : uint { + first, // emit the first item in the window + last // emit the last item in the window +} + +; +enum ThrottleTimerLogic : uint { + noop, // don't reset the timer on new items + rearm // reset the timer on new items +} + +; /// throttleFirst forwards one item and then enters a cooldown period during which it ignores items auto throttleFirst(Stream)(Stream s, Duration d) { - return throttling!(Stream, ThrottleEmitLogic.first, ThrottleTimerLogic.noop)(s, d); + return throttling!(Stream, ThrottleEmitLogic.first, + ThrottleTimerLogic.noop)(s, d); } /// throttleLast starts a cooldown period when it receives an item, after which it forwards the lastest value from the cooldown period auto throttleLast(Stream)(Stream s, Duration d) { - return throttling!(Stream, ThrottleEmitLogic.last, ThrottleTimerLogic.noop)(s, d); + return throttling!(Stream, ThrottleEmitLogic.last, + ThrottleTimerLogic.noop)(s, d); } /// debounce skips all items which are succeeded by another within the duration. Effectively it only emits items after a duration of silence auto debounce(Stream)(Stream s, Duration d) { - return throttling!(Stream, ThrottleEmitLogic.last, ThrottleTimerLogic.rearm)(s, d); + return throttling!(Stream, ThrottleEmitLogic.last, + ThrottleTimerLogic.rearm)(s, d); } -auto throttling(Stream, ThrottleEmitLogic emitLogic, ThrottleTimerLogic timerLogic)(Stream stream, Duration dur) if (models!(Stream, isStream)) { - alias Properties = StreamProperties!Stream; - return fromStreamOp!(Properties.ElementType, Properties.Value, ThrottleStreamOp!(Stream, emitLogic, timerLogic))(stream, dur); +auto throttling( + Stream, + ThrottleEmitLogic emitLogic, + ThrottleTimerLogic timerLogic +)(Stream stream, Duration dur) if (models!(Stream, isStream)) { + alias Properties = StreamProperties!Stream; + return fromStreamOp!( + Properties.ElementType, Properties.Value, + ThrottleStreamOp!(Stream, emitLogic, timerLogic))(stream, dur); } -template ThrottleStreamOp(Stream, ThrottleEmitLogic emitLogic, ThrottleTimerLogic timerLogic) { - import std.traits : ReturnType; - import concurrency.bitfield : SharedBitField; - alias Properties = StreamProperties!Stream; - alias DG = Properties.DG; - struct ThrottleStreamOp(Receiver) { - Duration dur; - DG dg; - Receiver receiver; - static if (emitLogic == ThrottleEmitLogic.last) - static if (!is(Properties.ElementType == void)) - Properties.ElementType item; - static if (!is(Properties.Value == void)) - Properties.Value value; - alias SchedulerAfterSender = ReturnType!(SchedulerType!(Receiver).scheduleAfter); - alias InnerReceiver = TimerReceiver!(typeof(this), Properties.ElementType, emitLogic, timerLogic); - StopSource stopSource; - StopSource timerStopSource; - StopCallback cb; - Throwable throwable; - alias Op = OpType!(Properties.Sender, SenderReceiver!(typeof(this), Properties.Value)); - alias TimerOp = OpType!(SchedulerAfterSender, InnerReceiver); - Op op; - TimerOp timerOp; - shared SharedBitField!ThrottleFlags flags; - @disable this(ref return scope inout typeof(this) rhs); - @disable this(this); - this(return Stream stream, Duration dur, DG dg, Receiver receiver) @trusted scope { - this.dur = dur; - this.dg = dg; - this.receiver = receiver; - stopSource = new StopSource(); - timerStopSource = new StopSource(); - op = stream.collect(cast(Properties.DG)&onItem).connect(SenderReceiver!(typeof(this), Properties.Value)(&this)); - } - static if (is(Properties.ElementType == void)) { - private void onItem() { - with (flags.update(ThrottleFlags.timerArmed)) { - if ((oldState & ThrottleFlags.timerArmed) == 0) { - static if (emitLogic == ThrottleEmitLogic.first) { - if (!push(t)) - return; - } - armTimer(); - } else { - static if (timerLogic == ThrottleTimerLogic.rearm) { - // release(); - rearmTimer(); - } - } - } - } - private bool push() { - try { - dg(); - return true; - } catch (Exception e) { - with (flags.lock(ThrottleFlags.doneOrError_produced)) { - if ((oldState & ThrottleFlags.doneOrError_produced) == 0) { - throwable = e; - } - release(); - process(newState); - } - return false; - } - } - } else { - private void onItem(Properties.ElementType t) { - with (flags.lock(ThrottleFlags.timerArmed)) { - static if (emitLogic == ThrottleEmitLogic.last) - item = t; - release(); - if ((oldState & ThrottleFlags.timerArmed) == 0) { - static if (emitLogic == ThrottleEmitLogic.first) { - if (!push(t)) - return; - } - armTimer(); - } else { - static if (timerLogic == ThrottleTimerLogic.rearm) { - rearmTimer(); - } - } - } - } - private bool push(Properties.ElementType t) { - try { - dg(t); - return true; - } catch (Exception e) { - with (flags.lock(ThrottleFlags.doneOrError_produced)) { - if ((oldState & ThrottleFlags.doneOrError_produced) == 0) { - throwable = e; - } - release(); - process(newState); - } - return false; - } - } - } - private void setError(Throwable e) { - with (flags.lock(ThrottleFlags.doneOrError_produced, ThrottleFlags.counter)) { - if ((oldState & ThrottleFlags.doneOrError_produced) == 0) { - throwable = e; - } - release(); - process(newState); - } - } - void armTimer() { - timerOp = receiver.getScheduler().scheduleAfter(dur).connect(InnerReceiver(&this)); - timerOp.start(); - } - void rearmTimer() @trusted { - flags.update(ThrottleFlags.timerRearming); - timerStopSource.stop(); - - auto localFlags = flags.load!(MemoryOrder.acq); - // if old timer happens to trigger anyway (or the source is done) we can stop - if ((localFlags & ThrottleFlags.timerArmed) == 0 || (localFlags / ThrottleFlags.counter) > 0) - return; - - timerStopSource.reset(); - - flags.update(0,0,ThrottleFlags.timerRearming); - timerOp = receiver.getScheduler().scheduleAfter(dur).connect(InnerReceiver(&this)); - timerOp.start(); - } - void process(size_t newState) { - auto count = newState / ThrottleFlags.counter; - bool isDone = count == 2 || (count == 1 && (newState & ThrottleFlags.timerArmed) == 0); - - if (!isDone) { - stopSource.stop(); - timerStopSource.stop(); - return; - } - - cb.dispose(); - - if (receiver.getStopToken().isStopRequested) - receiver.setDone(); - else if ((newState & ThrottleFlags.value_produced) > 0) { - static if (emitLogic == ThrottleEmitLogic.last) { - if ((newState & ThrottleFlags.timerArmed) > 0) { - try { - static if (!is(Properties.ElementType == void)) - dg(item); - else - dg(); - } catch (Exception e) { - receiver.setError(e); - return; - } - } - } - import concurrency.receiver : setValueOrError; - static if (is(Properties.Value == void)) - receiver.setValueOrError(); - else - receiver.setValueOrError(value); - } else if ((newState & ThrottleFlags.doneOrError_produced) > 0) { - if (throwable) - receiver.setError(throwable); - else - receiver.setDone(); - } - } - private void stop() @trusted nothrow { - stopSource.stop(); - timerStopSource.stop(); - } - void start() @trusted nothrow scope { - cb = receiver.getStopToken().onStop(cast(void delegate() nothrow @safe shared)&this.stop); // butt ugly cast, but it won't take the second overload - op.start(); - } - } +template ThrottleStreamOp(Stream, ThrottleEmitLogic emitLogic, + ThrottleTimerLogic timerLogic) { + import std.traits : ReturnType; + import concurrency.bitfield : SharedBitField; + alias Properties = StreamProperties!Stream; + alias DG = Properties.DG; + struct ThrottleStreamOp(Receiver) { + Duration dur; + DG dg; + Receiver receiver; + static if (emitLogic == ThrottleEmitLogic.last) + static if (!is(Properties.ElementType == void)) + Properties.ElementType item; + static if (!is(Properties.Value == void)) + Properties.Value value; + alias SchedulerAfterSender = + ReturnType!(SchedulerType!(Receiver).scheduleAfter); + alias InnerReceiver = + TimerReceiver!(typeof(this), Properties.ElementType, emitLogic, + timerLogic); + StopSource stopSource; + StopSource timerStopSource; + StopCallback cb; + Throwable throwable; + alias Op = OpType!(Properties.Sender, + SenderReceiver!(typeof(this), Properties.Value)); + alias TimerOp = OpType!(SchedulerAfterSender, InnerReceiver); + Op op; + TimerOp timerOp; + shared SharedBitField!ThrottleFlags flags; + @disable + this(ref return scope inout typeof(this) rhs); + @disable + this(this); + this(return Stream stream, Duration dur, DG dg, + Receiver receiver) @trusted scope { + this.dur = dur; + this.dg = dg; + this.receiver = receiver; + stopSource = new StopSource(); + timerStopSource = new StopSource(); + op = stream.collect(cast(Properties.DG) &onItem).connect( + SenderReceiver!(typeof(this), Properties.Value)(&this)); + } + + static if (is(Properties.ElementType == void)) { + private void onItem() { + with (flags.update(ThrottleFlags.timerArmed)) { + if ((oldState & ThrottleFlags.timerArmed) == 0) { + static if (emitLogic == ThrottleEmitLogic.first) { + if (!push(t)) + return; + } + + armTimer(); + } else { + static if (timerLogic == ThrottleTimerLogic.rearm) { + // release(); + rearmTimer(); + } + } + } + } + + private bool push() { + try { + dg(); + return true; + } catch (Exception e) { + with (flags.lock(ThrottleFlags.doneOrError_produced)) { + if ((oldState & ThrottleFlags.doneOrError_produced) + == 0) { + throwable = e; + } + + release(); + process(newState); + } + + return false; + } + } + } else { + private void onItem(Properties.ElementType t) { + with (flags.lock(ThrottleFlags.timerArmed)) { + static if (emitLogic == ThrottleEmitLogic.last) + item = t; + release(); + if ((oldState & ThrottleFlags.timerArmed) == 0) { + static if (emitLogic == ThrottleEmitLogic.first) { + if (!push(t)) + return; + } + + armTimer(); + } else { + static if (timerLogic == ThrottleTimerLogic.rearm) { + rearmTimer(); + } + } + } + } + + private bool push(Properties.ElementType t) { + try { + dg(t); + return true; + } catch (Exception e) { + with (flags.lock(ThrottleFlags.doneOrError_produced)) { + if ((oldState & ThrottleFlags.doneOrError_produced) + == 0) { + throwable = e; + } + + release(); + process(newState); + } + + return false; + } + } + } + + private void setError(Throwable e) { + with (flags.lock(ThrottleFlags.doneOrError_produced, + ThrottleFlags.counter)) { + if ((oldState & ThrottleFlags.doneOrError_produced) == 0) { + throwable = e; + } + + release(); + process(newState); + } + } + + void armTimer() { + timerOp = receiver.getScheduler().scheduleAfter(dur) + .connect(InnerReceiver(&this)); + timerOp.start(); + } + + void rearmTimer() @trusted { + flags.update(ThrottleFlags.timerRearming); + timerStopSource.stop(); + + auto localFlags = flags.load!(MemoryOrder.acq); + // if old timer happens to trigger anyway (or the source is done) we can stop + if ((localFlags & ThrottleFlags.timerArmed) == 0 + || (localFlags / ThrottleFlags.counter) > 0) + return; + + timerStopSource.reset(); + + flags.update(0, 0, ThrottleFlags.timerRearming); + timerOp = receiver.getScheduler().scheduleAfter(dur) + .connect(InnerReceiver(&this)); + timerOp.start(); + } + + void process(size_t newState) { + auto count = newState / ThrottleFlags.counter; + bool isDone = count == 2 + || (count == 1 && (newState & ThrottleFlags.timerArmed) == 0); + + if (!isDone) { + stopSource.stop(); + timerStopSource.stop(); + return; + } + + cb.dispose(); + + if (receiver.getStopToken().isStopRequested) + receiver.setDone(); + else if ((newState & ThrottleFlags.value_produced) > 0) { + static if (emitLogic == ThrottleEmitLogic.last) { + if ((newState & ThrottleFlags.timerArmed) > 0) { + try { + static if (!is(Properties.ElementType == void)) + dg(item); + else + dg(); + } catch (Exception e) { + receiver.setError(e); + return; + } + } + } + + import concurrency.receiver : setValueOrError; + static if (is(Properties.Value == void)) + receiver.setValueOrError(); + else + receiver.setValueOrError(value); + } else if ((newState & ThrottleFlags.doneOrError_produced) > 0) { + if (throwable) + receiver.setError(throwable); + else + receiver.setDone(); + } + } + + private void stop() @trusted nothrow { + stopSource.stop(); + timerStopSource.stop(); + } + + void start() @trusted nothrow scope { + cb = receiver.getStopToken().onStop( + cast(void delegate() nothrow @safe shared) &this.stop + ); // butt ugly cast, but it won't take the second overload + op.start(); + } + } } -struct TimerReceiver(Op, ElementType, ThrottleEmitLogic emitLogic, ThrottleTimerLogic timerLogic) { - Op* state; - void setValue() @safe { - with (state.flags.lock()) { - if (was(ThrottleFlags.timerRearming)) - return; - - static if (!is(ElementType == void) && emitLogic == ThrottleEmitLogic.last) - auto item = state.item; - release(ThrottleFlags.timerArmed); - static if (emitLogic == ThrottleEmitLogic.last) { - static if (!is(ElementType == void)) - state.push(item); - else - state.push(); - } - } - } - void setDone() @safe nothrow { - // TODO: would be nice if we can merge in next update... - if ((state.flags.load!(MemoryOrder.acq) & ThrottleFlags.timerRearming) > 0) - return; - with (state.flags.update(ThrottleFlags.doneOrError_produced, ThrottleFlags.counter)) { - state.process(newState); - } - } - void setError(Throwable e) nothrow @safe { - // TODO: would be nice if we can merge in next lock... - if ((state.flags.load!(MemoryOrder.acq) & ThrottleFlags.timerRearming) > 0) - return; - state.setError(e); - } - auto getStopToken() { - return StopToken(state.timerStopSource); - } - auto getScheduler() { - return state.receiver.getScheduler(); - } +struct TimerReceiver(Op, ElementType, ThrottleEmitLogic emitLogic, + ThrottleTimerLogic timerLogic) { + Op* state; + void setValue() @safe { + with (state.flags.lock()) { + if (was(ThrottleFlags.timerRearming)) + return; + + static if (!is(ElementType == void) + && emitLogic == ThrottleEmitLogic.last) + auto item = state.item; + release(ThrottleFlags.timerArmed); + static if (emitLogic == ThrottleEmitLogic.last) { + static if (!is(ElementType == void)) + state.push(item); + else + state.push(); + } + } + } + + void setDone() @safe nothrow { + // TODO: would be nice if we can merge in next update... + if ((state.flags.load!(MemoryOrder.acq) & ThrottleFlags.timerRearming) + > 0) + return; + with (state.flags.update(ThrottleFlags.doneOrError_produced, + ThrottleFlags.counter)) { + state.process(newState); + } + } + + void setError(Throwable e) nothrow @safe { + // TODO: would be nice if we can merge in next lock... + if ((state.flags.load!(MemoryOrder.acq) & ThrottleFlags.timerRearming) + > 0) + return; + state.setError(e); + } + + auto getStopToken() { + return StopToken(state.timerStopSource); + } + + auto getScheduler() { + return state.receiver.getScheduler(); + } } struct SenderReceiver(Op, Value) { - Op* state; - static if (is(Value == void)) - void setValue() { - with (state.flags.update(ThrottleFlags.value_produced, ThrottleFlags.counter)) { - state.process(newState); - } - } - else - void setValue(Value value) { - with (state.flags.lock(ThrottleFlags.value_produced, ThrottleFlags.counter)) { - state.value = value; - release(); - state.process(newState); - } - } - void setDone() { - with (state.flags.update(ThrottleFlags.doneOrError_produced, ThrottleFlags.counter)) { - state.process(newState); - } - } - void setError(Throwable e) nothrow @safe { - state.setError(e); - } - auto getStopToken() { - return StopToken(state.stopSource); - } - auto getScheduler() { - return state.receiver.getScheduler(); - } + Op* state; + static if (is(Value == void)) + void setValue() { + with (state.flags.update(ThrottleFlags.value_produced, + ThrottleFlags.counter)) { + state.process(newState); + } + } + + else + void setValue(Value value) { + with (state.flags.lock(ThrottleFlags.value_produced, + ThrottleFlags.counter)) { + state.value = value; + release(); + state.process(newState); + } + } + + void setDone() { + with (state.flags.update(ThrottleFlags.doneOrError_produced, + ThrottleFlags.counter)) { + state.process(newState); + } + } + + void setError(Throwable e) nothrow @safe { + state.setError(e); + } + + auto getStopToken() { + return StopToken(state.stopSource); + } + + auto getScheduler() { + return state.receiver.getScheduler(); + } } diff --git a/source/concurrency/stream/tolist.d b/source/concurrency/stream/tolist.d index efae5bf..83c6706 100644 --- a/source/concurrency/stream/tolist.d +++ b/source/concurrency/stream/tolist.d @@ -6,62 +6,72 @@ import concepts; /// toList collects all the stream's values and emits the array as a Sender auto toList(Stream)(Stream stream) if (models!(Stream, isStream)) { - alias Properties = StreamProperties!Stream; - static assert(is(Properties.Value == void), "sender must produce void for toList to work"); - return ToListSender!Stream(stream); + alias Properties = StreamProperties!Stream; + static assert(is(Properties.Value == void), + "sender must produce void for toList to work"); + return ToListSender!Stream(stream); } struct ToListSender(Stream) { - alias Properties = StreamProperties!Stream; - alias Value = Properties.ElementType[]; - Stream stream; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = ToListOp!(Stream, Receiver)(stream, receiver); - return op; - } + alias Properties = StreamProperties!Stream; + alias Value = Properties.ElementType[]; + Stream stream; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = ToListOp!(Stream, Receiver)(stream, receiver); + return op; + } } struct ToListOp(Stream, Receiver) { - alias Properties = StreamProperties!Stream; - alias State = ToListState!(Receiver, Properties.ElementType); - State state; - alias Op = OpType!(Properties.Sender, ToListReceiver!(State)); - Op op; - @disable this(this); - @disable this(ref return scope typeof(this) rhs); - this(Stream stream, return Receiver receiver) @trusted return scope { - state.receiver = receiver; - op = stream.collect(cast(Properties.DG)&item).connect(ToListReceiver!(State)(&state)); - } - void item(Properties.ElementType t) { - state.arr ~= t; - } - void start() nothrow @safe { - op.start(); - } + alias Properties = StreamProperties!Stream; + alias State = ToListState!(Receiver, Properties.ElementType); + State state; + alias Op = OpType!(Properties.Sender, ToListReceiver!(State)); + Op op; + @disable + this(this); + @disable + this(ref return scope typeof(this) rhs); + this(Stream stream, return Receiver receiver) @trusted return scope { + state.receiver = receiver; + op = stream.collect(cast(Properties.DG) &item) + .connect(ToListReceiver!(State)(&state)); + } + + void item(Properties.ElementType t) { + state.arr ~= t; + } + + void start() nothrow @safe { + op.start(); + } } struct ToListState(Receiver, ElementType) { - Receiver receiver; - ElementType[] arr; + Receiver receiver; + ElementType[] arr; } struct ToListReceiver(State) { - State* state; - void setValue() @safe { - state.receiver.setValue(state.arr); - } - void setDone() @safe nothrow { - state.receiver.setDone(); - } - void setError(Throwable t) nothrow @safe { - state.receiver.setError(t); - } - auto getStopToken() nothrow @safe { - return state.receiver.getStopToken(); - } - auto getScheduler() nothrow @safe { - return state.receiver.getScheduler(); - } + State* state; + void setValue() @safe { + state.receiver.setValue(state.arr); + } + + void setDone() @safe nothrow { + state.receiver.setDone(); + } + + void setError(Throwable t) nothrow @safe { + state.receiver.setError(t); + } + + auto getStopToken() nothrow @safe { + return state.receiver.getStopToken(); + } + + auto getScheduler() nothrow @safe { + return state.receiver.getScheduler(); + } } diff --git a/source/concurrency/stream/transform.d b/source/concurrency/stream/transform.d index cbd4e36..bfcb342 100644 --- a/source/concurrency/stream/transform.d +++ b/source/concurrency/stream/transform.d @@ -8,47 +8,54 @@ import std.traits : ReturnType; import concurrency.utils : isThreadSafeFunction; import concepts; -auto transform(Stream, Fun)(Stream stream, Fun fun) if (models!(Stream, isStream)) { - static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; - return fromStreamOp!(ReturnType!Fun, Properties.Value, TransformStreamOp!(Stream, Fun))(stream, fun); +auto transform(Stream, Fun)(Stream stream, Fun fun) + if (models!(Stream, isStream)) { + static assert(isThreadSafeFunction!Fun); + alias Properties = StreamProperties!Stream; + return fromStreamOp!(ReturnType!Fun, Properties.Value, + TransformStreamOp!(Stream, Fun))(stream, fun); } template TransformStreamOp(Stream, Fun) { - static assert(isThreadSafeFunction!Fun); - alias Properties = StreamProperties!Stream; - alias InnerElementType = ReturnType!Fun; - alias DG = CollectDelegate!(InnerElementType); - struct TransformStreamOp(Receiver) { - alias Op = OpType!(Properties.Sender, Receiver); - Fun fun; - DG dg; - Op op; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Stream stream, Fun fun, DG dg, Receiver receiver) @trusted { - this.fun = fun; - this.dg = dg; - op = stream.collect(cast(Properties.DG)&item).connect(receiver); - } - static if (is(Properties.ElementType == void)) - void item() { - static if (is(InnerElementType == void)) { - fun(); - dg(); - } else - dg(fun()); - } - else - void item(Properties.ElementType t) { - static if (is(InnerElementType == void)) { - fun(t); - dg(); - } else - dg(fun(t)); - } - void start() nothrow @safe { - op.start(); - } - } + static assert(isThreadSafeFunction!Fun); + alias Properties = StreamProperties!Stream; + alias InnerElementType = ReturnType!Fun; + alias DG = CollectDelegate!(InnerElementType); + struct TransformStreamOp(Receiver) { + alias Op = OpType!(Properties.Sender, Receiver); + Fun fun; + DG dg; + Op op; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Stream stream, Fun fun, DG dg, Receiver receiver) @trusted { + this.fun = fun; + this.dg = dg; + op = stream.collect(cast(Properties.DG) &item).connect(receiver); + } + + static if (is(Properties.ElementType == void)) + void item() { + static if (is(InnerElementType == void)) { + fun(); + dg(); + } else + dg(fun()); + } + + else + void item(Properties.ElementType t) { + static if (is(InnerElementType == void)) { + fun(t); + dg(); + } else + dg(fun(t)); + } + + void start() nothrow @safe { + op.start(); + } + } } diff --git a/source/concurrency/syncwait.d b/source/concurrency/syncwait.d index 000816d..89393b3 100644 --- a/source/concurrency/syncwait.d +++ b/source/concurrency/syncwait.d @@ -4,161 +4,182 @@ import concurrency.stoptoken; import concurrency.sender; import concurrency.thread; import concepts; -import mir.algebraic: reflectErr, Variant, Algebraic, assumeOk; +import mir.algebraic : reflectErr, Variant, Algebraic, assumeOk; bool isMainThread() @trusted { - import core.thread : Thread; - return Thread.getThis().isMainThread(); + import core.thread : Thread; + return Thread.getThis().isMainThread(); } package struct SyncWaitReceiver2(Value) { - static struct State { - LocalThreadWorker worker; - bool canceled; - static if (!is(Value == void)) - Value result; - Throwable throwable; - StopSource stopSource; - - this(StopSource stopSource) { - this.stopSource = stopSource; - worker = LocalThreadWorker(getLocalThreadExecutor()); - } - } - State* state; - void setDone() nothrow @safe { - state.canceled = true; - state.worker.stop(); - } - - void setError(Throwable e) nothrow @safe { - state.throwable = e; - state.worker.stop(); - } - static if (is(Value == void)) - void setValue() nothrow @safe { - state.worker.stop(); - } - else - void setValue(Value value) nothrow @safe { - state.result = value; - state.worker.stop(); - } - auto getStopToken() nothrow @safe @nogc { - return StopToken(state.stopSource); - } - auto getScheduler() nothrow @safe { - import concurrency.scheduler : SchedulerAdapter; - return SchedulerAdapter!(LocalThreadWorker*)(&state.worker); - } + static struct State { + LocalThreadWorker worker; + bool canceled; + static if (!is(Value == void)) + Value result; + Throwable throwable; + StopSource stopSource; + + this(StopSource stopSource) { + this.stopSource = stopSource; + worker = LocalThreadWorker(getLocalThreadExecutor()); + } + } + + State* state; + void setDone() nothrow @safe { + state.canceled = true; + state.worker.stop(); + } + + void setError(Throwable e) nothrow @safe { + state.throwable = e; + state.worker.stop(); + } + + static if (is(Value == void)) + void setValue() nothrow @safe { + state.worker.stop(); + } + + else + void setValue(Value value) nothrow @safe { + state.result = value; + state.worker.stop(); + } + + auto getStopToken() nothrow @safe @nogc { + return StopToken(state.stopSource); + } + + auto getScheduler() nothrow @safe { + import concurrency.scheduler : SchedulerAdapter; + return SchedulerAdapter!(LocalThreadWorker*)(&state.worker); + } } -@reflectErr enum Cancelled { cancelled } +@reflectErr +enum Cancelled { + cancelled +} struct Result(T) { - alias V = Variant!(Cancelled, Exception, T); - V result; - this(P)(P p) { - result = p; - } - bool isCancelled() { - return result._is!Cancelled; - } - bool isError() { - return result._is!Exception; - } - bool isOk() { - return result.isOk; - } - auto value() { - static if (!is(T == void)) - alias valueHandler = (T v) => v; - else - alias valueHandler = (){}; - - import mir.algebraic : match; - return result.match!(valueHandler, - function T (Cancelled c) { - throw new Exception("Cancelled"); - }, - function T (Exception e) { - throw e; - }); - } - auto get(T)() { - return result.get!T; - } - auto assumeOk() { - return value(); - } + alias V = Variant!(Cancelled, Exception, T); + V result; + this(P)(P p) { + result = p; + } + + bool isCancelled() { + return result._is!Cancelled; + } + + bool isError() { + return result._is!Exception; + } + + bool isOk() { + return result.isOk; + } + + auto value() { + static if (!is(T == void)) + alias valueHandler = (T v) => v; + else + alias valueHandler = () {}; + + import mir.algebraic : match; + return result.match!(valueHandler, function T(Cancelled c) { + throw new Exception("Cancelled"); + }, function T(Exception e) { + throw e; + }); + } + + auto get(T)() { + return result.get!T; + } + + auto assumeOk() { + return value(); + } } /// matches over the result of syncWait template match(Handlers...) { - // has to be separate because of dual-context limitation - auto match(T)(Result!T r) { - import mir.algebraic : match, optionalMatch; - return r.result.optionalMatch!(r => r).match!(Handlers); - } + // has to be separate because of dual-context limitation + auto match(T)(Result!T r) { + import mir.algebraic : match, optionalMatch; + return r.result.optionalMatch!(r => r).match!(Handlers); + } } void setTopLevelStopSource(shared StopSource stopSource) @trusted { - import std.exception : enforce; - enforce(parentStopSource is null); - parentStopSource = cast()stopSource; + import std.exception : enforce; + enforce(parentStopSource is null); + parentStopSource = cast() stopSource; } package(concurrency) static StopSource parentStopSource; /// Start the Sender and waits until it completes, cancels, or has an error. -auto syncWait(Sender, StopSource)(auto ref Sender sender, StopSource stopSource) { - return syncWaitImpl(sender, (()@trusted=>cast()stopSource)()); +auto syncWait(Sender, StopSource)(auto ref Sender sender, + StopSource stopSource) { + return syncWaitImpl(sender, (() @trusted => cast() stopSource)()); } auto syncWait(Sender)(auto scope ref Sender sender) { - import concurrency.signal : globalStopSource; - auto childStopSource = new shared StopSource(); - StopToken parentStopToken = parentStopSource ? StopToken(parentStopSource) : StopToken(globalStopSource); - - StopCallback cb = parentStopToken.onStop(() shared { childStopSource.stop(); }); - auto result = syncWaitImpl(sender, (()@trusted=>cast()childStopSource)()); - // detach stopSource - cb.dispose(); - return result; + import concurrency.signal : globalStopSource; + auto childStopSource = new shared StopSource(); + StopToken parentStopToken = parentStopSource + ? StopToken(parentStopSource) + : StopToken(globalStopSource); + + StopCallback cb = parentStopToken.onStop(() shared { + childStopSource.stop(); + }); + auto result = + syncWaitImpl(sender, (() @trusted => cast() childStopSource)()); + // detach stopSource + cb.dispose(); + return result; } -private Result!(Sender.Value) syncWaitImpl(Sender)(auto scope ref Sender sender, StopSource stopSource) @safe { - import mir.algebraic : Algebraic, Nullable; - static assert(models!(Sender, isSender)); - import concurrency.signal; - import core.stdc.signal : SIGTERM, SIGINT; +private +Result!(Sender.Value) syncWaitImpl(Sender)(auto scope ref Sender sender, + StopSource stopSource) @safe { + import mir.algebraic : Algebraic, Nullable; + static assert(models!(Sender, isSender)); + import concurrency.signal; + import core.stdc.signal : SIGTERM, SIGINT; + + alias Value = Sender.Value; + alias Receiver = SyncWaitReceiver2!(Value); - alias Value = Sender.Value; - alias Receiver = SyncWaitReceiver2!(Value); + /// TODO: not fiber safe + auto old = parentStopSource; + parentStopSource = stopSource; - /// TODO: not fiber safe - auto old = parentStopSource; - parentStopSource = stopSource; + auto state = Receiver.State(stopSource); + scope receiver = (() @trusted => Receiver(&state))(); + auto op = sender.connect(receiver); + op.start(); - auto state = Receiver.State(stopSource); - scope receiver = (()@trusted => Receiver(&state))(); - auto op = sender.connect(receiver); - op.start(); + state.worker.start(); - state.worker.start(); + parentStopSource = old; - parentStopSource = old; + if (state.canceled) + return Result!Value(Cancelled()); - if (state.canceled) - return Result!Value(Cancelled()); + if (state.throwable !is null) { + if (auto e = cast(Exception) state.throwable) + return Result!Value(e); + throw state.throwable; + } - if (state.throwable !is null) { - if (auto e = cast(Exception)state.throwable) - return Result!Value(e); - throw state.throwable; - } - static if (is(Value == void)) - return Result!Value(); - else - return Result!Value(state.result); + static if (is(Value == void)) + return Result!Value(); + else + return Result!Value(state.result); } diff --git a/source/concurrency/thread.d b/source/concurrency/thread.d index 93cabe0..8eb9349 100644 --- a/source/concurrency/thread.d +++ b/source/concurrency/thread.d @@ -15,232 +15,243 @@ import concurrency.data.queue.mpsc; // can load it to access the host's localThreadExecutor TLS instance. // Otherwise they would access their own local instance. // should not be called directly by usercode, call `silThreadExecutor` instead. -export extern(C) LocalThreadExecutor concurrency_getLocalThreadExecutor() @safe { - static LocalThreadExecutor localThreadExecutor; - if (localThreadExecutor is null) { - localThreadExecutor = new LocalThreadExecutor(); - } - - return localThreadExecutor; +export extern(C) +LocalThreadExecutor concurrency_getLocalThreadExecutor() @safe { + static LocalThreadExecutor localThreadExecutor; + if (localThreadExecutor is null) { + localThreadExecutor = new LocalThreadExecutor(); + } + + return localThreadExecutor; } LocalThreadExecutor getLocalThreadExecutor() @trusted { - import concurrency.utils : dynamicLoad; - static LocalThreadExecutor localThreadExecutor; - if (localThreadExecutor is null) { - localThreadExecutor = dynamicLoad!concurrency_getLocalThreadExecutor()(); - } - return localThreadExecutor; + import concurrency.utils : dynamicLoad; + static LocalThreadExecutor localThreadExecutor; + if (localThreadExecutor is null) { + localThreadExecutor = + dynamicLoad!concurrency_getLocalThreadExecutor()(); + } + + return localThreadExecutor; } private struct AddTimer { - Timer timer; - Duration dur; + Timer timer; + Duration dur; } private struct RemoveTimer { - Timer timer; + Timer timer; } private struct Noop {} -private alias WorkItem = Variant!(typeof(null), VoidDelegate, VoidFunction, AddTimer, RemoveTimer, Noop); // null signifies end +private alias WorkItem = + Variant!(typeof(null), VoidDelegate, VoidFunction, AddTimer, RemoveTimer, + Noop); // null signifies end private struct WorkNode { - WorkItem payload; - shared WorkNode* next; + WorkItem payload; + shared WorkNode* next; } private alias WorkQueue = WaitableQueue!(MPSCQueue!WorkNode); class LocalThreadExecutor : Executor { - import core.atomic : atomicOp, atomicStore, atomicLoad, cas; - import core.thread : ThreadID; - import std.process : thisThreadID; - import concurrency.scheduler : Timer; - import concurrency.timingwheels; - - static struct Node { - VoidDelegate dg; - Node* next; - } - private { - ThreadID threadId; - WorkQueue queue; - TimingWheels!Timer wheels; - shared ulong nextTimerId; - } - - this() @safe { - threadId = thisThreadID; - queue = new WorkQueue; - } - - void execute(VoidDelegate dg) @trusted { - if (isInContext) - dg(); - else - queue.push(new WorkNode(WorkItem(dg))); - } - - void execute(VoidFunction fn) @trusted { - if (isInContext) - fn(); - else - queue.push(new WorkNode(WorkItem(fn))); - } - - bool isInContext() @trusted { - return thisThreadID == threadId; - } + import core.atomic : atomicOp, atomicStore, atomicLoad, cas; + import core.thread : ThreadID; + import std.process : thisThreadID; + import concurrency.scheduler : Timer; + import concurrency.timingwheels; + + static struct Node { + VoidDelegate dg; + Node* next; + } + + private { + ThreadID threadId; + WorkQueue queue; + TimingWheels!Timer wheels; + shared ulong nextTimerId; + } + + this() @safe { + threadId = thisThreadID; + queue = new WorkQueue; + } + + void execute(VoidDelegate dg) @trusted { + if (isInContext) + dg(); + else + queue.push(new WorkNode(WorkItem(dg))); + } + + void execute(VoidFunction fn) @trusted { + if (isInContext) + fn(); + else + queue.push(new WorkNode(WorkItem(fn))); + } + + bool isInContext() @trusted { + return thisThreadID == threadId; + } } package struct LocalThreadWorker { - import core.time : Duration, msecs, hnsecs; - import concurrency.scheduler : Timer, TimerTrigger, TimerDelegate; - - private { - LocalThreadExecutor executor; - } - - this(LocalThreadExecutor e) @safe { - executor = e; - } - - private bool removeTimer(RemoveTimer cmd) { - auto removed = executor.wheels.cancel(cmd.timer); - cmd.timer.dg(TimerTrigger.cancel); - return removed; - } - - void start() @trusted { - assert(isInContext); // start can only be called on the thread - import std.datetime.systime : Clock; - import std.array : Appender; - Appender!(Timer[]) expiredTimers; - auto ticks = 1.msecs; // represents the granularity - executor.wheels.reset(); - executor.wheels.init(); - bool running = true; - while (running) { - import std.meta : AliasSeq; - alias handlers = AliasSeq!((typeof(null)){running = false;}, - (RemoveTimer cmd) => removeTimer(cmd), - (AddTimer cmd) { - auto real_now = Clock.currStdTime; - auto tw_now = executor.wheels.currStdTime(ticks); - auto delay = (real_now - tw_now).hnsecs; - auto at = (cmd.dur + delay)/ticks; - executor.wheels.schedule(cmd.timer, at); - }, - (VoidFunction fn) => fn(), - (VoidDelegate dg) => dg(), - (Noop){} - ); - auto nextTrigger = executor.wheels.timeUntilNextEvent(ticks, Clock.currStdTime); - bool handleIt = false; - if (nextTrigger.isNull()) { - auto work = executor.queue.pop(); - if (work is null) - continue; - work.payload.match!(handlers); - } else { - if (nextTrigger.get > 0.msecs) { - auto work = executor.queue.pop(nextTrigger.get); - if (work !is null) { - - work.payload.match!(handlers); - continue; - } - } - int advance = executor.wheels.ticksToCatchUp(ticks, Clock.currStdTime); - if (advance > 0) { - import std.range : retro; - executor.wheels.advance(advance, expiredTimers); - // NOTE timingwheels keeps the timers in reverse order, so we iterate in reverse - foreach(t; expiredTimers.data.retro) { - t.dg(TimerTrigger.trigger); - } - expiredTimers.shrinkTo(0); - } - } - } - version (unittest) { - if (!executor.queue.empty) { - auto work = executor.queue.pop(); - import std.stdio; - writeln("Got unwanted message ", work); - assert(0); - } - assert(executor.wheels.totalTimers == 0, "Still timers left"); - } - } - - void schedule(VoidDelegate dg) { - executor.queue.push(new WorkNode(WorkItem(dg))); - } - - Timer addTimer(TimerDelegate dg, Duration dur) @trusted { - import core.atomic : atomicOp; - ulong id = executor.nextTimerId.atomicOp!("+=")(1); - Timer timer = Timer(dg, id); - executor.queue.push(new WorkNode(WorkItem(AddTimer(timer, dur)))); - return timer; - } - - void cancelTimer(Timer timer) @trusted { - import std.algorithm : find; - - auto cmd = RemoveTimer(timer); - if (isInContext) { - if (removeTimer(cmd)) - return; - // if the timer is still in the queue, rewrite the queue node to a Noop - auto nodes = executor.queue[].find!((node) { - if (!node.payload._is!AddTimer) - return false; - return node.payload.get!AddTimer.timer.id == timer.id; - }); - - if (!nodes.empty) { - nodes.front.payload = WorkItem(Noop()); - } - } else - executor.queue.push(new WorkNode(WorkItem(RemoveTimer(timer)))); - } - - void stop() nothrow @trusted { - try { - executor.queue.push(new WorkNode(WorkItem(null))); - } catch (Exception e) { - assert(false, e.msg); - } - } - - bool isInContext() @trusted { - return executor.isInContext; - } + import core.time : Duration, msecs, hnsecs; + import concurrency.scheduler : Timer, TimerTrigger, TimerDelegate; + + private { + LocalThreadExecutor executor; + } + + this(LocalThreadExecutor e) @safe { + executor = e; + } + + private bool removeTimer(RemoveTimer cmd) { + auto removed = executor.wheels.cancel(cmd.timer); + cmd.timer.dg(TimerTrigger.cancel); + return removed; + } + + void start() @trusted { + assert(isInContext); // start can only be called on the thread + import std.datetime.systime : Clock; + import std.array : Appender; + Appender!(Timer[]) expiredTimers; + auto ticks = 1.msecs; // represents the granularity + executor.wheels.reset(); + executor.wheels.init(); + bool running = true; + while (running) { + import std.meta : AliasSeq; + alias handlers = AliasSeq!((typeof(null)) { + running = false; + }, (RemoveTimer cmd) => removeTimer(cmd), (AddTimer cmd) { + auto real_now = Clock.currStdTime; + auto tw_now = executor.wheels.currStdTime(ticks); + auto delay = (real_now - tw_now).hnsecs; + auto at = (cmd.dur + delay) / ticks; + executor.wheels.schedule(cmd.timer, at); + }, (VoidFunction fn) => fn(), (VoidDelegate dg) => dg(), (Noop) {}); + auto nextTrigger = + executor.wheels.timeUntilNextEvent(ticks, Clock.currStdTime); + bool handleIt = false; + if (nextTrigger.isNull()) { + auto work = executor.queue.pop(); + if (work is null) + continue; + work.payload.match!(handlers); + } else { + if (nextTrigger.get > 0.msecs) { + auto work = executor.queue.pop(nextTrigger.get); + if (work !is null) { + work.payload.match!(handlers); + continue; + } + } + + int advance = + executor.wheels.ticksToCatchUp(ticks, Clock.currStdTime); + if (advance > 0) { + import std.range : retro; + executor.wheels.advance(advance, expiredTimers); + // NOTE timingwheels keeps the timers in reverse order, so we iterate in reverse + foreach (t; expiredTimers.data.retro) { + t.dg(TimerTrigger.trigger); + } + + expiredTimers.shrinkTo(0); + } + } + } + + version(unittest) { + if (!executor.queue.empty) { + auto work = executor.queue.pop(); + import std.stdio; + writeln("Got unwanted message ", work); + assert(0); + } + + assert(executor.wheels.totalTimers == 0, "Still timers left"); + } + } + + void schedule(VoidDelegate dg) { + executor.queue.push(new WorkNode(WorkItem(dg))); + } + + Timer addTimer(TimerDelegate dg, Duration dur) @trusted { + import core.atomic : atomicOp; + ulong id = executor.nextTimerId.atomicOp!("+=")(1); + Timer timer = Timer(dg, id); + executor.queue.push(new WorkNode(WorkItem(AddTimer(timer, dur)))); + return timer; + } + + void cancelTimer(Timer timer) @trusted { + import std.algorithm : find; + + auto cmd = RemoveTimer(timer); + if (isInContext) { + if (removeTimer(cmd)) + return; + // if the timer is still in the queue, rewrite the queue node to a Noop + auto nodes = executor.queue[].find!((node) { + if (!node.payload._is!AddTimer) + return false; + return node.payload.get!AddTimer.timer.id == timer.id; + }); + + if (!nodes.empty) { + nodes.front.payload = WorkItem(Noop()); + } + } else + executor.queue.push(new WorkNode(WorkItem(RemoveTimer(timer)))); + } + + void stop() nothrow @trusted { + try { + executor.queue.push(new WorkNode(WorkItem(null))); + } catch (Exception e) { + assert(false, e.msg); + } + } + + bool isInContext() @trusted { + return executor.isInContext; + } } private Semaphore localSemaphore() { - static Semaphore semaphore; - if (semaphore is null) - semaphore = new Semaphore(); - return semaphore; + static Semaphore semaphore; + if (semaphore is null) + semaphore = new Semaphore(); + return semaphore; } package void executeInNewThread(VoidFunction fn) @system nothrow { - import concurrency.utils : closure; - import core.thread : Thread, thread_detachThis, thread_detachInstance; - version (Posix) import core.sys.posix.pthread : pthread_detach, pthread_self; - - auto t = new Thread(cast(void delegate())closure((VoidFunction fn) @trusted { - fn(); - version (Posix) - pthread_detach(pthread_self); //NOTE: see git.symmetry.dev/SIL/plugins/alpha/web/-/issues/3 - }, fn)).start(); - try { - /* + import concurrency.utils : closure; + import core.thread : Thread, thread_detachThis, thread_detachInstance; + version(Posix) + import core.sys.posix.pthread : pthread_detach, pthread_self; + + auto t = + new Thread(cast(void delegate()) closure((VoidFunction fn) @trusted { + fn(); + version(Posix) + pthread_detach( + pthread_self + ); //NOTE: see git.symmetry.dev/SIL/plugins/alpha/web/-/issues/3 + }, fn)).start(); + try { + /* the isDaemon is really only a protecting against a race condition in druntime, which is only introduced because I am detaching the thread, which I need to do if I don't want to leak memory. @@ -250,22 +261,27 @@ package void executeInNewThread(VoidFunction fn) @system nothrow { So it is fine if the exception is ignored. */ - t.isDaemon = true; // is needed because of pthread_detach (otherwise there is a race between druntime joining and the thread exiting) - } catch (Exception e) {} + t.isDaemon = + true; // is needed because of pthread_detach (otherwise there is a race between druntime joining and the thread exiting) + } catch (Exception e) {} } package void executeInNewThread(VoidDelegate fn) @system nothrow { - import concurrency.utils : closure; - import core.thread : Thread, thread_detachThis, thread_detachInstance; - version (Posix) import core.sys.posix.pthread : pthread_detach, pthread_self; - - auto t = new Thread(cast(void delegate())closure((VoidDelegate fn) @trusted { - fn(); - version (Posix) - pthread_detach(pthread_self); //NOTE: see git.symmetry.dev/SIL/plugins/alpha/web/-/issues/3 - }, fn)).start(); - try { - /* + import concurrency.utils : closure; + import core.thread : Thread, thread_detachThis, thread_detachInstance; + version(Posix) + import core.sys.posix.pthread : pthread_detach, pthread_self; + + auto t = + new Thread(cast(void delegate()) closure((VoidDelegate fn) @trusted { + fn(); + version(Posix) + pthread_detach( + pthread_self + ); //NOTE: see git.symmetry.dev/SIL/plugins/alpha/web/-/issues/3 + }, fn)).start(); + try { + /* the isDaemon is really only a protecting against a race condition in druntime, which is only introduced because I am detaching the thread, which I need to do if I don't want to leak memory. @@ -275,192 +291,217 @@ package void executeInNewThread(VoidDelegate fn) @system nothrow { So it is fine if the exception is ignored. */ - t.isDaemon = true; // is needed because of pthread_detach (otherwise there is a race between druntime joining and the thread exiting) - } catch (Exception e) {} + t.isDaemon = + true; // is needed because of pthread_detach (otherwise there is a race between druntime joining and the thread exiting) + } catch (Exception e) {} } class ThreadExecutor : Executor { - void execute(VoidFunction fn) @trusted { - executeInNewThread(fn); - } - void execute(VoidDelegate fn) @trusted { - executeInNewThread(fn); - } - bool isInContext() @safe { return false; } + void execute(VoidFunction fn) @trusted { + executeInNewThread(fn); + } + + void execute(VoidDelegate fn) @trusted { + executeInNewThread(fn); + } + + bool isInContext() @safe { + return false; + } } -auto executeAndWait(Executor, Work, Args...)(Executor executor, Work work, Args args) { - import core.sync.semaphore; - import std.traits; - - if (executor.isInContext) - return work(args); - - Semaphore semaphore = localSemaphore(); - - alias RT = ReturnType!Work; - struct Context { - Work work; - Args args; - Semaphore semaphore; - static if (is(RT == void)) { - void run() { - work(args); - semaphore.notify(); - } - } else { - RT result; - void run() { - result = work(args); - semaphore.notify(); - } - } - } - Context c = Context(work, args, semaphore); - executor.execute(cast(VoidDelegate)&c.run); - semaphore.wait(); - static if (!is(RT == void)) { - return c.result; - } +auto executeAndWait(Executor, Work, Args...)(Executor executor, Work work, + Args args) { + import core.sync.semaphore; + import std.traits; + + if (executor.isInContext) + return work(args); + + Semaphore semaphore = localSemaphore(); + + alias RT = ReturnType!Work; + struct Context { + Work work; + Args args; + Semaphore semaphore; + static if (is(RT == void)) { + void run() { + work(args); + semaphore.notify(); + } + } else { + RT result; + void run() { + result = work(args); + semaphore.notify(); + } + } + } + + Context c = Context(work, args, semaphore); + executor.execute(cast(VoidDelegate) &c.run); + semaphore.wait(); + static if (!is(RT == void)) { + return c.result; + } } shared static this() { - import concurrency.utils : resetScheduler; + import concurrency.utils : resetScheduler; - resetScheduler(); + resetScheduler(); } struct ThreadSender { - static assert (models!(typeof(this), isSender)); - alias Value = void; - static struct Op(Receiver) { - private Receiver receiver; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Receiver receiver) { - this.receiver = receiver; - } - void start() @trusted nothrow scope { - executeInNewThread(cast(VoidDelegate)&run); - } - void run() @trusted { - import concurrency.receiver : setValueOrError; - import concurrency.error : clone; - import concurrency : parentStopSource; - - parentStopSource = receiver.getStopToken().source; - - try { - receiver.setValue(); - } catch (Exception e) { - receiver.setError(e); - } catch (Throwable t) { - receiver.setError(t.clone()); - } - - parentStopSource = null; - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(receiver); - return op; - } + static assert(models!(typeof(this), isSender)); + alias Value = void; + static struct Op(Receiver) { + private Receiver receiver; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Receiver receiver) { + this.receiver = receiver; + } + + void start() @trusted nothrow scope { + executeInNewThread(cast(VoidDelegate) &run); + } + + void run() @trusted { + import concurrency.receiver : setValueOrError; + import concurrency.error : clone; + import concurrency : parentStopSource; + + parentStopSource = receiver.getStopToken().source; + + try { + receiver.setValue(); + } catch (Exception e) { + receiver.setError(e); + } catch (Throwable t) { + receiver.setError(t.clone()); + } + + parentStopSource = null; + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(receiver); + return op; + } } struct StdTaskPool { - import std.parallelism : Task, TaskPool; - TaskPool pool; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(TaskPool pool) @trusted scope shared { - this.pool = cast(shared)pool; - } - ~this() nothrow @trusted scope { - try { - pool.finish(true); - } catch (Exception e) { - // can't really happen - assert(0); - } - } - auto getScheduler() return @safe { - return StdTaskPoolProtoScheduler(pool); - } - auto getScheduler() return @trusted shared { - return StdTaskPoolProtoScheduler(cast()pool); - } + import std.parallelism : Task, TaskPool; + TaskPool pool; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(TaskPool pool) @trusted scope shared { + this.pool = cast(shared) pool; + } + + ~this() nothrow @trusted scope { + try { + pool.finish(true); + } catch (Exception e) { + // can't really happen + assert(0); + } + } + + auto getScheduler() return @safe { + return StdTaskPoolProtoScheduler(pool); + } + + auto getScheduler() return @trusted shared { + return StdTaskPoolProtoScheduler(cast() pool); + } } shared(StdTaskPool) stdTaskPool(size_t nWorkers = 0) @safe { - import std.parallelism : TaskPool; - return shared StdTaskPool(new TaskPool(nWorkers)); + import std.parallelism : TaskPool; + return shared StdTaskPool(new TaskPool(nWorkers)); } struct StdTaskPoolProtoScheduler { - import std.parallelism : TaskPool; - TaskPool pool; - auto schedule() { - return TaskPoolSender(pool); - } - auto withBaseScheduler(Scheduler)(Scheduler scheduler) { - return StdTaskPoolScheduler!(Scheduler)(pool, scheduler); - } + import std.parallelism : TaskPool; + TaskPool pool; + auto schedule() { + return TaskPoolSender(pool); + } + + auto withBaseScheduler(Scheduler)(Scheduler scheduler) { + return StdTaskPoolScheduler!(Scheduler)(pool, scheduler); + } } private struct StdTaskPoolScheduler(Scheduler) { - import std.parallelism : TaskPool; - import core.time : Duration; - TaskPool pool; - Scheduler scheduler; - auto schedule() { - return TaskPoolSender(pool); - } - auto scheduleAfter(Duration run) { - import concurrency.operations : via; - return schedule().via(scheduler.scheduleAfter(run)); - } + import std.parallelism : TaskPool; + import core.time : Duration; + TaskPool pool; + Scheduler scheduler; + auto schedule() { + return TaskPoolSender(pool); + } + + auto scheduleAfter(Duration run) { + import concurrency.operations : via; + return schedule().via(scheduler.scheduleAfter(run)); + } } private struct TaskPoolSender { - import std.parallelism : Task, TaskPool, scopedTask, task; - import std.traits : ReturnType; - import concurrency.error : clone; - alias Value = void; - TaskPool pool; - static struct Op(Receiver) { - static void setValue(Receiver receiver) @trusted nothrow { - import concurrency : parentStopSource; - parentStopSource = receiver.getStopToken().source; - try { - receiver.setValue(); - } catch (Exception e) { - receiver.setError(e); - } catch (Throwable t) { - receiver.setError(t.clone); - } - parentStopSource = null; - } - TaskPool pool; - alias TaskType = typeof(task!setValue(Receiver.init)); - TaskType myTask; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - this(Receiver receiver, TaskPool pool) @safe return scope { - myTask = task!(setValue)(receiver); - this.pool = pool; - } - void start() @trusted nothrow scope { - try { - pool.put(myTask); - } catch (Exception e) { - myTask.args[0].setError(e); - } - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(receiver, pool); - return op; - } + import std.parallelism : Task, TaskPool, scopedTask, task; + import std.traits : ReturnType; + import concurrency.error : clone; + alias Value = void; + TaskPool pool; + static struct Op(Receiver) { + static void setValue(Receiver receiver) @trusted nothrow { + import concurrency : parentStopSource; + parentStopSource = receiver.getStopToken().source; + try { + receiver.setValue(); + } catch (Exception e) { + receiver.setError(e); + } catch (Throwable t) { + receiver.setError(t.clone); + } + + parentStopSource = null; + } + + TaskPool pool; + alias TaskType = typeof(task!setValue(Receiver.init)); + TaskType myTask; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + this(Receiver receiver, TaskPool pool) @safe return scope { + myTask = task!(setValue)(receiver); + this.pool = pool; + } + + void start() @trusted nothrow scope { + try { + pool.put(myTask); + } catch (Exception e) { + myTask.args[0].setError(e); + } + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(receiver, pool); + return op; + } } diff --git a/source/concurrency/timingwheels.d b/source/concurrency/timingwheels.d index b458710..b447cc8 100644 --- a/source/concurrency/timingwheels.d +++ b/source/concurrency/timingwheels.d @@ -19,7 +19,7 @@ import std.algorithm; import std.experimental.logger; import std.experimental.allocator; -import std.experimental.allocator.mallocator: Mallocator; +import std.experimental.allocator.mallocator : Mallocator; import core.thread; import core.memory; @@ -28,200 +28,186 @@ import ikod.containers.hashmap; import automem; import mir.algebraic : Nullable, nullable; -version(twtesting) -{ - import unit_threaded; +version(twtesting) { + import unit_threaded; } +version(twtesting) { + private class Timer { + static ulong _current_id; + private { + ulong _id; + } -version(twtesting) -{ - private class Timer - { - static ulong _current_id; - private - { - ulong _id; - } - this() @safe @nogc - { - _id = _current_id; - _current_id++; - } - ~this() @safe @nogc - { - - } - ulong id() @safe @nogc - { - return _id; - } - override string toString() - { - return "%d".format(_id); - } - } + this() @safe @nogc { + _id = _current_id; + _current_id++; + } + + ~this() @safe @nogc {} + + ulong id() @safe @nogc { + return _id; + } + + override string toString() { + return "%d".format(_id); + } + } } + /// /// scheduling error occurs at schedule() when ticks == 0 or timer already scheduled. /// /// -class ScheduleTimerError: Exception -{ - /// - this(string msg, string file = __FILE__, size_t line = __LINE__) @nogc @safe - { - super(msg, file, line); - } +class ScheduleTimerError : Exception { + /// + this(string msg, string file = __FILE__, size_t line = __LINE__) @nogc + @safe { + super(msg, file, line); + } } + /// /// Cancel timer error occurs if you try to cancel timer which is not scheduled. /// -class CancelTimerError: Exception -{ - /// - this(string msg, string file = __FILE__, size_t line = __LINE__) @nogc @safe - { - super(msg, file, line); - } +class CancelTimerError : Exception { + /// + this(string msg, string file = __FILE__, size_t line = __LINE__) @nogc + @safe { + super(msg, file, line); + } } + /// /// Advancing error occurs if number of ticks for advance not in range 0=0); - } - alias Ticks = ulong; // ticks are 64 bit unsigned integers. - - // hashing ticks to slots - // 8 levels, each level 256 slots, with of slot on each level 256 times - // translate ticks to level - // 0x00_00_00_00_00_00_00_00 <- ticks - // ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ - // □ □ □ □ □ □ □ □ \ - // □ □ □ □ □ □ □ □ | - // . . . . . . . . | 256 slots - // . . . . . . . . | - // □ □ □ □ □ □ □ □ / - // 7 6 5 4 3 2 1 0 - // <- 8 levels - // each slot - double linked list of timers - - // ticks to level = bsr(ticks)/8 - pragma(inline) private pure int t2l(ulong t) @safe @nogc nothrow - { - if (t == 0) - { - return 0; - } - return bsr(t)/LEVELS; - } - // ticks to slot = ticks >> (level*8) - pragma(inline) private pure int t2s(ulong t, int l) @safe @nogc nothrow - { - return (t >> (l<<3)) & MASK; - } - // level to ticks - // l[0] -> 256 - // l[1] -> 256*256 - // ... - pragma(inline) private pure ulong l2t(int l) @safe @nogc nothrow - { - return SLOTS<0); - auto n = freeList.next; - () @trusted { - dispose(allocator, freeList); - }(); - freeListLen--; - freeList = n; - } - } - - private ListElement!T* getOrCreate() - { - ListElement!T* result; - if (freeList !is null) - { - result = freeList; - freeList = freeList.next; - freeListLen--; - return result; - } - result = make!(ListElement!T)(allocator); - return result; - } - private void returnToFreeList(ListElement!T* le) - { - if ( freeListLen >= FreeListMaxLen ) - { - // this can be safely disposed as we do not leak ListElements outide this module - () @trusted { - dispose(allocator, le); - }(); - } - else - { - le.position = 0xffff; - le.next = freeList; - freeList = le; - freeListLen++; - } - } - void init() - { - startedAt = Clock.currStdTime; - } - void init(ulong time) { - startedAt = time; - } - /++ +/// +struct TimingWheels(T) { + import core.bitop : bsr; + + private { + alias TimerIdType = ReturnType!(T.id); + alias allocator = Mallocator.instance; + + enum MASK = 0xff; + enum LEVELS = 8; + enum LEVEL_MAX = LEVELS - 1; + enum SLOTS = 256; + enum FreeListMaxLen = 100; + + struct ListElement(T) { + private { + T timer; + ulong scheduled_at; + ushort position; + ListElement!T* prev, next; + } + } + + struct Slot { + ListElement!T* head; + } + + struct Level { + // now if counter of ticks processed on this level + ulong now; + Slot[SLOTS] slots; + } + + Level[LEVELS] levels; + ListElement!T* freeList; + int freeListLen; + HashMap!(TimerIdType, ListElement!T*) ptrs; + long startedAt; + } + + invariant { + assert(freeListLen >= 0); + } + + alias Ticks = ulong; // ticks are 64 bit unsigned integers. + + // hashing ticks to slots + // 8 levels, each level 256 slots, with of slot on each level 256 times + // translate ticks to level + // 0x00_00_00_00_00_00_00_00 <- ticks + // ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ + // □ □ □ □ □ □ □ □ \ + // □ □ □ □ □ □ □ □ | + // . . . . . . . . | 256 slots + // . . . . . . . . | + // □ □ □ □ □ □ □ □ / + // 7 6 5 4 3 2 1 0 + // <- 8 levels + // each slot - double linked list of timers + + // ticks to level = bsr(ticks)/8 + pragma(inline) + private pure int t2l(ulong t) @safe @nogc nothrow { + if (t == 0) { + return 0; + } + + return bsr(t) / LEVELS; + } + + // ticks to slot = ticks >> (level*8) + pragma(inline) + private pure int t2s(ulong t, int l) @safe @nogc nothrow { + return (t >> (l << 3)) & MASK; + } + + // level to ticks + // l[0] -> 256 + // l[1] -> 256*256 + // ... + pragma(inline) + private pure ulong l2t(int l) @safe @nogc nothrow { + return SLOTS << l; + } + + ~this() { + ptrs.clear; + for (int l = 0; l <= LEVEL_MAX; l++) + for (int s = 0; s < SLOTS; s++) { + while (levels[l].slots[s].head) { + auto le = levels[l].slots[s].head; + dl_unlink(le, &levels[l].slots[s].head); + () @trusted { + dispose(allocator, le); + }(); + } + } + + while (freeList) { + assert(freeListLen > 0); + auto n = freeList.next; + () @trusted { + dispose(allocator, freeList); + }(); + freeListLen--; + freeList = n; + } + } + + private ListElement!T* getOrCreate() { + ListElement!T* result; + if (freeList !is null) { + result = freeList; + freeList = freeList.next; + freeListLen--; + return result; + } + + result = make!(ListElement!T)(allocator); + return result; + } + + private void returnToFreeList(ListElement!T* le) { + if (freeListLen >= FreeListMaxLen) { + // this can be safely disposed as we do not leak ListElements outide this module + () @trusted { + dispose(allocator, le); + }(); + } else { + le.position = 0xffff; + le.next = freeList; + freeList = le; + freeListLen++; + } + } + + void init() { + startedAt = Clock.currStdTime; + } + + void init(ulong time) { + startedAt = time; + } + + /++ + Return internal view on current time - it is time at the call to $(B init) + plus total number of steps multiplied by $(B tick) duration. + Params: + tick = tick duration +/ - auto currStdTime(Duration tick) - { - return startedAt + levels[0].now * tick.split!"hnsecs".hnsecs; - } - /// - /// Schedule timer to $(B ticks) ticks forward from internal 'now'. - ///Params: - /// timer = timer to schedule; - /// ticks = ticks in the future to schedule timer. (0 < ticks < ulong.max); - ///Returns: - /// void - ///Throws: - /// ScheduleTimerError - /// when thicks == 0 - /// or when timer already scheduled - /// - void schedule(T)(T timer, const ulong ticks) - { - if (ticks == 0) - { - throw new ScheduleTimerError("ticks can't be 0"); - } - auto timer_id = timer.id(); - if (ptrs.contains(timer_id)) - { - throw new ScheduleTimerError("Timer already scheduled"); - } - size_t level_index = 0; - long t = ticks; - long s = 1; // width of the slot in ticks on level - long shift = 0; - while(t > s<<8) // while t > slots on level - { - t -= (SLOTS - (levels[level_index].now & MASK)) * s; - level_index++; - s = s << 8; - shift += 8; - } - auto level = &levels[level_index]; - auto mask = s - 1; - size_t slot_index = (level.now + (t>>shift) + ((t&mask)>0?1:0)) & MASK; - auto slot = &levels[level_index].slots[slot_index]; - debug(timingwheels) safe_tracef("use level/slot %d/%d, level now: %d", level_index, slot_index, level.now); - auto le = getOrCreate(); - le.timer = timer; - le.position = ((level_index << 8 ) | slot_index) & 0xffff; - le.scheduled_at = levels[0].now + ticks; - dl_insertFront(le, &slot.head); - ptrs[timer_id] = le; - debug(timingwheels) safe_tracef("scheduled timer id: %s, ticks: %s, now: %d, scheduled at: %s to level: %s, slot %s", - timer_id, ticks, levels[0].now, le.scheduled_at, level_index, slot_index); - } - /// Cancel timer - ///Params: - /// timer = timer to cancel - ///Returns: - /// void - ///Throws: - /// CancelTimerError - /// if timer not in wheel - bool cancel(T)(T timer) - { - // get list element pointer - auto v = ptrs.fetch(timer.id()); - if ( !v.ok ) - { - return false; - } - auto le = v.value; - immutable level_index = le.position>>8; - immutable slot_index = le.position & 0xff; - assert(timer is le.timer); - debug(timingwheels) safe_tracef("cancel timer, l:%d, s:%d", level_index, slot_index); - dl_unlink(le, &levels[level_index].slots[slot_index].head); - returnToFreeList(le); - ptrs.remove(timer.id()); - return true; - } - /// Number of ticks to rotate wheels until internal wheel 'now' - /// catch up with real world realTime. - /// Calculation based on time when wheels were stared and total - /// numer of ticks pasded. - ///Params: - /// tick = your tick length (Duration) - /// realTime = current real world now (Clock.currStdTime) - ///Returns: ticks to advance so that we catch up real world current time - int ticksToCatchUp(Duration tick, ulong realTime) - { - auto c = startedAt + tick.split!"hnsecs".hnsecs * levels[0].now; - auto v = (realTime - c) / tick.split!"hnsecs".hnsecs; - if ( v > 256 ) - { - return 256; - } - return cast(int)v; - } - /// Time until next scheduled timer event. - /// You provide tick size and current real world time. - /// This function find ticks until next event and use time of the start and - /// total steps executed to calculate time delta from $(B realNow) to next event. - ///Params: - /// tick = your accepted tick duration. - /// realNow = real world now, result of Clock.currStdTime - ///Returns: time until next event. Can be zero or negative in case you have already expired events. - /// - Nullable!Duration timeUntilNextEvent(const Duration tick, ulong realNow) - { - assert(startedAt>0, "Forgot to call init()?"); - if (totalTimers == 0) - return typeof(return).init; - immutable n = ticksUntilNextEvent(); - immutable target = startedAt + (levels[0].now + n) * tick.split!"hnsecs".hnsecs; - auto delta = (target - realNow).hnsecs; - debug(timingwheels) safe_tracef("ticksUntilNextEvent=%s, tick=%s, startedAt=%s", n, tick, SysTime(startedAt)); - return nullable(delta); - } - - /// - /// Adnvance wheel and return all timers expired during wheel turn. - // - /// Params: - /// ticks = how many ticks to advance. Must be in range 0 <= 256 - /// Returns: list of expired timers - /// - import std.array : Appender; - void advance(this W)(ulong ticks, ref Appender!(T[]) app) - { - if (ticks > l2t(0)) - { - throw new AdvanceWheelError("You can't advance that much"); - } - if (ticks == 0) - { - throw new AdvanceWheelError("ticks must be > 0"); - } - debug(timingwheels) safe_tracef("advancing %d ticks", ticks); - auto level = &levels[0]; - - while(ticks) - { - ticks--; - immutable now = ++level.now; - immutable slot_index = now & MASK; - auto slot = &level.slots[slot_index]; - //debug(timingwheels) safe_tracef("level 0, now=%s", now); - while(slot.head) - { - auto le = slot.head; - auto timer = le.timer; - debug(timingwheels) safe_tracef("return timer: %s, scheduled at %s", timer, le.scheduled_at); - app.put(timer); - dl_unlink(le, &slot.head); - returnToFreeList(le); - ptrs.remove(timer.id()); - } - if (slot_index == 0) - { - advance_level(1); - } - } - } - auto totalTimers() pure @safe @nogc - { - return ptrs.length(); - } - auto allTimers() @safe @nogc - { - struct AllTimers - { - HashMap!(TimerIdType, T) _map; - auto timers() - { - return _map.byValue; - } - auto length() - { - return _map.length(); - } - bool contains(TimerIdType id) - { - return _map.contains(id); - } - } - alias AllResult = automem.RefCounted!(AllTimers, Mallocator); - AllTimers result; - foreach (p; ptrs.byPair) - { - result._map[p.key] = p.value.timer; - } - return result; - } - // - // ticks until next event on level 0 or until next wheel rotation - // If you have empty ticks it is safe to sleep - you will not miss anything, just wake up - // at the time when next timer have to be processed. - //Returns: number of safe "sleep" ticks. - // - private int ticksUntilNextEvent() - out(r; r<=256) - { - int result = 1; - auto level = &levels[0]; - immutable uint now = levels[0].now & MASK; - auto slot = (now + 1) & MASK; - //assert(level.slots[now].head == null); - do - { - if (level.slots[slot].head != null) - { - break; - } - result++; - slot = (slot + 1) & MASK; - } - while(slot != now); - - return min(result, 256-now); - } - - private void advance_level(int level_index) - in(level_index>0) - { - debug(timingwheels) safe_tracef("running advance on level %d", level_index); - immutable now0 = levels[0].now; - auto level = &levels[level_index]; - immutable now = ++level.now; - immutable slot_index = now & MASK; - debug(timingwheels) safe_tracef("level %s, now=%s", level_index, now); - auto slot = &level.slots[slot_index]; - debug(timingwheels) safe_tracef("haldle l%s:s%s timers", level_index, slot_index); - while(slot.head) - { - auto listElement = slot.head; - - immutable delta = listElement.scheduled_at - now0; - size_t lower_level_index = 0; - long t = delta; - size_t s = 1; // width of the slot in ticks on level - size_t shift = 0; - while(t > s<<8) // while t > slots on level - { - t -= (SLOTS - (levels[lower_level_index].now & MASK)) * s; - lower_level_index++; - s = s << 8; - shift += 8; - } - auto mask = s - 1; - size_t lower_level_slot_index = (levels[lower_level_index].now + (t>>shift) + ((t&mask)>0?1:0)) & MASK; - debug(timingwheels) safe_tracef("move timer id: %s, scheduledAt; %d to level %s, slot: %s (delta=%s)", - listElement.timer.id(), listElement.scheduled_at, lower_level_index, lower_level_slot_index, delta); - listElement.position = ((lower_level_index<<8) | lower_level_slot_index) & 0xffff; - dl_relink(listElement, &slot.head, &levels[lower_level_index].slots[lower_level_slot_index].head); - } - if (slot_index == 0 && level_index < LEVEL_MAX) - { - advance_level(level_index+1); - } - } - void reset() { - this = TimingWheels!T(); - } - + auto currStdTime(Duration tick) { + return startedAt + levels[0].now * tick.split!"hnsecs".hnsecs; + } + + /// + /// Schedule timer to $(B ticks) ticks forward from internal 'now'. + ///Params: + /// timer = timer to schedule; + /// ticks = ticks in the future to schedule timer. (0 < ticks < ulong.max); + ///Returns: + /// void + ///Throws: + /// ScheduleTimerError + /// when thicks == 0 + /// or when timer already scheduled + /// + void schedule(T)(T timer, const ulong ticks) { + if (ticks == 0) { + throw new ScheduleTimerError("ticks can't be 0"); + } + + auto timer_id = timer.id(); + if (ptrs.contains(timer_id)) { + throw new ScheduleTimerError("Timer already scheduled"); + } + + size_t level_index = 0; + long t = ticks; + long s = 1; // width of the slot in ticks on level + long shift = 0; + while (t > s << 8) // while t > slots on level + { + t -= (SLOTS - (levels[level_index].now & MASK)) * s; + level_index++; + s = s << 8; + shift += 8; + } + + auto level = &levels[level_index]; + auto mask = s - 1; + size_t slot_index = + (level.now + (t >> shift) + ((t & mask) > 0 ? 1 : 0)) & MASK; + auto slot = &levels[level_index].slots[slot_index]; + debug(timingwheels) + safe_tracef("use level/slot %d/%d, level now: %d", level_index, + slot_index, level.now); + auto le = getOrCreate(); + le.timer = timer; + le.position = ((level_index << 8) | slot_index) & 0xffff; + le.scheduled_at = levels[0].now + ticks; + dl_insertFront(le, &slot.head); + ptrs[timer_id] = le; + debug(timingwheels) + safe_tracef( + "scheduled timer id: %s, ticks: %s, now: %d, scheduled at: %s to level: %s, slot %s", + timer_id, + ticks, + levels[0].now, + le.scheduled_at, + level_index, + slot_index + ); + } + + /// Cancel timer + ///Params: + /// timer = timer to cancel + ///Returns: + /// void + ///Throws: + /// CancelTimerError + /// if timer not in wheel + bool cancel(T)(T timer) { + // get list element pointer + auto v = ptrs.fetch(timer.id()); + if (!v.ok) { + return false; + } + + auto le = v.value; + immutable level_index = le.position >> 8; + immutable slot_index = le.position & 0xff; + assert(timer is le.timer); + debug(timingwheels) + safe_tracef("cancel timer, l:%d, s:%d", level_index, slot_index); + dl_unlink(le, &levels[level_index].slots[slot_index].head); + returnToFreeList(le); + ptrs.remove(timer.id()); + return true; + } + + /// Number of ticks to rotate wheels until internal wheel 'now' + /// catch up with real world realTime. + /// Calculation based on time when wheels were stared and total + /// numer of ticks pasded. + ///Params: + /// tick = your tick length (Duration) + /// realTime = current real world now (Clock.currStdTime) + ///Returns: ticks to advance so that we catch up real world current time + int ticksToCatchUp(Duration tick, ulong realTime) { + auto c = startedAt + tick.split!"hnsecs".hnsecs * levels[0].now; + auto v = (realTime - c) / tick.split!"hnsecs".hnsecs; + if (v > 256) { + return 256; + } + + return cast(int) v; + } + + /// Time until next scheduled timer event. + /// You provide tick size and current real world time. + /// This function find ticks until next event and use time of the start and + /// total steps executed to calculate time delta from $(B realNow) to next event. + ///Params: + /// tick = your accepted tick duration. + /// realNow = real world now, result of Clock.currStdTime + ///Returns: time until next event. Can be zero or negative in case you have already expired events. + /// + Nullable!Duration timeUntilNextEvent(const Duration tick, ulong realNow) { + assert(startedAt > 0, "Forgot to call init()?"); + if (totalTimers == 0) + return typeof(return).init; + immutable n = ticksUntilNextEvent(); + immutable target = + startedAt + (levels[0].now + n) * tick.split!"hnsecs".hnsecs; + auto delta = (target - realNow).hnsecs; + debug(timingwheels) + safe_tracef("ticksUntilNextEvent=%s, tick=%s, startedAt=%s", n, + tick, SysTime(startedAt)); + return nullable(delta); + } + + /// + /// Adnvance wheel and return all timers expired during wheel turn. + // + /// Params: + /// ticks = how many ticks to advance. Must be in range 0 <= 256 + /// Returns: list of expired timers + /// + import std.array : Appender; + void advance(this W)(ulong ticks, ref Appender!(T[]) app) { + if (ticks > l2t(0)) { + throw new AdvanceWheelError("You can't advance that much"); + } + + if (ticks == 0) { + throw new AdvanceWheelError("ticks must be > 0"); + } + + debug(timingwheels) + safe_tracef("advancing %d ticks", ticks); + auto level = &levels[0]; + + while (ticks) { + ticks--; + immutable now = ++level.now; + immutable slot_index = now & MASK; + auto slot = &level.slots[slot_index]; + //debug(timingwheels) safe_tracef("level 0, now=%s", now); + while (slot.head) { + auto le = slot.head; + auto timer = le.timer; + debug(timingwheels) + safe_tracef("return timer: %s, scheduled at %s", timer, + le.scheduled_at); + app.put(timer); + dl_unlink(le, &slot.head); + returnToFreeList(le); + ptrs.remove(timer.id()); + } + + if (slot_index == 0) { + advance_level(1); + } + } + } + + auto totalTimers() pure @safe @nogc { + return ptrs.length(); + } + + auto allTimers() @safe @nogc { + struct AllTimers { + HashMap!(TimerIdType, T) _map; + auto timers() { + return _map.byValue; + } + + auto length() { + return _map.length(); + } + + bool contains(TimerIdType id) { + return _map.contains(id); + } + } + + alias AllResult = automem.RefCounted!(AllTimers, Mallocator); + AllTimers result; + foreach (p; ptrs.byPair) { + result._map[p.key] = p.value.timer; + } + + return result; + } + + // + // ticks until next event on level 0 or until next wheel rotation + // If you have empty ticks it is safe to sleep - you will not miss anything, just wake up + // at the time when next timer have to be processed. + //Returns: number of safe "sleep" ticks. + // + private int ticksUntilNextEvent() out(r; r <= 256) { + int result = 1; + auto level = &levels[0]; + immutable uint now = levels[0].now & MASK; + auto slot = (now + 1) & MASK; + //assert(level.slots[now].head == null); + do { + if (level.slots[slot].head != null) { + break; + } + + result++; + slot = (slot + 1) & MASK; + } while (slot != now); + + return min(result, 256 - now); + } + + private void advance_level(int level_index) in(level_index > 0) { + debug(timingwheels) + safe_tracef("running advance on level %d", level_index); + immutable now0 = levels[0].now; + auto level = &levels[level_index]; + immutable now = ++level.now; + immutable slot_index = now & MASK; + debug(timingwheels) + safe_tracef("level %s, now=%s", level_index, now); + auto slot = &level.slots[slot_index]; + debug(timingwheels) + safe_tracef("haldle l%s:s%s timers", level_index, slot_index); + while (slot.head) { + auto listElement = slot.head; + + immutable delta = listElement.scheduled_at - now0; + size_t lower_level_index = 0; + long t = delta; + size_t s = 1; // width of the slot in ticks on level + size_t shift = 0; + while (t > s << 8) // while t > slots on level + { + t -= (SLOTS - (levels[lower_level_index].now & MASK)) * s; + lower_level_index++; + s = s << 8; + shift += 8; + } + + auto mask = s - 1; + size_t lower_level_slot_index = (levels[lower_level_index].now + + (t >> shift) + ((t & mask) > 0 ? 1 : 0)) & MASK; + debug(timingwheels) + safe_tracef( + "move timer id: %s, scheduledAt; %d to level %s, slot: %s (delta=%s)", + listElement.timer.id(), + listElement.scheduled_at, + lower_level_index, + lower_level_slot_index, + delta + ); + listElement.position = + ((lower_level_index << 8) | lower_level_slot_index) & 0xffff; + dl_relink( + listElement, &slot.head, + &levels[lower_level_index].slots[lower_level_slot_index].head); + } + + if (slot_index == 0 && level_index < LEVEL_MAX) { + advance_level(level_index + 1); + } + } + + void reset() { + this = TimingWheels!T(); + } } version(twtesting): @("TimingWheels") -unittest -{ - import std.stdio; - globalLogLevel = LogLevel.info; - TimingWheels!Timer w; - w.init(); - assert(w.t2l(1) == 0); - assert(w.t2s(1, 0) == 1); - immutable t = 0x00_00_00_11_00_00_00_77; - immutable level = w.t2l(t); - assert(level==4); - immutable slot = w.t2s(t, level); - assert(slot == 0x11); - auto timer = new Timer(); - () @nogc @safe { - w.schedule(timer, 2); - bool thrown; - // check that you can't schedule same timer twice - try - { - w.schedule(timer, 5); - } - catch(ScheduleTimerError e) - { - thrown = true; - } - assert(thrown); - thrown = false; - try - { - w.advance(1024); - } - catch(AdvanceWheelError e) - { - thrown = true; - } - assert(thrown); - thrown = false; - w.cancel(timer); - w.advance(1); - }(); - w = TimingWheels!Timer(); - w.init(); - w.schedule(timer, 1); - auto r = w.advance(1); - assert(r.timers.count == 1); - w.schedule(timer, 256); - r = w.advance(255); - assert(r.timers.count == 0); - r = w.advance(1); - assert(r.timers.count == 1); - w.schedule(timer, 256*256); - int c; - for(int i=0;i<256;i++) - { - r = w.advance(256); - c += r.timers.count; - } - assert(c==1); +unittest { + import std.stdio; + globalLogLevel = LogLevel.info; + TimingWheels!Timer w; + w.init(); + assert(w.t2l(1) == 0); + assert(w.t2s(1, 0) == 1); + immutable t = 0x00_00_00_11_00_00_00_77; + immutable level = w.t2l(t); + assert(level == 4); + immutable slot = w.t2s(t, level); + assert(slot == 0x11); + auto timer = new Timer(); + () @nogc @safe { + w.schedule(timer, 2); + bool thrown; + // check that you can't schedule same timer twice + try { + w.schedule(timer, 5); + } catch (ScheduleTimerError e) { + thrown = true; + } + + assert(thrown); + thrown = false; + try { + w.advance(1024); + } catch (AdvanceWheelError e) { + thrown = true; + } + + assert(thrown); + thrown = false; + w.cancel(timer); + w.advance(1); + }(); + w = TimingWheels!Timer(); + w.init(); + w.schedule(timer, 1); + auto r = w.advance(1); + assert(r.timers.count == 1); + w.schedule(timer, 256); + r = w.advance(255); + assert(r.timers.count == 0); + r = w.advance(1); + assert(r.timers.count == 1); + w.schedule(timer, 256 * 256); + int c; + for (int i = 0; i < 256; i++) { + r = w.advance(256); + c += r.timers.count; + } + + assert(c == 1); } -@("rt") -@Tags("noauto") -unittest -{ - globalLogLevel = LogLevel.info; - TimingWheels!Timer w; - Duration Tick = 5.msecs; - w.init(); - ulong now = Clock.currStdTime; - assert(now - w.currStdTime(Tick) < 5*10_000); - Thread.sleep(2*Tick); - now = Clock.currStdTime; - assert((now - w.currStdTime(Tick))/10_000 - (2*Tick).split!"msecs".msecs < 10); - auto toCatchUp = w.ticksToCatchUp(Tick, now); - toCatchUp.shouldEqual(2); - auto t = w.advance(toCatchUp); - toCatchUp = w.ticksToCatchUp(Tick, now); - toCatchUp.shouldEqual(0); + +@("rt") @Tags("noauto") +unittest { + globalLogLevel = LogLevel.info; + TimingWheels!Timer w; + Duration Tick = 5.msecs; + w.init(); + ulong now = Clock.currStdTime; + assert(now - w.currStdTime(Tick) < 5 * 10_000); + Thread.sleep(2 * Tick); + now = Clock.currStdTime; + assert((now - w.currStdTime(Tick)) / 10_000 - (2 * Tick).split!"msecs".msecs + < 10); + auto toCatchUp = w.ticksToCatchUp(Tick, now); + toCatchUp.shouldEqual(2); + auto t = w.advance(toCatchUp); + toCatchUp = w.ticksToCatchUp(Tick, now); + toCatchUp.shouldEqual(0); } + @("cancel") -unittest -{ - globalLogLevel = LogLevel.info; - TimingWheels!Timer w; - w.init(); - Timer timer0 = new Timer(); - Timer timer1 = new Timer(); - w.schedule(timer0, 256); - w.schedule(timer1, 256+128); - auto r = w.advance(255); - assert(r.timers.count == 0); - w.cancel(timer0); - r = w.advance(1); - assert(r.timers.count == 0); - assertThrown!CancelTimerError(w.cancel(timer0)); - w.cancel(timer1); +unittest { + globalLogLevel = LogLevel.info; + TimingWheels!Timer w; + w.init(); + Timer timer0 = new Timer(); + Timer timer1 = new Timer(); + w.schedule(timer0, 256); + w.schedule(timer1, 256 + 128); + auto r = w.advance(255); + assert(r.timers.count == 0); + w.cancel(timer0); + r = w.advance(1); + assert(r.timers.count == 0); + assertThrown!CancelTimerError(w.cancel(timer0)); + w.cancel(timer1); } + @("ticksUntilNextEvent") -unittest -{ - globalLogLevel = LogLevel.info; - TimingWheels!Timer w; - w.init(); - auto s = w.ticksUntilNextEvent; - assert(s==256); - auto r = w.advance(s); - assert(r.timers.count == 0); - Timer t = new Timer(); - w.schedule(t, 50); - s = w.ticksUntilNextEvent; - assert(s==50); - r = w.advance(s); - assert(r.timers.count == 1); +unittest { + globalLogLevel = LogLevel.info; + TimingWheels!Timer w; + w.init(); + auto s = w.ticksUntilNextEvent; + assert(s == 256); + auto r = w.advance(s); + assert(r.timers.count == 0); + Timer t = new Timer(); + w.schedule(t, 50); + s = w.ticksUntilNextEvent; + assert(s == 50); + r = w.advance(s); + assert(r.timers.count == 1); } -@("load") -@Serial -unittest -{ - import std.array:array; - globalLogLevel = LogLevel.info; - enum TIMERS = 100_000; - Timer._current_id = 1; - auto w = TimingWheels!Timer(); - w.init(); - for(int i=1;i<=TIMERS;i++) - { - auto t = new Timer(); - w.schedule(t, i); - } - int counter; - for(int i=1;i<=TIMERS;i++) - { - auto r = w.advance(1); - auto timers = r.timers; - auto t = timers.array()[0]; - assert(t.id == i, "expected t.id=%s, got %s".format(t.id, i)); - assert(timers.count == 1); - counter++; - } - assert(counter == TIMERS, "expected 100 timers, got %d".format(counter)); - - for(int i=1;i<=TIMERS;i++) - { - auto t = new Timer(); - w.schedule(t, i); - } - counter = 0; - for(int i=TIMERS+1;i<=2*TIMERS;i++) - { - auto r = w.advance(1); - auto timers = r.timers; - auto t = timers.array()[0]; - assert(t.id == i, "expected t.id=%s, got %s".format(t.id, i)); - assert(timers.count == 1); - counter++; - } - assert(counter == TIMERS, "expected 100 timers, got %d".format(counter)); - +@("load") @Serial +unittest { + import std.array : array; + globalLogLevel = LogLevel.info; + enum TIMERS = 100_000; + Timer._current_id = 1; + auto w = TimingWheels!Timer(); + w.init(); + for (int i = 1; i <= TIMERS; i++) { + auto t = new Timer(); + w.schedule(t, i); + } + + int counter; + for (int i = 1; i <= TIMERS; i++) { + auto r = w.advance(1); + auto timers = r.timers; + auto t = timers.array()[0]; + assert(t.id == i, "expected t.id=%s, got %s".format(t.id, i)); + assert(timers.count == 1); + counter++; + } + + assert(counter == TIMERS, "expected 100 timers, got %d".format(counter)); + + for (int i = 1; i <= TIMERS; i++) { + auto t = new Timer(); + w.schedule(t, i); + } + + counter = 0; + for (int i = TIMERS + 1; i <= 2 * TIMERS; i++) { + auto r = w.advance(1); + auto timers = r.timers; + auto t = timers.array()[0]; + assert(t.id == i, "expected t.id=%s, got %s".format(t.id, i)); + assert(timers.count == 1); + counter++; + } + + assert(counter == TIMERS, "expected 100 timers, got %d".format(counter)); } + // @("cornercase") // @Serial // unittest @@ -834,104 +830,101 @@ unittest /// /// /// -@("example") -@Tags("noauto") -@Values(1.msecs,2.msecs,3.msecs,4.msecs,5.msecs,6.msecs,7.msecs,8.msecs, 9.msecs,10.msecs) -@Serial -unittest -{ - import std; - globalLogLevel = LogLevel.info; - auto rnd = Random(142); - auto Tick = getValue!Duration(); - /// track execution - int counter; - SysTime last; - - /// this is our Timer - class Timer - { - static ulong __id; - private ulong _id; - private string _name; - this(string name) - { - _id = __id++; - _name = name; - } - /// must provide id() method - ulong id() - { - return _id; - } - } - - enum IOWakeUpInterval = 100; // to simulate random IO wakeups in interval 0 - 100.msecs - - // each tick span 5 msecs - this is our link with time in reality - TimingWheels!Timer w; - w.init(); - auto durationToTicks(Duration d) - { - // we have to adjust w.now and realtime 'now' before scheduling timer - auto real_now = Clock.currStdTime; - auto tw_now = w.currStdTime(Tick); - auto delay = (real_now - tw_now).hnsecs; - return (d + delay)/Tick; - } - void process_timer(Timer t) - { - switch(t._name) - { - case "periodic": - if ( last.stdTime == 0) - { - // initialize tracking - last = Clock.currTime - 50.msecs; - } - auto delta = Clock.currTime - last; - assert(delta - 50.msecs <= max(Tick + Tick/20, 5.msecs), "delta-50.msecs=%s".format(delta-50.msecs)); - writefln("@ %s - delta: %sms (should be 50ms)", t._name, (Clock.currTime - last).split!"msecs".msecs); - last = Clock.currTime; - counter++; - w.schedule(t, durationToTicks(50.msecs)); // rearm - break; - default: - writefln("@ %s", t._name); - break; - } - } - // emulate some random initial delay - auto randomInitialDelay = uniform(0, 500, rnd).msecs; - Thread.sleep(randomInitialDelay); - // - // start one arbitrary timer and one periodic timer - // - auto some_timer = new Timer("some"); - auto periodic_timer = new Timer("periodic"); - w.schedule(some_timer, durationToTicks(32.msecs)); - w.schedule(periodic_timer, durationToTicks(50.msecs)); - - while(counter < 10) - { - auto realNow = Clock.currStdTime; - auto randomIoInterval = uniform(0, IOWakeUpInterval, rnd).msecs; - auto nextTimerEvent = max(w.timeUntilNextEvent(Tick, realNow), 0.msecs); - // wait for what should happen earlier - auto time_to_sleep = min(randomIoInterval, nextTimerEvent); - writefln("* sleep until timer event or random I/O for %s", time_to_sleep); - Thread.sleep(time_to_sleep); - // make steps if required - int ticks = w.ticksToCatchUp(Tick, Clock.currStdTime); - if (ticks > 0) - { - auto wr = w.advance(ticks); - foreach(t; wr.timers) - { - process_timer(t); - } - } - // emulate some random processing time - Thread.sleep(uniform(0, 5, rnd).msecs); - } +@("example") @Tags("noauto") +@Values(1.msecs, 2.msecs, 3.msecs, 4.msecs, 5.msecs, 6.msecs, 7.msecs, 8.msecs, + 9.msecs, 10.msecs) @Serial +unittest { + import std; + globalLogLevel = LogLevel.info; + auto rnd = Random(142); + auto Tick = getValue!Duration(); + /// track execution + int counter; + SysTime last; + + /// this is our Timer + class Timer { + static ulong __id; + private ulong _id; + private string _name; + this(string name) { + _id = __id++; + _name = name; + } + + /// must provide id() method + ulong id() { + return _id; + } + } + + enum IOWakeUpInterval = + 100; // to simulate random IO wakeups in interval 0 - 100.msecs + + // each tick span 5 msecs - this is our link with time in reality + TimingWheels!Timer w; + w.init(); + auto durationToTicks(Duration d) { + // we have to adjust w.now and realtime 'now' before scheduling timer + auto real_now = Clock.currStdTime; + auto tw_now = w.currStdTime(Tick); + auto delay = (real_now - tw_now).hnsecs; + return (d + delay) / Tick; + } + + void process_timer(Timer t) { + switch (t._name) { + case "periodic": + if (last.stdTime == 0) { + // initialize tracking + last = Clock.currTime - 50.msecs; + } + + auto delta = Clock.currTime - last; + assert(delta - 50.msecs <= max(Tick + Tick / 20, 5.msecs), + "delta-50.msecs=%s".format(delta - 50.msecs)); + writefln("@ %s - delta: %sms (should be 50ms)", t._name, + (Clock.currTime - last).split!"msecs".msecs); + last = Clock.currTime; + counter++; + w.schedule(t, durationToTicks(50.msecs)); // rearm + break; + default: + writefln("@ %s", t._name); + break; + } + } + + // emulate some random initial delay + auto randomInitialDelay = uniform(0, 500, rnd).msecs; + Thread.sleep(randomInitialDelay); + // + // start one arbitrary timer and one periodic timer + // + auto some_timer = new Timer("some"); + auto periodic_timer = new Timer("periodic"); + w.schedule(some_timer, durationToTicks(32.msecs)); + w.schedule(periodic_timer, durationToTicks(50.msecs)); + + while (counter < 10) { + auto realNow = Clock.currStdTime; + auto randomIoInterval = uniform(0, IOWakeUpInterval, rnd).msecs; + auto nextTimerEvent = max(w.timeUntilNextEvent(Tick, realNow), 0.msecs); + // wait for what should happen earlier + auto time_to_sleep = min(randomIoInterval, nextTimerEvent); + writefln("* sleep until timer event or random I/O for %s", + time_to_sleep); + Thread.sleep(time_to_sleep); + // make steps if required + int ticks = w.ticksToCatchUp(Tick, Clock.currStdTime); + if (ticks > 0) { + auto wr = w.advance(ticks); + foreach (t; wr.timers) { + process_timer(t); + } + } + + // emulate some random processing time + Thread.sleep(uniform(0, 5, rnd).msecs); + } } diff --git a/source/concurrency/utils.d b/source/concurrency/utils.d index aa549b9..94ee6a6 100644 --- a/source/concurrency/utils.d +++ b/source/concurrency/utils.d @@ -4,84 +4,103 @@ import std.traits; /// Helper used in with() to easily access `shared` methods of a class struct SharedGuard(T) if (is(T == class)) { - import core.sync.mutex : Mutex; + import core.sync.mutex : Mutex; private: - Mutex mutex; + Mutex mutex; public: - T reg; - alias reg this; - @disable this(); - @disable this(this); - static SharedGuard acquire(shared T reg, Mutex mutex) { - mutex.lock_nothrow(); - auto instance = SharedGuard.init; - instance.reg = (cast() reg); - instance.mutex = mutex; - return instance; - } - ~this() { - mutex.unlock_nothrow(); - } + T reg; + alias reg this; + @disable + this(); + @disable + this(this); + static SharedGuard acquire(shared T reg, Mutex mutex) { + mutex.lock_nothrow(); + auto instance = SharedGuard.init; + instance.reg = (cast() reg); + instance.mutex = mutex; + return instance; + } + + ~this() { + mutex.unlock_nothrow(); + } } /// A manually constructed closure, aimed at shared struct Closure(Fun, Args...) { - Fun fun; - Args args; - auto apply() shared { - return fun((cast()this).args); - } + Fun fun; + Args args; + auto apply() shared { + return fun((cast() this).args); + } } /// This is a terrible workaround for closure bugs in D. When a local is used in a delegate D is supposed to move it to the heap. I haven't seen that happen in all cases so we have this manual workaround. -auto closure(Fun, Args...)(Fun fun, Args args) @trusted if (isFunctionPointer!Fun) { - static assert(!hasFunctionAttributes!(Fun, "@system"), "no @system functions please"); - auto cl = cast(shared)new Closure!(Fun, Args)(fun, args); - /// need to cast to @safe because a @trusted delegate doesn't fit a @safe one... - static if (hasFunctionAttributes!(Fun, "nothrow")) - alias ResultType = void delegate() nothrow shared @safe; - else - alias ResultType = void delegate() shared @safe; - return cast(ResultType)&cl.apply; +auto closure(Fun, Args...)(Fun fun, Args args) @trusted + if (isFunctionPointer!Fun) { + static assert(!hasFunctionAttributes!(Fun, "@system"), + "no @system functions please"); + auto cl = cast(shared) new Closure!(Fun, Args)(fun, args); + /// need to cast to @safe because a @trusted delegate doesn't fit a @safe one... + static if (hasFunctionAttributes!(Fun, "nothrow")) + alias ResultType = void delegate() nothrow shared @safe; + else + alias ResultType = void delegate() shared @safe; + return cast(ResultType) &cl.apply; } /// don't want vibe-d to overwrite the scheduler void resetScheduler() @trusted { - import std.concurrency : scheduler; - if (scheduler !is null) - scheduler = null; + import std.concurrency : scheduler; + if (scheduler !is null) + scheduler = null; } enum NoVoid(T) = !is(T == void); void spin_yield() nothrow @trusted @nogc { - // TODO: could use the pause asm instruction - // it is available in LDC as intrinsic... but not in DMD - import core.thread : Thread; + // TODO: could use the pause asm instruction + // it is available in LDC as intrinsic... but not in DMD + import core.thread : Thread; - Thread.yield(); + Thread.yield(); } /// ugly ugly -static if (__traits(compiles, () { import core.atomic : casWeak; }) && __traits(compiles, () { - import core.internal.atomic : atomicCompareExchangeWeakNoResult; - })) - public import core.atomic : casWeak; - else { - import core.atomic : MemoryOrder; - auto casWeak(MemoryOrder M1 = MemoryOrder.seq, MemoryOrder M2 = MemoryOrder.seq, T, V1, V2)(T* here, V1 ifThis, V2 writeThis) pure nothrow @nogc @safe { - import core.atomic : cas; - - static if (__traits(compiles, cas!(M1, M2)(here, ifThis, writeThis))) - return cas!(M1, M2)(here, ifThis, writeThis); - else - return cas(here, ifThis, writeThis); - } - } - -enum isThreadSafeFunction(alias Fun) = !hasFunctionAttributes!(Fun, "@system") && (isFunction!Fun || isFunctionPointer!Fun || hasFunctionAttributes!(Fun, "shared")); - -enum isThreadSafeCallable(alias Fun) = (isAggregateType!Fun && isCallable!Fun && __traits(compiles, () @safe { shared Fun f; f(); })) || (isSomeFunction!Fun && isThreadSafeFunction!Fun); +static if (__traits(compiles, () { + import core.atomic : casWeak; + }) && __traits(compiles, () { + import core.internal.atomic : atomicCompareExchangeWeakNoResult; + })) + public import core.atomic : casWeak; +else { + import core.atomic : MemoryOrder; + auto casWeak( + MemoryOrder M1 = MemoryOrder.seq, + MemoryOrder M2 = MemoryOrder.seq, + T, + V1, + V2 + )(T* here, V1 ifThis, V2 writeThis) pure nothrow @nogc @safe { + import core.atomic : cas; + + static if (__traits(compiles, cas!(M1, M2)(here, ifThis, writeThis))) + return cas!(M1, M2)(here, ifThis, writeThis); + else + return cas(here, ifThis, writeThis); + } +} + +enum isThreadSafeFunction(alias Fun) = !hasFunctionAttributes!(Fun, "@system") + && (isFunction!Fun || isFunctionPointer!Fun + || hasFunctionAttributes!(Fun, "shared")); + +enum isThreadSafeCallable(alias Fun) = + (isAggregateType!Fun && isCallable!Fun && __traits(compiles, () @safe { + shared Fun f; + f(); + })) || (isSomeFunction!Fun && isThreadSafeFunction!Fun); // Loads a function from the main process. // When using dynamic libraries globals and TLS variables are duplicated. @@ -90,31 +109,32 @@ enum isThreadSafeCallable(alias Fun) = (isAggregateType!Fun && isCallable!Fun && // We do this by exporting accessors functions which a dynamic library can // call to get access to the global. auto dynamicLoad(alias fun)() nothrow @trusted { - alias Fn = typeof(&fun); - __gshared Fn fn; + alias Fn = typeof(&fun); + __gshared Fn fn; - if (fn is null) - fn = dynamicLoadRaw!fun; + if (fn is null) + fn = dynamicLoadRaw!fun; - // If dynamic loading fails we just pick the local function. - // This serves two purposes, 1) if users aren't using dynamic - // libraries and the application isn't compiled with the proper linker - // flags for exporting functions, it won't be found, and 2) it will - // reference the function so it won't be compiled away. - if(fn is null) - fn = &fun; + // If dynamic loading fails we just pick the local function. + // This serves two purposes, 1) if users aren't using dynamic + // libraries and the application isn't compiled with the proper linker + // flags for exporting functions, it won't be found, and 2) it will + // reference the function so it won't be compiled away. + if (fn is null) + fn = &fun; - return fn; + return fn; } auto dynamicLoadRaw(alias fun)() nothrow @trusted { - alias Fn = typeof(&fun); - version (Windows) { - import core.sys.windows.windows; - return cast(Fn) GetProcAddress(GetModuleHandle(null), fun.mangleof); - } else version (Posix) { - import core.sys.posix.dlfcn : dlopen, dlsym, dlerror, RTLD_LAZY; - auto parent = dlopen(null, RTLD_LAZY); - return cast(Fn) dlsym(parent, fun.mangleof); - } else static assert(false, "platform not supported"); + alias Fn = typeof(&fun); + version(Windows) { + import core.sys.windows.windows; + return cast(Fn) GetProcAddress(GetModuleHandle(null), fun.mangleof); + } else version(Posix) { + import core.sys.posix.dlfcn : dlopen, dlsym, dlerror, RTLD_LAZY; + auto parent = dlopen(null, RTLD_LAZY); + return cast(Fn) dlsym(parent, fun.mangleof); + } else + static assert(false, "platform not supported"); } diff --git a/tests/ut/concurrency/asyncscope.d b/tests/ut/concurrency/asyncscope.d index 6218585..25eab59 100644 --- a/tests/ut/concurrency/asyncscope.d +++ b/tests/ut/concurrency/asyncscope.d @@ -6,179 +6,195 @@ import concurrency.sender : VoidSender, DoneSender, ThrowingSender; import concurrency.stoptoken : StopToken; import unit_threaded; -@("cleanup.empty") -@safe unittest { - auto s = asyncScope(); - s.cleanup.syncWait.assumeOk; - s.cleanup.syncWait.assumeOk; // test twice -} - -@("cleanup.voidsender.single") -@safe unittest { - auto s = asyncScope(); - s.spawn(VoidSender()).should == true; - s.cleanup.syncWait.assumeOk; - s.cleanup.syncWait.assumeOk; // test twice -} - -@("cleanup.voidsender.triple") -@safe unittest { - auto s = asyncScope(); - s.spawn(VoidSender()).should == true; - s.spawn(VoidSender()).should == true; - s.spawn(VoidSender()).should == true; - s.cleanup.syncWait.assumeOk; - s.cleanup.syncWait.assumeOk; // test twice -} - -@("cleanup.waitingsender.single") -@safe unittest { - auto s = asyncScope(); - s.spawn(waitingTask).should == true; - s.cleanup.syncWait.assumeOk; - s.cleanup.syncWait.assumeOk; // test twice -} - -@("cleanup.waitingsender.triple") -@safe unittest { - auto s = asyncScope(); - s.spawn(waitingTask).should == true; - s.spawn(waitingTask).should == true; - s.spawn(waitingTask).should == true; - s.cleanup.syncWait.assumeOk; - s.cleanup.syncWait.assumeOk; // test twice -} - -@("spawn.stopped") -@safe unittest { - auto s = asyncScope(); - s.cleanup.syncWait.assumeOk; - s.spawn(VoidSender()).should == false; -} - -@("spawn.error") -@safe unittest { - auto s = asyncScope(); - s.spawn(ThrowingSender()).should == true; - s.cleanup.syncWait.assumeOk.shouldThrow; - s.cleanup.syncWait.assumeOk.shouldThrow; // test twice -} - -@("spawn.reentry") -@safe unittest { - import concurrency.sender : justFrom; - auto s = asyncScope(); - s.spawn(justFrom(() shared { s.spawn(VoidSender()); })).should == true; - s.cleanup.syncWait.assumeOk; -} - -@("spawn.value.transform") -@safe unittest { - import concurrency.sender : just; - import concurrency.operations : then; - auto s = asyncScope(); - s.spawn(just(42).then((int) {})).should == true; - s.cleanup.syncWait.assumeOk; - s.cleanup.syncWait.assumeOk; // test twice -} - -@("cleanup.scoped") -@safe unittest { - import concurrency.operations : onTermination; - import core.atomic : atomicStore; - shared bool p; - { - auto s = asyncScope(); - s.spawn(waitingTask().onTermination(() shared { p.atomicStore(true); })); - } - p.should == true; -} - -@("cleanup.nested.struct") -@safe unittest { - import concurrency.operations : onTermination; - import core.atomic : atomicStore; - shared bool p; - static struct S { - shared AsyncScope s; - } - { - S s = S(asyncScope); - s.s.spawn(waitingTask().onTermination(() shared { p.atomicStore(true); })); - } - p.should == true; -} - -@("cleanup.nested.class") -@trusted unittest { - import concurrency.operations : onTermination; - import core.atomic : atomicStore; - shared bool p; - static class S { - shared AsyncScope s; - this() { - s = asyncScope(); - } - } - auto s = new S(); - s.s.spawn(waitingTask().onTermination(() shared { p.atomicStore(true); })); - destroy(s); - p.should == true; -} - -@("spawn.assert.thread") -@safe unittest { - import concurrency.thread : ThreadSender; - import concurrency.operations : then; - auto fail = ThreadSender().then(() shared { - assert(false, "bad things happen"); - }); - auto s = asyncScope(); - - s.spawn(fail).should == true; - s.cleanup.syncWait.shouldThrow!Throwable; -} - -@("spawn.assert.inline") -@trusted unittest { - import concurrency.thread : ThreadSender; - import concurrency.sender : justFrom; - - auto fail = justFrom(() shared { - assert(0, "bad things happen 2"); - }); - auto s = asyncScope(); - - s.spawn(fail).shouldThrow!Throwable; - s.cleanup.syncWait.shouldThrow!Throwable; -} - -@("cleanup.assert.then") -@safe unittest { - import concurrency.thread : ThreadSender; - import concurrency.operations : then; - auto s = asyncScope(); - - s.cleanup.then(() shared { assert(false, "Ohh no!"); }).syncWait.shouldThrow!Throwable; +@("cleanup.empty") @safe +unittest { + auto s = asyncScope(); + s.cleanup.syncWait.assumeOk; + s.cleanup.syncWait.assumeOk; // test twice +} + +@("cleanup.voidsender.single") @safe +unittest { + auto s = asyncScope(); + s.spawn(VoidSender()).should == true; + s.cleanup.syncWait.assumeOk; + s.cleanup.syncWait.assumeOk; // test twice +} + +@("cleanup.voidsender.triple") @safe +unittest { + auto s = asyncScope(); + s.spawn(VoidSender()).should == true; + s.spawn(VoidSender()).should == true; + s.spawn(VoidSender()).should == true; + s.cleanup.syncWait.assumeOk; + s.cleanup.syncWait.assumeOk; // test twice +} + +@("cleanup.waitingsender.single") @safe +unittest { + auto s = asyncScope(); + s.spawn(waitingTask).should == true; + s.cleanup.syncWait.assumeOk; + s.cleanup.syncWait.assumeOk; // test twice +} + +@("cleanup.waitingsender.triple") @safe +unittest { + auto s = asyncScope(); + s.spawn(waitingTask).should == true; + s.spawn(waitingTask).should == true; + s.spawn(waitingTask).should == true; + s.cleanup.syncWait.assumeOk; + s.cleanup.syncWait.assumeOk; // test twice +} + +@("spawn.stopped") @safe +unittest { + auto s = asyncScope(); + s.cleanup.syncWait.assumeOk; + s.spawn(VoidSender()).should == false; +} + +@("spawn.error") @safe +unittest { + auto s = asyncScope(); + s.spawn(ThrowingSender()).should == true; + s.cleanup.syncWait.assumeOk.shouldThrow; + s.cleanup.syncWait.assumeOk.shouldThrow; // test twice +} + +@("spawn.reentry") @safe +unittest { + import concurrency.sender : justFrom; + auto s = asyncScope(); + s.spawn(justFrom(() shared { + s.spawn(VoidSender()); + })).should == true; + s.cleanup.syncWait.assumeOk; +} + +@("spawn.value.transform") @safe +unittest { + import concurrency.sender : just; + import concurrency.operations : then; + auto s = asyncScope(); + s.spawn(just(42).then((int) {})).should == true; + s.cleanup.syncWait.assumeOk; + s.cleanup.syncWait.assumeOk; // test twice +} + +@("cleanup.scoped") @safe +unittest { + import concurrency.operations : onTermination; + import core.atomic : atomicStore; + shared bool p; + { + auto s = asyncScope(); + s.spawn(waitingTask().onTermination(() shared { + p.atomicStore(true); + })); + } + + p.should == true; +} + +@("cleanup.nested.struct") @safe +unittest { + import concurrency.operations : onTermination; + import core.atomic : atomicStore; + shared bool p; + static struct S { + shared AsyncScope s; + } + + { + S s = S(asyncScope); + s.s.spawn(waitingTask().onTermination(() shared { + p.atomicStore(true); + })); + } + + p.should == true; +} + +@("cleanup.nested.class") @trusted +unittest { + import concurrency.operations : onTermination; + import core.atomic : atomicStore; + shared bool p; + static class S { + shared AsyncScope s; + this() { + s = asyncScope(); + } + } + + auto s = new S(); + s.s.spawn(waitingTask().onTermination(() shared { + p.atomicStore(true); + })); + destroy(s); + p.should == true; +} + +@("spawn.assert.thread") @safe +unittest { + import concurrency.thread : ThreadSender; + import concurrency.operations : then; + auto fail = ThreadSender().then(() shared { + assert(false, "bad things happen"); + }); + auto s = asyncScope(); + + s.spawn(fail).should == true; + s.cleanup.syncWait.shouldThrow!Throwable; +} + +@("spawn.assert.inline") @trusted +unittest { + import concurrency.thread : ThreadSender; + import concurrency.sender : justFrom; + + auto fail = justFrom(() shared { + assert(0, "bad things happen 2"); + }); + auto s = asyncScope(); + + s.spawn(fail).shouldThrow!Throwable; + s.cleanup.syncWait.shouldThrow!Throwable; +} + +@("cleanup.assert.then") @safe +unittest { + import concurrency.thread : ThreadSender; + import concurrency.operations : then; + auto s = asyncScope(); + + s.cleanup.then(() shared { + assert(false, "Ohh no!"); + }).syncWait.shouldThrow!Throwable; } auto waitingTask() { - import concurrency.thread : ThreadSender; - import concurrency.operations : withStopToken; - - return ThreadSender().withStopToken((StopToken token) @trusted { - import core.thread : Thread; - while (!token.isStopRequested) { Thread.yield(); } - }); -} - -@("withScheduler") -@safe unittest { - import concurrency.sender : VoidSender; - import concurrency.operations : withScheduler; - import concurrency.scheduler : localThreadScheduler; - auto s = asyncScope(); - - s.spawn(VoidSender().withScheduler(localThreadScheduler)); - s.cleanup.syncWait.assumeOk; + import concurrency.thread : ThreadSender; + import concurrency.operations : withStopToken; + + return ThreadSender().withStopToken((StopToken token) @trusted { + import core.thread : Thread; + while (!token.isStopRequested) { + Thread.yield(); + } + }); +} + +@("withScheduler") @safe +unittest { + import concurrency.sender : VoidSender; + import concurrency.operations : withScheduler; + import concurrency.scheduler : localThreadScheduler; + auto s = asyncScope(); + + s.spawn(VoidSender().withScheduler(localThreadScheduler)); + s.cleanup.syncWait.assumeOk; } diff --git a/tests/ut/concurrency/fork.d b/tests/ut/concurrency/fork.d index cf49fa6..4c01675 100644 --- a/tests/ut/concurrency/fork.d +++ b/tests/ut/concurrency/fork.d @@ -1,6 +1,6 @@ module ut.concurrency.fork; -version (Posix): +version(Posix): import concurrency; import concurrency.fork; @@ -9,13 +9,16 @@ import concurrency.operations; import concurrency.receiver; import unit_threaded; -@("sync_wait.fork") -@trusted unittest { - ForkSender(getLocalThreadExecutor(), () shared {}).syncWait.isOk.shouldEqual(true); +@("sync_wait.fork") @trusted +unittest { + ForkSender(getLocalThreadExecutor(), () shared {}).syncWait.isOk + .shouldEqual(true); } -@("sync_wait.fork.exception") -@trusted unittest { - import core.stdc.stdlib; - ForkSender(getLocalThreadExecutor(), () shared @trusted { exit(1); }).syncWait.assumeOk.shouldThrow(); +@("sync_wait.fork.exception") @trusted +unittest { + import core.stdc.stdlib; + ForkSender(getLocalThreadExecutor(), () shared @trusted { + exit(1); + }).syncWait.assumeOk.shouldThrow(); } diff --git a/tests/ut/concurrency/mpsc.d b/tests/ut/concurrency/mpsc.d index 6b456e4..153c70a 100644 --- a/tests/ut/concurrency/mpsc.d +++ b/tests/ut/concurrency/mpsc.d @@ -5,65 +5,71 @@ import concurrency.data.queue.mpsc; import concurrency : syncWait; struct Node { - int payload; - shared Node* next; + int payload; + shared Node* next; } auto intProducer(Q)(Q q, int num) { - import concurrency.sender : just; - import concurrency.thread; - import concurrency.operations; + import concurrency.sender : just; + import concurrency.thread; + import concurrency.operations; - auto producer = q.producer(); - return just(producer, num).then((shared MPSCQueueProducer!Node producer, int num) shared { - foreach(i; 0..num) - producer.push(new Node(i+1)); - }).via(ThreadSender()); + auto producer = q.producer(); + return just(producer, num) + .then((shared MPSCQueueProducer!Node producer, int num) shared { + foreach (i; 0 .. num) + producer.push(new Node(i + 1)); + }).via(ThreadSender()); } auto intSummer(Q)(Q q) { - import concurrency.operations : withStopToken, via; - import concurrency.thread; - import concurrency.sender : justFrom, just; - import concurrency.stoptoken : StopToken; - import core.time : msecs; + import concurrency.operations : withStopToken, via; + import concurrency.thread; + import concurrency.sender : justFrom, just; + import concurrency.stoptoken : StopToken; + import core.time : msecs; - return just(q).withStopToken((StopToken stopToken, Q q) shared @safe { - int sum = 0; - while (!stopToken.isStopRequested()) { - if (auto node = q.pop()) { - sum += node.payload; - } - } - while (true) { - if (auto node = q.pop()) - sum += node.payload; - else - break; - } - return sum; - }).via(ThreadSender()); + return just(q).withStopToken((StopToken stopToken, Q q) shared @safe { + int sum = 0; + while (!stopToken.isStopRequested()) { + if (auto node = q.pop()) { + sum += node.payload; + } + } + + while (true) { + if (auto node = q.pop()) + sum += node.payload; + else + break; + } + + return sum; + }).via(ThreadSender()); } -@("single") -@safe unittest { - import concurrency.operations : race, stopWhen; - import core.time : msecs; +@("single") @safe +unittest { + import concurrency.operations : race, stopWhen; + import core.time : msecs; - auto q = new MPSCQueue!Node(); - q.intSummer.stopWhen(intProducer(q, 50000)).syncWait.value.should == 1250025000; - q.empty.should == true; + auto q = new MPSCQueue!Node(); + q.intSummer.stopWhen(intProducer(q, 50000)).syncWait.value.should + == 1250025000; + q.empty.should == true; } -@("race") -@safe unittest { - import concurrency.operations : race, stopWhen, whenAll; +@("race") @safe +unittest { + import concurrency.operations : race, stopWhen, whenAll; - auto q = new MPSCQueue!Node(); - q.intSummer.stopWhen(whenAll(intProducer(q, 10000), - intProducer(q, 10000), - intProducer(q, 10000), - intProducer(q, 10000), - )).syncWait.value.should == 200020000; - q.empty.should == true; + auto q = new MPSCQueue!Node(); + q + .intSummer + .stopWhen(whenAll(intProducer(q, 10000), intProducer(q, 10000), + intProducer(q, 10000), intProducer(q, 10000), )) + .syncWait + .value + .should == 200020000; + q.empty.should == true; } diff --git a/tests/ut/concurrency/nursery.d b/tests/ut/concurrency/nursery.d index b041bdd..f681474 100644 --- a/tests/ut/concurrency/nursery.d +++ b/tests/ut/concurrency/nursery.d @@ -8,168 +8,174 @@ import concurrency.nursery; import concurrency.stoptoken; import unit_threaded; -@("run.stopped") -@safe unittest { - auto nursery = new shared Nursery(); - nursery.stop(); - nursery.syncWait().isCancelled.should == true; +@("run.stopped") @safe +unittest { + auto nursery = new shared Nursery(); + nursery.stop(); + nursery.syncWait().isCancelled.should == true; } -@("run.empty") -@safe unittest { - auto nursery = new shared Nursery(); - auto stop = justFrom(() shared => nursery.stop()); - whenAll(nursery, stop).syncWait().isCancelled.should == true; +@("run.empty") @safe +unittest { + auto nursery = new shared Nursery(); + auto stop = justFrom(() shared => nursery.stop()); + whenAll(nursery, stop).syncWait().isCancelled.should == true; } -@("run.value") -@safe unittest { - auto nursery = new shared Nursery(); - nursery.run(ValueSender!(int)(5)); - nursery.syncWait.assumeOk; - nursery.getStopToken().isStopRequested().shouldBeFalse(); +@("run.value") @safe +unittest { + auto nursery = new shared Nursery(); + nursery.run(ValueSender!(int)(5)); + nursery.syncWait.assumeOk; + nursery.getStopToken().isStopRequested().shouldBeFalse(); } -@("run.exception") -@safe unittest { - auto nursery = new shared Nursery(); - nursery.run(ThrowingSender()); - nursery.syncWait.assumeOk.shouldThrow(); - nursery.getStopToken().isStopRequested().shouldBeTrue(); +@("run.exception") @safe +unittest { + auto nursery = new shared Nursery(); + nursery.run(ThrowingSender()); + nursery.syncWait.assumeOk.shouldThrow(); + nursery.getStopToken().isStopRequested().shouldBeTrue(); } -@("run.value.then") -@safe unittest { - auto nursery = new shared Nursery(); - shared(int) global; - nursery.run(ValueSender!(int)(5).then((int c) shared => global = c)); - global.shouldEqual(0); - nursery.syncWait.assumeOk; - global.shouldEqual(5); +@("run.value.then") @safe +unittest { + auto nursery = new shared Nursery(); + shared(int) global; + nursery.run(ValueSender!(int)(5).then((int c) shared => global = c)); + global.shouldEqual(0); + nursery.syncWait.assumeOk; + global.shouldEqual(5); } -@("run.thread.run") -@safe unittest { - auto nursery = new shared Nursery(); - shared(int) global; - nursery.run(ThreadSender().then(() shared @safe { - nursery.run(ValueSender!(int)(5).then((int c) shared @safe { - global = c; - })); - })); - global.shouldEqual(0); - nursery.syncWait.assumeOk; - global.shouldEqual(5); - nursery.getStopToken().isStopRequested().shouldBeFalse(); +@("run.thread.run") @safe +unittest { + auto nursery = new shared Nursery(); + shared(int) global; + nursery.run(ThreadSender().then(() shared @safe { + nursery.run(ValueSender!(int)(5).then((int c) shared @safe { + global = c; + })); + })); + global.shouldEqual(0); + nursery.syncWait.assumeOk; + global.shouldEqual(5); + nursery.getStopToken().isStopRequested().shouldBeFalse(); } -@("run.thread.stop.internal") -@safe unittest { - auto nursery = new shared Nursery(); - nursery.run(ThreadSender().then(() shared @safe => nursery.stop())); - nursery.syncWait.isCancelled.should == true; - nursery.getStopToken().isStopRequested().shouldBeTrue(); +@("run.thread.stop.internal") @safe +unittest { + auto nursery = new shared Nursery(); + nursery.run(ThreadSender().then(() shared @safe => nursery.stop())); + nursery.syncWait.isCancelled.should == true; + nursery.getStopToken().isStopRequested().shouldBeTrue(); } -@("run.thread.stop.external") -@trusted unittest { - auto nursery = new shared Nursery(); - auto stopSource = new shared StopSource(); - nursery.run(ThreadSender().then(() shared @safe => stopSource.stop())); - nursery.syncWait(cast(StopSource)stopSource).isCancelled.should == true; - nursery.getStopToken().isStopRequested().shouldBeTrue(); - stopSource.isStopRequested().shouldBeTrue(); +@("run.thread.stop.external") @trusted +unittest { + auto nursery = new shared Nursery(); + auto stopSource = new shared StopSource(); + nursery.run(ThreadSender().then(() shared @safe => stopSource.stop())); + nursery.syncWait(cast(StopSource) stopSource).isCancelled.should == true; + nursery.getStopToken().isStopRequested().shouldBeTrue(); + stopSource.isStopRequested().shouldBeTrue(); } -@("run.thread.stop.internal.sibling") -@safe unittest { - import core.thread : Thread; - auto nursery = new shared Nursery(); - auto thread1 = ThreadSender().then(() shared @trusted { - auto token = nursery.getStopToken(); - while (!token.isStopRequested()) Thread.yield(); - }); - auto thread2 = ThreadSender().then(() shared @safe => nursery.stop()); - nursery.run(thread1); - nursery.run(thread2); - nursery.syncWait.isCancelled.should == true; - nursery.getStopToken().isStopRequested().shouldBeTrue(); +@("run.thread.stop.internal.sibling") @safe +unittest { + import core.thread : Thread; + auto nursery = new shared Nursery(); + auto thread1 = ThreadSender().then(() shared @trusted { + auto token = nursery.getStopToken(); + while (!token.isStopRequested()) + Thread.yield(); + }); + auto thread2 = ThreadSender().then(() shared @safe => nursery.stop()); + nursery.run(thread1); + nursery.run(thread2); + nursery.syncWait.isCancelled.should == true; + nursery.getStopToken().isStopRequested().shouldBeTrue(); } -@("run.nested") -@safe unittest { - auto nursery1 = new shared Nursery(); - auto nursery2 = new shared Nursery(); - shared(int) global; - nursery1.run(nursery2); - nursery2.run(ValueSender!(int)(99).then((int c) shared => global = c)); - global.shouldEqual(0); - nursery1.syncWait.assumeOk; - global.shouldEqual(99); - nursery1.getStopToken().isStopRequested().shouldBeFalse(); - nursery2.getStopToken().isStopRequested().shouldBeFalse(); +@("run.nested") @safe +unittest { + auto nursery1 = new shared Nursery(); + auto nursery2 = new shared Nursery(); + shared(int) global; + nursery1.run(nursery2); + nursery2.run(ValueSender!(int)(99).then((int c) shared => global = c)); + global.shouldEqual(0); + nursery1.syncWait.assumeOk; + global.shouldEqual(99); + nursery1.getStopToken().isStopRequested().shouldBeFalse(); + nursery2.getStopToken().isStopRequested().shouldBeFalse(); } -@("run.error") -@safe unittest { - import core.thread : Thread; - auto nursery = new shared Nursery(); - auto thread1 = ThreadSender().then(() shared @trusted { - auto token = nursery.getStopToken(); - while (!token.isStopRequested()) Thread.yield(); - }); - auto thread2 = ThreadSender().withStopToken((StopToken token) shared @trusted { - while (!token.isStopRequested()) Thread.yield(); - }); - auto thread3 = ThreadSender().then(() shared @safe { throw new Exception("Error should stop everyone"); }); - nursery.run(thread1); - nursery.run(thread2); - nursery.run(thread3); - nursery.getStopToken().isStopRequested().shouldBeFalse(); - nursery.syncWait.assumeOk.shouldThrow(); - nursery.getStopToken().isStopRequested().shouldBeTrue(); +@("run.error") @safe +unittest { + import core.thread : Thread; + auto nursery = new shared Nursery(); + auto thread1 = ThreadSender().then(() shared @trusted { + auto token = nursery.getStopToken(); + while (!token.isStopRequested()) + Thread.yield(); + }); + auto thread2 = + ThreadSender().withStopToken((StopToken token) shared @trusted { + while (!token.isStopRequested()) + Thread.yield(); + }); + auto thread3 = ThreadSender().then(() shared @safe { + throw new Exception("Error should stop everyone"); + }); + nursery.run(thread1); + nursery.run(thread2); + nursery.run(thread3); + nursery.getStopToken().isStopRequested().shouldBeFalse(); + nursery.syncWait.assumeOk.shouldThrow(); + nursery.getStopToken().isStopRequested().shouldBeTrue(); } -@("withStopSource.1") -@safe unittest { - import core.thread : Thread; - auto stopSource = new StopSource(); - auto nursery = new shared Nursery(); +@("withStopSource.1") @safe +unittest { + import core.thread : Thread; + auto stopSource = new StopSource(); + auto nursery = new shared Nursery(); - auto thread1 = ThreadSender() - .withStopToken((StopToken stopToken) shared @trusted { - while(!stopToken.isStopRequested) - Thread.yield(); - }) - .withStopSource(stopSource); + auto thread1 = + ThreadSender().withStopToken((StopToken stopToken) shared @trusted { + while (!stopToken.isStopRequested) + Thread.yield(); + }).withStopSource(stopSource); - // stop via the source - auto stopper = ValueSender!StopSource(stopSource).then((StopSource stopSource) shared => stopSource.stop()); + // stop via the source + auto stopper = ValueSender!StopSource(stopSource) + .then((StopSource stopSource) shared => stopSource.stop()); - nursery.run(thread1); - nursery.run(stopper); + nursery.run(thread1); + nursery.run(stopper); - nursery.syncWait.assumeOk; + nursery.syncWait.assumeOk; } -@("withStopSource.2") -@safe unittest { - import core.thread : Thread; - auto stopSource = new StopSource(); - auto nursery = new shared Nursery(); +@("withStopSource.2") @safe +unittest { + import core.thread : Thread; + auto stopSource = new StopSource(); + auto nursery = new shared Nursery(); - auto thread1 = ThreadSender() - .withStopToken((StopToken stopToken) shared @trusted { - while(!stopToken.isStopRequested) - Thread.yield(); - }) - .withStopSource(stopSource); + auto thread1 = + ThreadSender().withStopToken((StopToken stopToken) shared @trusted { + while (!stopToken.isStopRequested) + Thread.yield(); + }).withStopSource(stopSource); - // stop via the nursery - auto stopper = ValueSender!(shared Nursery)(nursery).then((shared Nursery nursery) shared => nursery.stop()); + // stop via the nursery + auto stopper = ValueSender!(shared Nursery)(nursery) + .then((shared Nursery nursery) shared => nursery.stop()); - nursery.run(thread1); - nursery.run(stopper); + nursery.run(thread1); + nursery.run(stopper); - nursery.syncWait.isCancelled.should == true; + nursery.syncWait.isCancelled.should == true; } diff --git a/tests/ut/concurrency/operations.d b/tests/ut/concurrency/operations.d index 5535466..43d33cc 100644 --- a/tests/ut/concurrency/operations.d +++ b/tests/ut/concurrency/operations.d @@ -14,704 +14,825 @@ import std.typecons; /// Used to test that Senders keep the operational state alive until one receiver's terminal is called struct OutOfBandValueSender(T) { - alias Value = T; - T value; - struct Op(Receiver) { - Receiver receiver; - T value; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void run() { - receiver.setValue(value); - } - void start() @trusted scope { - auto value = new Thread(&this.run).start(); - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(receiver, value); - return op; - } -} - -@("ignoreErrors.syncWait.value") -@safe unittest { - bool delegate() @safe shared dg = () shared { throw new Exception("Exceptions are rethrown"); }; - ThreadSender() - .then(dg) - .ignoreError() - .syncWait.isCancelled.should == true; -} - -@("oob") -@safe unittest { - auto oob = OutOfBandValueSender!int(43); - oob.syncWait.value.should == 43; -} - -@("race") -@safe unittest { - race(ValueSender!int(4), ValueSender!int(5)).syncWait.value.should == 4; - auto fastThread = ThreadSender().then(() shared => 1); - auto slowThread = ThreadSender().then(() shared @trusted { Thread.sleep(50.msecs); return 2; }); - race(fastThread, slowThread).syncWait.value.should == 1; - race(slowThread, fastThread).syncWait.value.should == 1; -} - -@("race.multiple") -@safe unittest { - race(ValueSender!int(4), ValueSender!int(5), ValueSender!int(6)).syncWait.value.should == 4; -} - -@("race.exception.single") -@safe unittest { - race(ThrowingSender(), ValueSender!int(5)).syncWait.value.should == 5; - race(ThrowingSender(), ThrowingSender()).syncWait.assumeOk.shouldThrow(); -} - -@("race.exception.double") -@safe unittest { - auto slow = ThreadSender().then(() shared @trusted { Thread.sleep(50.msecs); throw new Exception("Slow"); }); - auto fast = ThreadSender().then(() shared { throw new Exception("Fast"); }); - race(slow, fast).syncWait.assumeOk.shouldThrowWithMessage("Fast"); -} - -@("race.cancel-other") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - }); - race(waiting, ValueSender!int(88)).syncWait.value.get.should == 88; -} - -@("race.cancel") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - }); - auto nursery = new shared Nursery(); - nursery.run(race(waiting, waiting)); - nursery.run(ThreadSender().then(() @trusted shared { Thread.sleep(50.msecs); nursery.stop(); })); - nursery.syncWait.isCancelled.should == true; -} - -@("race.array.just") -@safe unittest { - race([just(4), just(5)]).syncWait.value.should == 4; -} - -@("race.array.void") -@safe unittest { - race([VoidSender(), VoidSender()]).syncWait.assumeOk; -} - -@("via") -@safe unittest { - import std.typecons : tuple; - ValueSender!int(3).via(ValueSender!int(6)).syncWait.value.should == tuple(6,3); - ValueSender!int(5).via(VoidSender()).syncWait.value.should == 5; - VoidSender().via(ValueSender!int(4)).syncWait.value.should == 4; -} - -@("then.value.delegate") -@safe unittest { - ValueSender!int(3).then((int i) shared => i*3).syncWait.value.shouldEqual(9); -} - -@("then.value.function") -@safe unittest { - ValueSender!int(3).then((int i) => i*3).syncWait.value.shouldEqual(9); -} - -@("then.oob") -@safe unittest { - OutOfBandValueSender!int(46).then((int i) shared => i*3).syncWait.value.shouldEqual(138); -} - -@("then.tuple") -@safe unittest { - just(1,2,3).then((Tuple!(int,int,int) t) shared => t[0]).syncWait.value.shouldEqual(1); -} - -@("then.tuple.expand") -@safe unittest { - just(1,2,3).then((int a,int b,int c) shared => a+b).syncWait.value.shouldEqual(3); -} - -@("whenAll.basic") -@safe unittest { - whenAll(ValueSender!int(1), ValueSender!int(2)).syncWait.value.should == tuple(1,2); - whenAll(ValueSender!int(1), ValueSender!int(2), ValueSender!int(3)).syncWait.value.should == tuple(1,2,3); - whenAll(VoidSender(), ValueSender!int(2)).syncWait.value.should == 2; - whenAll(ValueSender!int(1), VoidSender()).syncWait.value.should == 1; - whenAll(VoidSender(), VoidSender()).syncWait.assumeOk; - whenAll(ValueSender!int(1), ThrowingSender()).syncWait.assumeOk.shouldThrowWithMessage("ThrowingSender"); - whenAll(ThrowingSender(), ValueSender!int(1)).syncWait.assumeOk.shouldThrowWithMessage("ThrowingSender"); - whenAll(ValueSender!int(1), DoneSender()).syncWait.isCancelled.should == true; - whenAll(DoneSender(), ValueSender!int(1)).syncWait.isCancelled.should == true; - whenAll(DoneSender(), ThrowingSender()).syncWait.isCancelled.should == true; - whenAll(ThrowingSender(), DoneSender()).syncWait.assumeOk.shouldThrowWithMessage("ThrowingSender"); - -} - -@("whenAll.cancel") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - }); - whenAll(waiting, DoneSender()).syncWait.isCancelled.should == true; - whenAll(ThrowingSender(), waiting).syncWait.assumeOk.shouldThrow; - whenAll(waiting, ThrowingSender()).syncWait.assumeOk.shouldThrow; - auto waitingInt = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - return 42; - }); - whenAll(waitingInt, DoneSender()).syncWait.isCancelled.should == true; - whenAll(ThrowingSender(), waitingInt).syncWait.assumeOk.shouldThrow; - whenAll(waitingInt, ThrowingSender()).syncWait.assumeOk.shouldThrow; -} - -@("whenAll.stop") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - }); - auto source = new StopSource(); - auto stopper = just(source).then((StopSource source) shared => source.stop()); - whenAll(waiting, stopper).withStopSource(source).syncWait.isCancelled.should == true; -} - -@("whenAll.array.just") -@safe unittest { - whenAll([just(4), just(5)]).syncWait.value.should == [4,5]; -} - -@("whenAll.array.void") -@safe unittest { - whenAll([VoidSender(), VoidSender()]).syncWait.assumeOk; -} - -@("retry") -@safe unittest { - ValueSender!int(5).retry(Times(5)).syncWait.value.should == 5; - int t = 3; - int n = 0; - struct Sender { - alias Value = void; - static struct Op(Receiver) { - Receiver receiver; - bool fail; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() @safe nothrow { - if (fail) - receiver.setError(new Exception("Fail fail fail")); - else - receiver.setValue(); - } - } - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!(Receiver)(receiver, n++ < t); - return op; - } - } - Sender().retry(Times(5)).syncWait.assumeOk; - n.should == 4; - n = 0; - - Sender().retry(Times(2)).syncWait.assumeOk.shouldThrowWithMessage("Fail fail fail"); - n.should == 2; - shared int p = 0; - ThreadSender().then(()shared { import core.atomic; p.atomicOp!("+=")(1); throw new Exception("Failed"); }).retry(Times(5)).syncWait.assumeOk.shouldThrowWithMessage("Failed"); - p.should == 5; -} - -@("retryWhen.immediate.success") -@safe unittest { - static struct Immediate { - auto failure(Exception e) { - return VoidSender(); - } - } - - VoidSender().retryWhen(Immediate()).syncWait.assumeOk; + alias Value = T; + T value; + struct Op(Receiver) { + Receiver receiver; + T value; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void run() { + receiver.setValue(value); + } + + void start() @trusted scope { + auto value = new Thread(&this.run).start(); + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(receiver, value); + return op; + } +} + +@("ignoreErrors.syncWait.value") @safe +unittest { + bool delegate() @safe shared dg = () shared { + throw new Exception("Exceptions are rethrown"); + }; + ThreadSender().then(dg).ignoreError().syncWait.isCancelled.should == true; +} + +@("oob") @safe +unittest { + auto oob = OutOfBandValueSender!int(43); + oob.syncWait.value.should == 43; +} + +@("race") @safe +unittest { + race(ValueSender!int(4), ValueSender!int(5)).syncWait.value.should == 4; + auto fastThread = ThreadSender().then(() shared => 1); + auto slowThread = ThreadSender().then(() shared @trusted { + Thread.sleep(50.msecs); + return 2; + }); + race(fastThread, slowThread).syncWait.value.should == 1; + race(slowThread, fastThread).syncWait.value.should == 1; +} + +@("race.multiple") @safe +unittest { + race(ValueSender!int(4), ValueSender!int(5), ValueSender!int(6)) + .syncWait.value.should == 4; +} + +@("race.exception.single") @safe +unittest { + race(ThrowingSender(), ValueSender!int(5)).syncWait.value.should == 5; + race(ThrowingSender(), ThrowingSender()).syncWait.assumeOk.shouldThrow(); +} + +@("race.exception.double") @safe +unittest { + auto slow = ThreadSender().then(() shared @trusted { + Thread.sleep(50.msecs); + throw new Exception("Slow"); + }); + auto fast = ThreadSender().then(() shared { + throw new Exception("Fast"); + }); + race(slow, fast).syncWait.assumeOk.shouldThrowWithMessage("Fast"); +} + +@("race.cancel-other") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + }); + race(waiting, ValueSender!int(88)).syncWait.value.get.should == 88; +} + +@("race.cancel") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + }); + auto nursery = new shared Nursery(); + nursery.run(race(waiting, waiting)); + nursery.run(ThreadSender().then(() @trusted shared { + Thread.sleep(50.msecs); + nursery.stop(); + })); + nursery.syncWait.isCancelled.should == true; +} + +@("race.array.just") @safe +unittest { + race([just(4), just(5)]).syncWait.value.should == 4; +} + +@("race.array.void") @safe +unittest { + race([VoidSender(), VoidSender()]).syncWait.assumeOk; +} + +@("via") @safe +unittest { + import std.typecons : tuple; + ValueSender!int(3).via(ValueSender!int(6)).syncWait.value.should + == tuple(6, 3); + ValueSender!int(5).via(VoidSender()).syncWait.value.should == 5; + VoidSender().via(ValueSender!int(4)).syncWait.value.should == 4; +} + +@("then.value.delegate") @safe +unittest { + ValueSender!int(3).then((int i) shared => i * 3).syncWait.value + .shouldEqual(9); +} + +@("then.value.function") @safe +unittest { + ValueSender!int(3).then((int i) => i * 3).syncWait.value.shouldEqual(9); +} + +@("then.oob") @safe +unittest { + OutOfBandValueSender!int(46).then((int i) shared => i * 3).syncWait.value + .shouldEqual(138); +} + +@("then.tuple") @safe +unittest { + just(1, 2, 3).then((Tuple!(int, int, int) t) shared => t[0]).syncWait.value + .shouldEqual(1); +} + +@("then.tuple.expand") @safe +unittest { + just(1, 2, 3).then((int a, int b, int c) shared => a + b).syncWait.value + .shouldEqual(3); +} + +@("whenAll.basic") @safe +unittest { + whenAll(ValueSender!int(1), ValueSender!int(2)).syncWait.value.should + == tuple(1, 2); + whenAll(ValueSender!int(1), ValueSender!int(2), ValueSender!int(3)) + .syncWait.value.should == tuple(1, 2, 3); + whenAll(VoidSender(), ValueSender!int(2)).syncWait.value.should == 2; + whenAll(ValueSender!int(1), VoidSender()).syncWait.value.should == 1; + whenAll(VoidSender(), VoidSender()).syncWait.assumeOk; + whenAll(ValueSender!int(1), ThrowingSender()) + .syncWait.assumeOk.shouldThrowWithMessage("ThrowingSender"); + whenAll(ThrowingSender(), ValueSender!int(1)) + .syncWait.assumeOk.shouldThrowWithMessage("ThrowingSender"); + whenAll(ValueSender!int(1), DoneSender()).syncWait.isCancelled.should + == true; + whenAll(DoneSender(), ValueSender!int(1)).syncWait.isCancelled.should + == true; + whenAll(DoneSender(), ThrowingSender()).syncWait.isCancelled.should == true; + whenAll(ThrowingSender(), DoneSender()) + .syncWait.assumeOk.shouldThrowWithMessage("ThrowingSender"); +} + +@("whenAll.cancel") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + }); + whenAll(waiting, DoneSender()).syncWait.isCancelled.should == true; + whenAll(ThrowingSender(), waiting).syncWait.assumeOk.shouldThrow; + whenAll(waiting, ThrowingSender()).syncWait.assumeOk.shouldThrow; + auto waitingInt = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + + return 42; + }); + whenAll(waitingInt, DoneSender()).syncWait.isCancelled.should == true; + whenAll(ThrowingSender(), waitingInt).syncWait.assumeOk.shouldThrow; + whenAll(waitingInt, ThrowingSender()).syncWait.assumeOk.shouldThrow; +} + +@("whenAll.stop") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + }); + auto source = new StopSource(); + auto stopper = + just(source).then((StopSource source) shared => source.stop()); + whenAll(waiting, stopper).withStopSource(source).syncWait.isCancelled.should + == true; +} + +@("whenAll.array.just") @safe +unittest { + whenAll([just(4), just(5)]).syncWait.value.should == [4, 5]; +} + +@("whenAll.array.void") @safe +unittest { + whenAll([VoidSender(), VoidSender()]).syncWait.assumeOk; +} + +@("retry") @safe +unittest { + ValueSender!int(5).retry(Times(5)).syncWait.value.should == 5; + int t = 3; + int n = 0; + struct Sender { + alias Value = void; + static struct Op(Receiver) { + Receiver receiver; + bool fail; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() @safe nothrow { + if (fail) + receiver.setError(new Exception("Fail fail fail")); + else + receiver.setValue(); + } + } + + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!(Receiver)(receiver, n++ < t); + return op; + } + } + + Sender().retry(Times(5)).syncWait.assumeOk; + n.should == 4; + n = 0; + + Sender().retry(Times(2)).syncWait.assumeOk + .shouldThrowWithMessage("Fail fail fail"); + n.should == 2; + shared int p = 0; + ThreadSender().then(() shared { + import core.atomic; + p.atomicOp!("+=")(1); + throw new Exception("Failed"); + }).retry(Times(5)).syncWait.assumeOk.shouldThrowWithMessage("Failed"); + p.should == 5; +} + +@("retryWhen.immediate.success") @safe +unittest { + static struct Immediate { + auto failure(Exception e) { + return VoidSender(); + } + } + + VoidSender().retryWhen(Immediate()).syncWait.assumeOk; } struct ConnectCounter { - alias Value = int; - int counter = 0; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = ValueSender!int(counter++).connect(receiver); - return op; - } -} - -@("retryWhen.immediate.retries") -@safe unittest { - static struct Immediate { - auto failure(Exception e) { - return VoidSender(); - } - } - ConnectCounter() - .then((int c) { if (c < 3) throw new Exception("jada"); return c; }) - .retryWhen(Immediate()) - .syncWait.value.should == 3; -} - -@("retryWhen.wait.retries") -@safe unittest { - import core.time : msecs; - import concurrency.scheduler : ManualTimeWorker; + alias Value = int; + int counter = 0; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = ValueSender!int(counter++).connect(receiver); + return op; + } +} + +@("retryWhen.immediate.retries") @safe +unittest { + static struct Immediate { + auto failure(Exception e) { + return VoidSender(); + } + } + + ConnectCounter().then((int c) { + if (c < 3) + throw new Exception("jada"); + return c; + }).retryWhen(Immediate()).syncWait.value.should == 3; +} + +@("retryWhen.wait.retries") @safe +unittest { + import core.time : msecs; + import concurrency.scheduler : ManualTimeWorker; + + static struct Wait { + auto failure(Exception e) @safe { + return delay(3.msecs); + } + } + + auto worker = new shared ManualTimeWorker(); + auto sender = ConnectCounter().then((int c) { + if (c < 3) + throw new Exception("jada"); + return c; + }).retryWhen(Wait()).withScheduler(worker.getScheduler); + + auto driver = just(worker).then((shared ManualTimeWorker worker) { + worker.timeUntilNextEvent().should == 3.msecs; + worker.advance(3.msecs); + worker.timeUntilNextEvent().should == 3.msecs; + worker.advance(3.msecs); + worker.timeUntilNextEvent().should == 3.msecs; + worker.advance(3.msecs); + worker.timeUntilNextEvent().should == null; + }); + + whenAll(sender, driver).syncWait.value.should == 3; +} + +@("retryWhen.throw") @safe +unittest { + static struct Throw { + auto failure(Exception t) @safe { + return ErrorSender(new Exception("inner")); + } + } + + ErrorSender(new Exception("outer")).retryWhen(Throw()).syncWait.assumeOk + .shouldThrowWithMessage("inner"); +} + +@("whenAll.oob") @safe +unittest { + auto oob = OutOfBandValueSender!int(43); + auto value = ValueSender!int(11); + whenAll(oob, value).syncWait.value.should == tuple(43, 11); +} + +@("withStopToken.oob") @safe +unittest { + auto oob = OutOfBandValueSender!int(44); + oob.withStopToken((StopToken stopToken, int t) => t).syncWait.value.should + == 44; +} + +@("withStopSource.oob") @safe +unittest { + auto oob = OutOfBandValueSender!int(45); + oob.withStopSource(new StopSource()).syncWait.value.should == 45; +} + +@("withStopSource.tuple") @safe +unittest { + just(14, 53).withStopToken((StopToken s, Tuple!(int, int) t) => t[0] * t[1]) + .syncWait.value.should == 742; +} - static struct Wait { - auto failure(Exception e) @safe { - return delay(3.msecs); - } - } - - auto worker = new shared ManualTimeWorker(); - auto sender = ConnectCounter() - .then((int c) { if (c < 3) throw new Exception("jada"); return c; }) - .retryWhen(Wait()) - .withScheduler(worker.getScheduler); - - auto driver = just(worker).then((shared ManualTimeWorker worker) { - worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - worker.timeUntilNextEvent().should == null; - }); - - whenAll(sender, driver).syncWait.value.should == 3; -} - -@("retryWhen.throw") -@safe unittest { - static struct Throw { - auto failure(Exception t) @safe { - return ErrorSender(new Exception("inner")); - } - } - - ErrorSender(new Exception("outer")).retryWhen(Throw()).syncWait.assumeOk.shouldThrowWithMessage("inner"); +@("value.withstoptoken.via.thread") @safe +unittest { + ValueSender!int(4).withStopToken((StopToken s, int i) { + throw new Exception("Badness"); + }).via(ThreadSender()).syncWait.assumeOk.shouldThrowWithMessage("Badness"); } -@("whenAll.oob") -@safe unittest { - auto oob = OutOfBandValueSender!int(43); - auto value = ValueSender!int(11); - whenAll(oob, value).syncWait.value.should == tuple(43, 11); +@("completewithcancellation") @safe +unittest { + ValueSender!void().completeWithCancellation.syncWait.isCancelled.should + == true; } -@("withStopToken.oob") -@safe unittest { - auto oob = OutOfBandValueSender!int(44); - oob.withStopToken((StopToken stopToken, int t) => t).syncWait.value.should == 44; +@("raceAll") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + }); + raceAll(waiting, DoneSender()).syncWait.isCancelled.should == true; + raceAll(waiting, just(42)).syncWait.value.should == 42; + raceAll(waiting, ThrowingSender()).syncWait.isError.should == true; } -@("withStopSource.oob") -@safe unittest { - auto oob = OutOfBandValueSender!int(45); - oob.withStopSource(new StopSource()).syncWait.value.should == 45; -} +@("on.ManualTimeWorker") @safe +unittest { + import concurrency.scheduler : ManualTimeWorker; -@("withStopSource.tuple") -@safe unittest { - just(14, 53).withStopToken((StopToken s, Tuple!(int, int) t) => t[0]*t[1]).syncWait.value.should == 742; -} - -@("value.withstoptoken.via.thread") -@safe unittest { - ValueSender!int(4).withStopToken((StopToken s, int i) { throw new Exception("Badness");}).via(ThreadSender()).syncWait.assumeOk.shouldThrowWithMessage("Badness"); -} - -@("completewithcancellation") -@safe unittest { - ValueSender!void().completeWithCancellation.syncWait.isCancelled.should == true; -} + auto worker = new shared ManualTimeWorker(); + auto driver = just(worker).then((shared ManualTimeWorker worker) shared { + worker.timeUntilNextEvent().should == 10.msecs; + worker.advance(5.msecs); + worker.timeUntilNextEvent().should == 5.msecs; + worker.advance(5.msecs); + worker.timeUntilNextEvent().should == null; + }); + auto timer = DelaySender(10.msecs).withScheduler(worker.getScheduler); -@("raceAll") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - }); - raceAll(waiting, DoneSender()).syncWait.isCancelled.should == true; - raceAll(waiting, just(42)).syncWait.value.should == 42; - raceAll(waiting, ThrowingSender()).syncWait.isError.should == true; + whenAll(timer, driver).syncWait().assumeOk; } -@("on.ManualTimeWorker") -@safe unittest { - import concurrency.scheduler : ManualTimeWorker; - - auto worker = new shared ManualTimeWorker(); - auto driver = just(worker).then((shared ManualTimeWorker worker) shared { - worker.timeUntilNextEvent().should == 10.msecs; - worker.advance(5.msecs); - worker.timeUntilNextEvent().should == 5.msecs; - worker.advance(5.msecs); - worker.timeUntilNextEvent().should == null; - }); - auto timer = DelaySender(10.msecs).withScheduler(worker.getScheduler); +@("on.ManualTimeWorker.cancel") @safe +unittest { + import concurrency.scheduler : ManualTimeWorker; - whenAll(timer, driver).syncWait().assumeOk; -} + auto worker = new shared ManualTimeWorker(); + auto source = new StopSource(); + auto driver = just(source).then((StopSource source) shared { + worker.timeUntilNextEvent().should == 10.msecs; + source.stop(); + worker.timeUntilNextEvent().should == null; + }); + auto timer = DelaySender(10.msecs).withScheduler(worker.getScheduler); -@("on.ManualTimeWorker.cancel") -@safe unittest { - import concurrency.scheduler : ManualTimeWorker; - - auto worker = new shared ManualTimeWorker(); - auto source = new StopSource(); - auto driver = just(source).then((StopSource source) shared { - worker.timeUntilNextEvent().should == 10.msecs; - source.stop(); - worker.timeUntilNextEvent().should == null; - }); - auto timer = DelaySender(10.msecs).withScheduler(worker.getScheduler); - - whenAll(timer, driver).syncWait(source).isCancelled.should == true; + whenAll(timer, driver).syncWait(source).isCancelled.should == true; } -@("then.stack.no-leak") -@safe unittest { - struct S { - void fun(int i) shared { - } - } - shared S s; - // its perfectly ok to point to a function on the stack - auto sender = just(42).then(&s.fun); - - sender.syncWait(); +@("then.stack.no-leak") @safe +unittest { + struct S { + void fun(int i) shared {} + } - void disappearSender(Sender)(Sender s) @safe; - // but the sender can't leak now - static assert(!__traits(compiles, disappearSender(sender))); -} + shared S s; + // its perfectly ok to point to a function on the stack + auto sender = just(42).then(&s.fun); -@("forwardOn") -@safe unittest { - auto pool = stdTaskPool(2); + sender.syncWait(); - VoidSender().forwardOn(pool.getScheduler).syncWait.assumeOk; - ErrorSender(new Exception("bad news")).forwardOn(pool.getScheduler).syncWait.isError.should == true; -DoneSender().forwardOn(pool.getScheduler).syncWait.isCancelled.should == true; - just(42).forwardOn(pool.getScheduler).syncWait.value.should == 42; + void disappearSender(Sender)(Sender s) @safe; + // but the sender can't leak now + static assert(!__traits(compiles, disappearSender(sender))); } -@("toSingleton") -@safe unittest { - import std.typecons : tuple; - import concurrency.scheduler : ManualTimeWorker; - import core.atomic : atomicOp; - - shared int g; - - auto worker = new shared ManualTimeWorker(); - - auto single = delay(2.msecs).then(() shared => g.atomicOp!"+="(1)).toSingleton(worker.getScheduler); - - auto driver = justFrom(() shared => worker.advance(2.msecs)); - - whenAll(single, single, driver).syncWait.value.should == tuple(1,1); - whenAll(single, single, driver).syncWait.value.should == tuple(2,2); -} - -@("stopOn") -@safe unittest { - auto sourceInner = new shared StopSource(); - auto sourceOuter = new shared StopSource(); - - shared bool b; - whenAll(delay(5.msecs).then(() shared => b = true).stopOn(StopToken(sourceInner)), - just(() => sourceOuter.stop()) - ).syncWait(sourceOuter).assumeOk; - b.should == true; - - shared bool d; - whenAll(delay(5.msecs).then(() shared => b = true).stopOn(StopToken(sourceInner)), - just(() => sourceInner.stop()) - ).syncWait(sourceOuter).assumeOk; - d.should == false; -} - -@("withChild") -@safe unittest { - import core.atomic; - - class State { - import core.sync.event : Event; - bool parentAfterChild; - Event childEvent, parentEvent; - this() shared @trusted { - (cast()childEvent).initialize(false, false); - (cast()parentEvent).initialize(false, false); - } - void signalChild() shared @trusted { - (cast()childEvent).set(); - } - void waitChild() shared @trusted { - (cast()childEvent).wait(); - } - void signalParent() shared @trusted { - (cast()parentEvent).set(); - } - void waitParent() shared @trusted { - (cast()parentEvent).wait(); - } - } - auto state = new shared State(); - auto source = new shared StopSource; - - import std.stdio; - auto child = just(state).withStopToken((StopToken token, shared State state) @trusted { - while(!token.isStopRequested) {} - state.signalParent(); - state.waitChild(); - }).via(ThreadSender()); - - auto parent = just(state).withStopToken((StopToken token, shared State state){ - state.waitParent(); - state.parentAfterChild.atomicStore(token.isStopRequested == false); - state.signalChild(); - }).via(ThreadSender()); - - whenAll(parent.withChild(child).withStopSource(source), just(source).then((shared StopSource s) => s.stop())).syncWait.isCancelled.should == true; - - state.parentAfterChild.atomicLoad.should == true; -} - -@("onTermination.value") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - just(42).onTermination(() @safe shared => g.atomicOp!"+="(1)).syncWait.assumeOk; - g.should == 1; -} - -@("onTermination.done") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - DoneSender().onTermination(() @safe shared => g.atomicOp!"+="(1)).syncWait.isCancelled.should == true; - g.should == 1; -} - -@("onTermination.error") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - ThrowingSender().onTermination(() @safe shared => g.atomicOp!"+="(1)).syncWait.isError.should == true; - g.should == 1; -} - -@("onError.value") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - just(42).onError((Exception e) @safe shared => g.atomicOp!"+="(1)).syncWait.assumeOk; - g.should == 0; -} - -@("onError.done") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - DoneSender().onError((Exception e) @safe shared => g.atomicOp!"+="(1)).syncWait.isCancelled.should == true; - g.should == 0; -} - -@("onError.error") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - ThrowingSender().onError((Exception e) @safe shared => g.atomicOp!"+="(1)).syncWait.isError.should == true; - g.should == 1; -} - -@("onError.throw") -@safe unittest { - import core.exception : AssertError; - auto err = ThrowingSender().onError((Exception e) @safe shared { throw new Exception("in onError"); }).syncWait.get!Exception; - err.msg.should == "in onError"; - err.next.msg.should == "ThrowingSender"; -} - -@("stopWhen.source.value") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - return 43; - }); - auto trigger = delay(100.msecs); - waiting.stopWhen(trigger).syncWait().value.should == 43; -} - -@("stopWhen.source.error") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - throw new Exception("Upside down"); - }); - auto trigger = delay(100.msecs); - waiting.stopWhen(trigger).syncWait().assumeOk.shouldThrowWithMessage("Upside down"); -} - -@("stopWhen.source.cancelled") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - }).completeWithCancellation; - auto trigger = delay(100.msecs); - waiting.stopWhen(trigger).syncWait().isCancelled.should == true; +@("forwardOn") @safe +unittest { + auto pool = stdTaskPool(2); + + VoidSender().forwardOn(pool.getScheduler).syncWait.assumeOk; + ErrorSender(new Exception("bad news")).forwardOn(pool.getScheduler).syncWait + .isError.should == true; + DoneSender().forwardOn(pool.getScheduler).syncWait.isCancelled.should + == true; + just(42).forwardOn(pool.getScheduler).syncWait.value.should == 42; } -@("stopWhen.trigger.error") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - throw new Exception("This occurres later, so the other one gets propagated"); - }); - auto trigger = ThrowingSender(); - waiting.stopWhen(trigger).syncWait().assumeOk.shouldThrowWithMessage("ThrowingSender"); -} - -@("stopWhen.trigger.cancelled.value") -@safe unittest { - auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { - while (!token.isStopRequested) { Thread.yield(); } - return 42; - }); - auto trigger = delay(100.msecs).completeWithCancellation; - waiting.stopWhen(trigger).syncWait().isCancelled.should == true; -} +@("toSingleton") @safe +unittest { + import std.typecons : tuple; + import concurrency.scheduler : ManualTimeWorker; + import core.atomic : atomicOp; -@("completewitherror.basic") -@safe unittest { - ValueSender!void().completeWithError(new Exception("hello")).syncWait.assumeOk.shouldThrowWithMessage("hello"); -} - -@("completewitherror.exception.base") -@safe unittest { - ErrorSender(new Exception("not you")).completeWithError(new Exception("overridden")).syncWait.assumeOk.shouldThrowWithMessage!Throwable("overridden"); -} - -@("completewitherror.throwable.base") -@safe unittest { - ErrorSender(new Throwable("precedence")).completeWithError(new Exception("hello")).syncWait.assumeOk.shouldThrowWithMessage!Throwable("precedence"); -} - -@("completewitherror.error.base") -@safe unittest { - ErrorSender(new Error("precedence")).completeWithError(new Exception("hello")).syncWait.assumeOk.shouldThrowWithMessage!Error("precedence"); -} - -@("onCompletion.value") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - just(42).onCompletion(() @safe shared => g.atomicOp!"+="(1)).syncWait.assumeOk; - g.should == 1; -} - -@("onCompletion.done") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - DoneSender().onCompletion(() @safe shared => g.atomicOp!"+="(1)).syncWait.isCancelled.should == true; - g.should == 1; -} - -@("onCompletion.error") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - ThrowingSender().onCompletion(() @safe shared => g.atomicOp!"+="(1)).syncWait.isError.should == true; - g.should == 0; -} - -@("onResult.value") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - just(42).onResult((Result!int r) @safe shared => g.atomicOp!"+="(1)).syncWait.assumeOk; - just(42).tee((Result!int r) @safe shared => g.atomicOp!"+="(1)).syncWait.assumeOk; - g.should == 2; -} - -@("onResult.done") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - DoneSender().onResult((Result!void r) @safe shared => g.atomicOp!"+="(1)).syncWait.isCancelled.should == true; - DoneSender().tee((Result!void r) @safe shared => g.atomicOp!"+="(1)).syncWait.isCancelled.should == true; - g.should == 2; -} - -@("onResult.error") -@safe unittest { - import core.atomic : atomicOp; - shared int g = 0; - ThrowingSender().onResult((Result!void r) @safe shared => g.atomicOp!"+="(1)).syncWait.isError.should == true; - ThrowingSender().tee((Result!void r) @safe shared => g.atomicOp!"+="(1)).syncWait.isError.should == true; - g.should == 2; -} - -@("repeat.race") -@safe unittest { - import core.atomic : atomicOp; - import concurrency.scheduler : ManualTimeWorker; - shared int p = 0; - - auto worker = new shared ManualTimeWorker(); - - auto base = delay(1.msecs).then(() shared => cast(void)p.atomicOp!"+="(1)).repeat(); - - auto driver = just(worker).then((shared ManualTimeWorker worker) { - worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - worker.timeUntilNextEvent().should == 1.msecs; - }); - - race(base, driver).withScheduler(worker.getScheduler).syncWait().assumeOk; - p.should == 2; -} - -@("repeat.error") -@safe unittest { - static struct CountdownOp(Receiver) { - Receiver receiver; - bool fail; - @disable this(ref return scope typeof(this) rhs); - @disable this(this); - void start() @safe nothrow { - if (fail) - receiver.setError(new Exception("Bye!")); - else - receiver.setValueOrError(); - } - } - - static struct Countdown { - alias Value = void; - int countdown; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = CountdownOp!(Receiver)(receiver, countdown-- == 0); - return op; - } - } - - Countdown(3).syncWait().assumeOk(); - Countdown(3).repeat().syncWait().isError.should == true; + shared int g; + + auto worker = new shared ManualTimeWorker(); + + auto single = delay(2.msecs).then(() shared => g.atomicOp!"+="(1)) + .toSingleton(worker.getScheduler); + + auto driver = justFrom(() shared => worker.advance(2.msecs)); + + whenAll(single, single, driver).syncWait.value.should == tuple(1, 1); + whenAll(single, single, driver).syncWait.value.should == tuple(2, 2); +} + +@("stopOn") @safe +unittest { + auto sourceInner = new shared StopSource(); + auto sourceOuter = new shared StopSource(); + + shared bool b; + whenAll( + delay(5.msecs).then(() shared => b = true) + .stopOn(StopToken(sourceInner)), + just(() => sourceOuter.stop()) + ).syncWait(sourceOuter).assumeOk; + b.should == true; + + shared bool d; + whenAll( + delay(5.msecs).then(() shared => b = true) + .stopOn(StopToken(sourceInner)), + just(() => sourceInner.stop()) + ).syncWait(sourceOuter).assumeOk; + d.should == false; +} + +@("withChild") @safe +unittest { + import core.atomic; + + class State { + import core.sync.event : Event; + bool parentAfterChild; + Event childEvent, parentEvent; + this() shared @trusted { + (cast() childEvent).initialize(false, false); + (cast() parentEvent).initialize(false, false); + } + + void signalChild() shared @trusted { + (cast() childEvent).set(); + } + + void waitChild() shared @trusted { + (cast() childEvent).wait(); + } + + void signalParent() shared @trusted { + (cast() parentEvent).set(); + } + + void waitParent() shared @trusted { + (cast() parentEvent).wait(); + } + } + + auto state = new shared State(); + auto source = new shared StopSource; + + import std.stdio; + auto child = just(state) + .withStopToken((StopToken token, shared State state) @trusted { + while (!token.isStopRequested) {} + state.signalParent(); + state.waitChild(); + }).via(ThreadSender()); + + auto parent = + just(state).withStopToken((StopToken token, shared State state) { + state.waitParent(); + state.parentAfterChild.atomicStore(token.isStopRequested == false); + state.signalChild(); + }).via(ThreadSender()); + + whenAll( + parent.withChild(child).withStopSource(source), + just(source).then((shared StopSource s) => s.stop()) + ).syncWait.isCancelled.should == true; + + state.parentAfterChild.atomicLoad.should == true; +} + +@("onTermination.value") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + just(42).onTermination(() @safe shared => g.atomicOp!"+="(1)).syncWait + .assumeOk; + g.should == 1; +} + +@("onTermination.done") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + DoneSender().onTermination(() @safe shared => g.atomicOp!"+="(1)).syncWait + .isCancelled.should == true; + g.should == 1; +} + +@("onTermination.error") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + ThrowingSender().onTermination(() @safe shared => g.atomicOp!"+="(1)) + .syncWait.isError.should == true; + g.should == 1; +} + +@("onError.value") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + just(42).onError((Exception e) @safe shared => g.atomicOp!"+="(1)).syncWait + .assumeOk; + g.should == 0; +} + +@("onError.done") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + DoneSender().onError((Exception e) @safe shared => g.atomicOp!"+="(1)) + .syncWait.isCancelled.should == true; + g.should == 0; +} + +@("onError.error") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + ThrowingSender().onError((Exception e) @safe shared => g.atomicOp!"+="(1)) + .syncWait.isError.should == true; + g.should == 1; +} + +@("onError.throw") @safe +unittest { + import core.exception : AssertError; + auto err = ThrowingSender().onError((Exception e) @safe shared { + throw new Exception("in onError"); + }).syncWait.get!Exception; + err.msg.should == "in onError"; + err.next.msg.should == "ThrowingSender"; +} + +@("stopWhen.source.value") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + + return 43; + }); + auto trigger = delay(100.msecs); + waiting.stopWhen(trigger).syncWait().value.should == 43; +} + +@("stopWhen.source.error") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + + throw new Exception("Upside down"); + }); + auto trigger = delay(100.msecs); + waiting.stopWhen(trigger).syncWait().assumeOk + .shouldThrowWithMessage("Upside down"); +} + +@("stopWhen.source.cancelled") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + }).completeWithCancellation; + auto trigger = delay(100.msecs); + waiting.stopWhen(trigger).syncWait().isCancelled.should == true; +} + +@("stopWhen.trigger.error") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + + throw new Exception( + "This occurres later, so the other one gets propagated"); + }); + auto trigger = ThrowingSender(); + waiting.stopWhen(trigger).syncWait().assumeOk + .shouldThrowWithMessage("ThrowingSender"); +} + +@("stopWhen.trigger.cancelled.value") @safe +unittest { + auto waiting = ThreadSender().withStopToken((StopToken token) @trusted { + while (!token.isStopRequested) { + Thread.yield(); + } + + return 42; + }); + auto trigger = delay(100.msecs).completeWithCancellation; + waiting.stopWhen(trigger).syncWait().isCancelled.should == true; +} + +@("completewitherror.basic") @safe +unittest { + ValueSender!void().completeWithError(new Exception("hello")).syncWait + .assumeOk.shouldThrowWithMessage("hello"); +} + +@("completewitherror.exception.base") @safe +unittest { + ErrorSender(new Exception("not you")) + .completeWithError(new Exception("overridden")).syncWait.assumeOk + .shouldThrowWithMessage!Throwable("overridden"); +} + +@("completewitherror.throwable.base") @safe +unittest { + ErrorSender(new Throwable("precedence")) + .completeWithError(new Exception("hello")).syncWait.assumeOk + .shouldThrowWithMessage!Throwable("precedence"); +} + +@("completewitherror.error.base") @safe +unittest { + ErrorSender(new Error("precedence")) + .completeWithError(new Exception("hello")).syncWait.assumeOk + .shouldThrowWithMessage!Error("precedence"); +} + +@("onCompletion.value") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + just(42).onCompletion(() @safe shared => g.atomicOp!"+="(1)).syncWait + .assumeOk; + g.should == 1; +} + +@("onCompletion.done") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + DoneSender().onCompletion(() @safe shared => g.atomicOp!"+="(1)).syncWait + .isCancelled.should == true; + g.should == 1; +} + +@("onCompletion.error") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + ThrowingSender().onCompletion(() @safe shared => g.atomicOp!"+="(1)) + .syncWait.isError.should == true; + g.should == 0; +} + +@("onResult.value") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + just(42).onResult((Result!int r) @safe shared => g.atomicOp!"+="(1)) + .syncWait.assumeOk; + just(42).tee((Result!int r) @safe shared => g.atomicOp!"+="(1)).syncWait + .assumeOk; + g.should == 2; +} + +@("onResult.done") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + DoneSender().onResult((Result!void r) @safe shared => g.atomicOp!"+="(1)) + .syncWait.isCancelled.should == true; + DoneSender().tee((Result!void r) @safe shared => g.atomicOp!"+="(1)) + .syncWait.isCancelled.should == true; + g.should == 2; +} + +@("onResult.error") @safe +unittest { + import core.atomic : atomicOp; + shared int g = 0; + ThrowingSender() + .onResult((Result!void r) @safe shared => g.atomicOp!"+="(1)).syncWait + .isError.should == true; + ThrowingSender().tee((Result!void r) @safe shared => g.atomicOp!"+="(1)) + .syncWait.isError.should == true; + g.should == 2; +} + +@("repeat.race") @safe +unittest { + import core.atomic : atomicOp; + import concurrency.scheduler : ManualTimeWorker; + shared int p = 0; + + auto worker = new shared ManualTimeWorker(); + + auto base = delay(1.msecs).then(() shared => cast(void) p.atomicOp!"+="(1)) + .repeat(); + + auto driver = just(worker).then((shared ManualTimeWorker worker) { + worker.timeUntilNextEvent().should == 1.msecs; + worker.advance(1.msecs); + worker.timeUntilNextEvent().should == 1.msecs; + worker.advance(1.msecs); + worker.timeUntilNextEvent().should == 1.msecs; + }); + + race(base, driver).withScheduler(worker.getScheduler).syncWait().assumeOk; + p.should == 2; +} + +@("repeat.error") @safe +unittest { + static struct CountdownOp(Receiver) { + Receiver receiver; + bool fail; + @disable + this(ref return scope typeof(this) rhs); + @disable + this(this); + void start() @safe nothrow { + if (fail) + receiver.setError(new Exception("Bye!")); + else + receiver.setValueOrError(); + } + } + + static struct Countdown { + alias Value = void; + int countdown; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = CountdownOp!(Receiver)(receiver, countdown-- == 0); + return op; + } + } + + Countdown(3).syncWait().assumeOk(); + Countdown(3).repeat().syncWait().isError.should == true; } diff --git a/tests/ut/concurrency/pressure.d b/tests/ut/concurrency/pressure.d index 8c3422f..5b25fa6 100644 --- a/tests/ut/concurrency/pressure.d +++ b/tests/ut/concurrency/pressure.d @@ -5,9 +5,9 @@ import concurrency.thread; import concurrency.operations; import unit_threaded; -@("100.threads") -@safe unittest { - foreach(i; 0..100) { - ThreadSender().then(() shared => 2*3).syncWait().value.shouldEqual(6); - } +@("100.threads") @safe +unittest { + foreach (i; 0 .. 100) { + ThreadSender().then(() shared => 2 * 3).syncWait().value.shouldEqual(6); + } } diff --git a/tests/ut/concurrency/pressure2.d b/tests/ut/concurrency/pressure2.d index 58fa3b7..36e049a 100644 --- a/tests/ut/concurrency/pressure2.d +++ b/tests/ut/concurrency/pressure2.d @@ -5,9 +5,9 @@ import concurrency.thread; import concurrency.operations; import unit_threaded; -@("100.threads") -@safe unittest { - foreach(i; 0..100) { - ThreadSender().then(() shared => 2*3).syncWait().value.shouldEqual(6); - } +@("100.threads") @safe +unittest { + foreach (i; 0 .. 100) { + ThreadSender().then(() shared => 2 * 3).syncWait().value.shouldEqual(6); + } } diff --git a/tests/ut/concurrency/scheduler.d b/tests/ut/concurrency/scheduler.d index f2f37c0..b4eb86e 100644 --- a/tests/ut/concurrency/scheduler.d +++ b/tests/ut/concurrency/scheduler.d @@ -8,95 +8,101 @@ import concurrency.stoptoken; import core.time : msecs; import concurrency.scheduler; -@("scheduleAfter") -@safe unittest { - DelaySender(10.msecs).syncWait; +@("scheduleAfter") @safe +unittest { + DelaySender(10.msecs).syncWait; } -@("scheduleAfter.cancel") -@safe unittest { - race(DelaySender(10.msecs), DelaySender(3.msecs)).syncWait; +@("scheduleAfter.cancel") @safe +unittest { + race(DelaySender(10.msecs), DelaySender(3.msecs)).syncWait; } -@("scheduleAfter.stop-before-add") -@safe unittest { - import concurrency.sender : delay, justFrom; - auto source = new shared StopSource(); - whenAll(justFrom(() shared => source.stop), delay(10.msecs)).syncWait(source); +@("scheduleAfter.stop-before-add") @safe +unittest { + import concurrency.sender : delay, justFrom; + auto source = new shared StopSource(); + whenAll(justFrom(() shared => source.stop), delay(10.msecs)) + .syncWait(source); } -@("ManualTimeWorker") -@safe unittest { - import core.atomic : atomicOp; - - shared int g, h; - auto worker = new shared ManualTimeWorker(); - worker.addTimer((TimerTrigger trigger) shared { g.atomicOp!"+="(1); }, 10.msecs); - worker.addTimer((TimerTrigger trigger) shared { h.atomicOp!"+="(1); }, 5.msecs); - - worker.timeUntilNextEvent().should == 5.msecs; - g.should == 0; - h.should == 0; - - worker.advance(4.msecs); - worker.timeUntilNextEvent().should == 1.msecs; - h.should == 0; - g.should == 0; - - worker.advance(1.msecs); - worker.timeUntilNextEvent().should == 5.msecs; - h.should == 1; - g.should == 0; - - worker.advance(5.msecs); - h.should == 1; - g.should == 1; - worker.timeUntilNextEvent().should == null; +@("ManualTimeWorker") @safe +unittest { + import core.atomic : atomicOp; + + shared int g, h; + auto worker = new shared ManualTimeWorker(); + worker.addTimer((TimerTrigger trigger) shared { + g.atomicOp!"+="(1); + }, 10.msecs); + worker.addTimer((TimerTrigger trigger) shared { + h.atomicOp!"+="(1); + }, 5.msecs); + + worker.timeUntilNextEvent().should == 5.msecs; + g.should == 0; + h.should == 0; + + worker.advance(4.msecs); + worker.timeUntilNextEvent().should == 1.msecs; + h.should == 0; + g.should == 0; + + worker.advance(1.msecs); + worker.timeUntilNextEvent().should == 5.msecs; + h.should == 1; + g.should == 0; + + worker.advance(5.msecs); + h.should == 1; + g.should == 1; + worker.timeUntilNextEvent().should == null; } -@("ManualTimeWorker.cancel") -@safe unittest { - import core.atomic : atomicOp; - - shared int g; - auto worker = new shared ManualTimeWorker(); - auto timer = worker.addTimer((TimerTrigger trigger) shared { g.atomicOp!"+="(1 + (trigger == TimerTrigger.cancel)); }, 10.msecs); - worker.timeUntilNextEvent().should == 10.msecs; - g.should == 0; - - worker.advance(4.msecs); - worker.timeUntilNextEvent().should == 6.msecs; - g.should == 0; - - worker.cancelTimer(timer); - worker.timeUntilNextEvent().should == null; - g.should == 2; +@("ManualTimeWorker.cancel") @safe +unittest { + import core.atomic : atomicOp; + + shared int g; + auto worker = new shared ManualTimeWorker(); + auto timer = worker.addTimer((TimerTrigger trigger) shared { + g.atomicOp!"+="(1 + (trigger == TimerTrigger.cancel)); + }, 10.msecs); + worker.timeUntilNextEvent().should == 10.msecs; + g.should == 0; + + worker.advance(4.msecs); + worker.timeUntilNextEvent().should == 6.msecs; + g.should == 0; + + worker.cancelTimer(timer); + worker.timeUntilNextEvent().should == null; + g.should == 2; } -@("ManualTimeWorker.error") -@safe unittest { - import core.time; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; +@("ManualTimeWorker.error") @safe +unittest { + import core.time; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; - shared int p = 0; - import concurrency.scheduler : ManualTimeWorker; + shared int p = 0; + import concurrency.scheduler : ManualTimeWorker; - auto worker = new shared ManualTimeWorker(); + auto worker = new shared ManualTimeWorker(); - auto sender = DelaySender(10.msecs) - .withScheduler(worker.getScheduler); + auto sender = DelaySender(10.msecs).withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - worker.advance(7.msecs); - throw new Exception("halt"); - }); + auto driver = justFrom(() shared { + worker.advance(7.msecs); + throw new Exception("halt"); + }); - whenAll(sender, driver).syncWait.assumeOk.shouldThrowWithMessage("halt"); + whenAll(sender, driver).syncWait.assumeOk.shouldThrowWithMessage("halt"); } -@("toSenderObject.Schedule") -@safe unittest { - import concurrency.sender : toSenderObject; - Schedule().toSenderObject.syncWait.assumeOk; +@("toSenderObject.Schedule") @safe +unittest { + import concurrency.sender : toSenderObject; + Schedule().toSenderObject.syncWait.assumeOk; } diff --git a/tests/ut/concurrency/sender.d b/tests/ut/concurrency/sender.d index 94169af..c3e55b2 100644 --- a/tests/ut/concurrency/sender.d +++ b/tests/ut/concurrency/sender.d @@ -8,389 +8,412 @@ import concurrency.receiver; import unit_threaded; import core.atomic : atomicOp; -@("syncWait.value") -@safe unittest { - ValueSender!(int)(5).syncWait.value.shouldEqual(5); - whenAll(just(5), ThrowingSender()).syncWait.value.shouldThrow(); - whenAll(just(5), DoneSender()).syncWait.value.shouldThrow(); +@("syncWait.value") @safe +unittest { + ValueSender!(int)(5).syncWait.value.shouldEqual(5); + whenAll(just(5), ThrowingSender()).syncWait.value.shouldThrow(); + whenAll(just(5), DoneSender()).syncWait.value.shouldThrow(); } -@("syncWait.assumeOk") -@safe unittest { - ThrowingSender().syncWait.assumeOk.shouldThrow(); - DoneSender().syncWait.assumeOk.shouldThrow(); - ErrorSender(new Exception("Failure")).syncWait.assumeOk.shouldThrow(); +@("syncWait.assumeOk") @safe +unittest { + ThrowingSender().syncWait.assumeOk.shouldThrow(); + DoneSender().syncWait.assumeOk.shouldThrow(); + ErrorSender(new Exception("Failure")).syncWait.assumeOk.shouldThrow(); } -@("syncWait.match") -@safe unittest { - ValueSender!(int)(5).syncWait.match!((int i) => true, "false").should == true; +@("syncWait.match") @safe +unittest { + ValueSender!(int)(5).syncWait.match!((int i) => true, "false").should + == true; } -@("syncWait.match.void") -@safe unittest { - VoidSender().syncWait.match!((typeof(null)) => true, "false").should == true; +@("syncWait.match.void") @safe +unittest { + VoidSender().syncWait.match!((typeof(null)) => true, "false").should + == true; } -@("syncWait.nested.basic") -@safe unittest { - import concurrency.stoptoken; - auto source = new shared StopSource(); +@("syncWait.nested.basic") @safe +unittest { + import concurrency.stoptoken; + auto source = new shared StopSource(); - justFrom(() shared { - VoidSender().withStopToken((StopToken token) shared @safe { - source.stop(); - token.isStopRequested.should == true; - }).syncWait().isCancelled.should == true; - }).syncWait(source).isCancelled.should == true; + justFrom(() shared { + VoidSender().withStopToken((StopToken token) shared @safe { + source.stop(); + token.isStopRequested.should == true; + }).syncWait().isCancelled.should == true; + }).syncWait(source).isCancelled.should == true; } -@("syncWait.nested.thread") -@safe unittest { - import concurrency.stoptoken; - auto source = new shared StopSource(); +@("syncWait.nested.thread") @safe +unittest { + import concurrency.stoptoken; + auto source = new shared StopSource(); - justFrom(() shared { - VoidSender().withStopToken((StopToken token) shared @safe { - source.stop(); - token.isStopRequested.should == true; - }).syncWait().isCancelled.should == true; - }).via(ThreadSender()).syncWait(source).isCancelled.should == true; + justFrom(() shared { + VoidSender().withStopToken((StopToken token) shared @safe { + source.stop(); + token.isStopRequested.should == true; + }).syncWait().isCancelled.should == true; + }).via(ThreadSender()).syncWait(source).isCancelled.should == true; } -@("syncWait.nested.threadpool") -@safe unittest { - import concurrency.stoptoken; - auto source = new shared StopSource(); +@("syncWait.nested.threadpool") @safe +unittest { + import concurrency.stoptoken; + auto source = new shared StopSource(); - auto pool = stdTaskPool(2); + auto pool = stdTaskPool(2); - justFrom(() shared { - VoidSender().withStopToken((StopToken token) shared @safe { - source.stop(); - token.isStopRequested.should == true; - }).syncWait().isCancelled.should == true; - }).via(pool.getScheduler().schedule()).syncWait(source).isCancelled.should == true; + justFrom(() shared { + VoidSender().withStopToken((StopToken token) shared @safe { + source.stop(); + token.isStopRequested.should == true; + }).syncWait().isCancelled.should == true; + }).via(pool.getScheduler().schedule()).syncWait(source).isCancelled + .should == true; } -@("value.start.attributes.1") -@safe nothrow @nogc unittest { - ValueSender!(int)(5).connect(NullReceiver!int()).start(); +@("value.start.attributes.1") @safe +nothrow @nogc unittest { + ValueSender!(int)(5).connect(NullReceiver!int()).start(); } -@("value.start.attributes.2") -@safe nothrow unittest { - ValueSender!(int)(5).connect(ThrowingNullReceiver!int()).start(); +@("value.start.attributes.2") @safe +nothrow unittest { + ValueSender!(int)(5).connect(ThrowingNullReceiver!int()).start(); } -@("value.void") -@safe unittest { - ValueSender!void().syncWait().assumeOk; +@("value.void") @safe +unittest { + ValueSender!void().syncWait().assumeOk; } -@("syncWait.thread") -@safe unittest { - ThreadSender().syncWait.assumeOk; +@("syncWait.thread") @safe +unittest { + ThreadSender().syncWait.assumeOk; } -@("syncWait.thread.then.value") -@safe unittest { - ThreadSender().then(() shared => 2*3).syncWait.value.shouldEqual(6); +@("syncWait.thread.then.value") @safe +unittest { + ThreadSender().then(() shared => 2 * 3).syncWait.value.shouldEqual(6); } -@("syncWait.thread.then.exception") -@safe unittest { - bool delegate() @safe shared dg = () shared { throw new Exception("Exceptions are forwarded"); }; - ThreadSender() - .then(dg) - .syncWait() - .isError.should == true; +@("syncWait.thread.then.exception") @safe +unittest { + bool delegate() @safe shared dg = () shared { + throw new Exception("Exceptions are forwarded"); + }; + ThreadSender().then(dg).syncWait().isError.should == true; } -@("toSenderObject.value") -@safe unittest { - ValueSender!(int)(4).toSenderObject.syncWait.value.shouldEqual(4); +@("toSenderObject.value") @safe +unittest { + ValueSender!(int)(4).toSenderObject.syncWait.value.shouldEqual(4); } -@("toSenderObject.thread") -@safe unittest { - ThreadSender().then(() shared => 2*3+1).toSenderObject.syncWait.value.shouldEqual(7); +@("toSenderObject.thread") @safe +unittest { + ThreadSender().then(() shared => 2 * 3 + 1).toSenderObject.syncWait.value + .shouldEqual(7); } -@("via.threadsender.error") -@safe unittest { - ThrowingSender().via(ThreadSender()).syncWait().isError.should == true; +@("via.threadsender.error") @safe +unittest { + ThrowingSender().via(ThreadSender()).syncWait().isError.should == true; } -@("toShared.basic") -@safe unittest { - import std.typecons : tuple; +@("toShared.basic") @safe +unittest { + import std.typecons : tuple; - shared int g; + shared int g; - auto s = just(1) - .then((int i) @trusted shared { return g.atomicOp!"+="(1); }) - .toShared(); + auto s = just(1).then((int i) @trusted shared { + return g.atomicOp!"+="(1); + }).toShared(); - whenAll(s, s).syncWait.value.should == tuple(1,1); - race(s, s).syncWait.value.should == 1; - s.syncWait.value.should == 1; - s.syncWait.value.should == 1; + whenAll(s, s).syncWait.value.should == tuple(1, 1); + race(s, s).syncWait.value.should == 1; + s.syncWait.value.should == 1; + s.syncWait.value.should == 1; - s.reset(); - s.syncWait.value.should == 2; - s.syncWait.value.should == 2; - whenAll(s, s).syncWait.value.should == tuple(2,2); - race(s, s).syncWait.value.should == 2; + s.reset(); + s.syncWait.value.should == 2; + s.syncWait.value.should == 2; + whenAll(s, s).syncWait.value.should == tuple(2, 2); + race(s, s).syncWait.value.should == 2; } -@("toShared.via.thread") -@safe unittest { - import concurrency.operations.toshared; +@("toShared.via.thread") @safe +unittest { + import concurrency.operations.toshared; - shared int g; + shared int g; - auto s = just(1) - .then((int i) @trusted shared { return g.atomicOp!"+="(1); }) - .via(ThreadSender()) - .toShared(); + auto s = just(1).then((int i) @trusted shared { + return g.atomicOp!"+="(1); + }).via(ThreadSender()).toShared(); - s.syncWait.value.should == 1; - s.syncWait.value.should == 1; + s.syncWait.value.should == 1; + s.syncWait.value.should == 1; - s.reset(); - s.syncWait.value.should == 2; - s.syncWait.value.should == 2; + s.reset(); + s.syncWait.value.should == 2; + s.syncWait.value.should == 2; } -@("toShared.error") -@safe unittest { - shared int g; +@("toShared.error") @safe +unittest { + shared int g; - auto s = VoidSender() - .then(() @trusted shared { g.atomicOp!"+="(1); throw new Exception("Error"); }) - .toShared(); + auto s = VoidSender().then(() @trusted shared { + g.atomicOp!"+="(1); + throw new Exception("Error"); + }).toShared(); - s.syncWait.assumeOk.shouldThrowWithMessage("Error"); - g.should == 1; - s.syncWait.assumeOk.shouldThrowWithMessage("Error"); - g.should == 1; + s.syncWait.assumeOk.shouldThrowWithMessage("Error"); + g.should == 1; + s.syncWait.assumeOk.shouldThrowWithMessage("Error"); + g.should == 1; - race(s, s).syncWait.assumeOk.shouldThrowWithMessage("Error"); - g.should == 1; + race(s, s).syncWait.assumeOk.shouldThrowWithMessage("Error"); + g.should == 1; - s.reset(); - s.syncWait.assumeOk.shouldThrowWithMessage("Error"); - g.should == 2; + s.reset(); + s.syncWait.assumeOk.shouldThrowWithMessage("Error"); + g.should == 2; } -@("toShared.done") -@safe unittest { - shared int g; +@("toShared.done") @safe +unittest { + shared int g; - auto s = DoneSender() - .via(VoidSender() - .then(() @trusted shared { g.atomicOp!"+="(1); })) - .toShared(); + auto s = DoneSender().via(VoidSender().then(() @trusted shared { + g.atomicOp!"+="(1); + })).toShared(); - s.syncWait.isCancelled.should == true; - g.should == 1; - s.syncWait.isCancelled.should == true; - g.should == 1; + s.syncWait.isCancelled.should == true; + g.should == 1; + s.syncWait.isCancelled.should == true; + g.should == 1; - race(s, s).syncWait.isCancelled.should == true; - g.should == 1; + race(s, s).syncWait.isCancelled.should == true; + g.should == 1; - s.reset(); - s.syncWait.isCancelled.should == true; - g.should == 2; + s.reset(); + s.syncWait.isCancelled.should == true; + g.should == 2; } -@("toShared.stop") -@safe unittest { - import concurrency.stoptoken; - import core.atomic : atomicStore, atomicLoad; - shared bool g; +@("toShared.stop") @safe +unittest { + import concurrency.stoptoken; + import core.atomic : atomicStore, atomicLoad; + shared bool g; - auto waiting = ThreadSender().withStopToken((StopToken token) shared @trusted { - while (!token.isStopRequested) { } - g.atomicStore(true); - }); - auto source = new StopSource(); - auto stopper = just(source).then((StopSource source) shared { source.stop(); }); + auto waiting = + ThreadSender().withStopToken((StopToken token) shared @trusted { + while (!token.isStopRequested) {} + g.atomicStore(true); + }); + auto source = new StopSource(); + auto stopper = just(source).then((StopSource source) shared { + source.stop(); + }); - whenAll(waiting.toShared().withStopSource(source), stopper).syncWait.isCancelled.should == true; + whenAll(waiting.toShared().withStopSource(source), stopper) + .syncWait.isCancelled.should == true; - g.atomicLoad.should == true; + g.atomicLoad.should == true; } -@("toShared.scheduler") -@safe unittest { - import core.time : msecs; - // by default toShared doesn't support scheduling - static assert(!__traits(compiles, { DelaySender(1.msecs).toShared().syncWait().assumeOk; })); - // have to pass scheduler explicitly - import concurrency.scheduler : localThreadScheduler; - DelaySender(1.msecs).toShared(localThreadScheduler).syncWait().assumeOk; +@("toShared.scheduler") @safe +unittest { + import core.time : msecs; + // by default toShared doesn't support scheduling + static assert(!__traits(compiles, { + DelaySender(1.msecs).toShared().syncWait().assumeOk; + })); + // have to pass scheduler explicitly + import concurrency.scheduler : localThreadScheduler; + DelaySender(1.msecs).toShared(localThreadScheduler).syncWait().assumeOk; } -@("toShared.nursery") -@safe unittest { - /// just see if we can instantiate - import concurrency.nursery; - import concurrency.scheduler; - auto n = new shared Nursery(); - auto s = n.toShared(localThreadScheduler()); +@("toShared.nursery") @safe +unittest { + /// just see if we can instantiate + import concurrency.nursery; + import concurrency.scheduler; + auto n = new shared Nursery(); + auto s = n.toShared(localThreadScheduler()); } -@("nvro") -@safe unittest { - static struct Op(Receiver) { - Receiver receiver; - void* atConstructor; - @disable this(ref return scope typeof(this) rhs); - this(Receiver receiver) @trusted { - this.receiver = receiver; - atConstructor = cast(void*)&this; - } - void start() @trusted nothrow scope { - void* atStart = cast(void*)&this; - receiver.setValue(atConstructor == atStart); - } - } - static struct NRVOSender { - alias Value = bool; - auto connect(Receiver)(return Receiver receiver) @safe return scope { - // ensure NRVO - auto op = Op!Receiver(receiver); - return op; - } - } - NRVOSender().syncWait().value.should == true; - NRVOSender().via(ThreadSender()).syncWait().value.should == true; - whenAll(NRVOSender(),VoidSender()).syncWait.value.should == true; - whenAll(VoidSender(),NRVOSender()).syncWait.value.should == true; - race(NRVOSender(),NRVOSender()).syncWait.value.should == true; -} - -@("justFrom") -@safe unittest { - justFrom(() shared =>42).syncWait.value.should == 42; -} +@("nvro") @safe +unittest { + static struct Op(Receiver) { + Receiver receiver; + void* atConstructor; + @disable + this(ref return scope typeof(this) rhs); + this(Receiver receiver) @trusted { + this.receiver = receiver; + atConstructor = cast(void*) &this; + } -@("justFrom.exception") -@safe unittest { - justFrom(() shared { throw new Exception("failure"); }).syncWait.isError.should == true; + void start() @trusted nothrow scope { + void* atStart = cast(void*) &this; + receiver.setValue(atConstructor == atStart); + } + } + + static struct NRVOSender { + alias Value = bool; + auto connect(Receiver)(return Receiver receiver) @safe return scope { + // ensure NRVO + auto op = Op!Receiver(receiver); + return op; + } + } + + NRVOSender().syncWait().value.should == true; + NRVOSender().via(ThreadSender()).syncWait().value.should == true; + whenAll(NRVOSender(), VoidSender()).syncWait.value.should == true; + whenAll(VoidSender(), NRVOSender()).syncWait.value.should == true; + race(NRVOSender(), NRVOSender()).syncWait.value.should == true; +} + +@("justFrom") @safe +unittest { + justFrom(() shared => 42).syncWait.value.should == 42; +} + +@("justFrom.exception") @safe +unittest { + justFrom(() shared { + throw new Exception("failure"); + }).syncWait.isError.should == true; +} + +@("delay") @safe +unittest { + import core.time : msecs; + import core.time : msecs; + import concurrency.scheduler : ManualTimeWorker; + + auto worker = new shared ManualTimeWorker(); + + auto d = race(delay(20.msecs).then(() shared => 2), + delay(1.msecs).then(() shared => 1)) + .withScheduler(worker.getScheduler); + + auto driver = just(worker).then((shared ManualTimeWorker worker) { + worker.timeUntilNextEvent().should == 1.msecs; + worker.advance(1.msecs); + }); + + whenAll(d, driver).syncWait.value.should == 1; +} + +@("promise.basic") @safe +unittest { + auto prom = promise!int; + auto cont = prom.sender.then((int i) => i * 2); + auto runner = justFrom(() shared => cast(void) prom.fulfill(72)); + + whenAll(cont, runner).syncWait.value.should == 144; +} + +@("promise.double") @safe +unittest { + import std.typecons : tuple; + auto prom = promise!int; + auto cont = prom.sender.then((int i) => i * 2); + auto runner = justFrom(() shared => cast(void) prom.fulfill(72)); + + whenAll(cont, cont, runner).syncWait.value.should == tuple(144, 144); +} + +@("promise.scheduler") @safe +unittest { + import std.typecons : tuple; + auto prom = promise!int; + auto pool = stdTaskPool(2); + + auto cont = prom.sender.forwardOn(pool.getScheduler).then((int i) => i * 2); + auto runner = + justFrom(() shared => cast(void) prom.fulfill(72)).via(ThreadSender()); + + whenAll(cont, cont, runner).syncWait.value.should == tuple(144, 144); +} + +@("promise.then.exception.inline") @safe +unittest { + auto prom = promise!int; + auto cont = prom.sender.then((int i) { + throw new Exception("nope"); + }); + prom.fulfill(33); + cont.syncWait().assumeOk.shouldThrowWithMessage("nope"); } -@("delay") -@safe unittest { - import core.time : msecs; - import core.time : msecs; - import concurrency.scheduler : ManualTimeWorker; - - auto worker = new shared ManualTimeWorker(); - - auto d = race(delay(20.msecs).then(() shared => 2), - delay(1.msecs).then(() shared => 1)) - .withScheduler(worker.getScheduler); - - auto driver = just(worker).then((shared ManualTimeWorker worker) { - worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - }); - - whenAll(d, driver).syncWait.value.should == 1; -} - -@("promise.basic") -@safe unittest { - auto prom = promise!int; - auto cont = prom.sender.then((int i) => i * 2); - auto runner = justFrom(() shared => cast(void)prom.fulfill(72)); - - whenAll(cont, runner).syncWait.value.should == 144; -} - -@("promise.double") -@safe unittest { - import std.typecons : tuple; - auto prom = promise!int; - auto cont = prom.sender.then((int i) => i * 2); - auto runner = justFrom(() shared => cast(void)prom.fulfill(72)); +@("promise.then.exception.thread") @safe +unittest { + auto prom = promise!int; + auto cont = prom.sender.then((int i) { + throw new Exception("nope"); + }); + auto runner = + justFrom(() shared => cast(void) prom.fulfill(72)).via(ThreadSender()); + whenAll(cont, runner).syncWait().assumeOk.shouldThrowWithMessage("nope"); +} - whenAll(cont, cont, runner).syncWait.value.should == tuple(144, 144); -} - -@("promise.scheduler") -@safe unittest { - import std.typecons : tuple; - auto prom = promise!int; - auto pool = stdTaskPool(2); - - auto cont = prom.sender.forwardOn(pool.getScheduler).then((int i) => i * 2); - auto runner = justFrom(() shared => cast(void)prom.fulfill(72)).via(ThreadSender()); - - whenAll(cont, cont, runner).syncWait.value.should == tuple(144, 144); -} - -@("promise.then.exception.inline") -@safe unittest { - auto prom = promise!int; - auto cont = prom.sender.then((int i) { throw new Exception("nope"); }); - prom.fulfill(33); - cont.syncWait().assumeOk.shouldThrowWithMessage("nope"); -} - -@("promise.then.exception.thread") -@safe unittest { - auto prom = promise!int; - auto cont = prom.sender.then((int i) { throw new Exception("nope"); }); - auto runner = justFrom(() shared => cast(void)prom.fulfill(72)).via(ThreadSender()); - whenAll(cont, runner).syncWait().assumeOk.shouldThrowWithMessage("nope"); -} - -@("just.tuple") -@safe unittest { - import std.typecons : tuple; - import concurrency.stoptoken; - just(14, 52).syncWait.value.should == tuple(14, 52); - just(14, 53).then((int a, int b) => a*b).syncWait.value.should == 742; - just(14, 54).withStopToken((StopToken s, int a, int b) => a*b).syncWait.value.should == 756; +@("just.tuple") @safe +unittest { + import std.typecons : tuple; + import concurrency.stoptoken; + just(14, 52).syncWait.value.should == tuple(14, 52); + just(14, 53).then((int a, int b) => a * b).syncWait.value.should == 742; + just(14, 54).withStopToken((StopToken s, int a, int b) => a * b).syncWait + .value.should == 756; } -@("just.scope") -@safe unittest { - void disappearSender(Sender)(Sender s) @safe; - int g; - scope int* s = &g; - just(s).syncWait().value.should == s; - just(s).retry(Times(5)).syncWait().value.should == s; - static assert(!__traits(compiles, disappearSender(just(s)))); - static assert(!__traits(compiles, disappearSender(just(s).retry(Times(5))))); +@("just.scope") @safe +unittest { + void disappearSender(Sender)(Sender s) @safe; + int g; + scope int* s = &g; + just(s).syncWait().value.should == s; + just(s).retry(Times(5)).syncWait().value.should == s; + static assert(!__traits(compiles, disappearSender(just(s)))); + static assert( + !__traits(compiles, disappearSender(just(s).retry(Times(5))))); } -@("defer.static.fun") -@safe unittest { - static auto fun() { - return just(1); - } +@("defer.static.fun") @safe +unittest { + static auto fun() { + return just(1); + } - defer(&fun).syncWait().value.should == 1; + defer(&fun).syncWait().value.should == 1; } -@("defer.delegate") -@safe unittest { - defer(() => just(1)).syncWait().value.should == 1; +@("defer.delegate") @safe +unittest { + defer(() => just(1)).syncWait().value.should == 1; } -@("defer.opCall") -@safe unittest { - static struct S { - auto opCall() @safe shared { - return just(1); - } - } - shared S s; - defer(s).syncWait().value.should == 1; +@("defer.opCall") @safe +unittest { + static struct S { + auto opCall() @safe shared { + return just(1); + } + } + + shared S s; + defer(s).syncWait().value.should == 1; } diff --git a/tests/ut/concurrency/slist.d b/tests/ut/concurrency/slist.d index e8ed112..a894100 100644 --- a/tests/ut/concurrency/slist.d +++ b/tests/ut/concurrency/slist.d @@ -10,121 +10,121 @@ import concurrency : syncWait; import std.range : walkLength; import unit_threaded; -@("pushFront.race") -@safe unittest { - auto list = new shared SList!int; +@("pushFront.race") @safe +unittest { + auto list = new shared SList!int; - auto filler = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..100) { - list.pushFront(i); - } - }).via(ThreadSender()); + auto filler = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 100) { + list.pushFront(i); + } + }).via(ThreadSender()); - whenAll(filler, filler).syncWait(); + whenAll(filler, filler).syncWait(); - list[].walkLength.should == 200; + list[].walkLength.should == 200; } -@("pushBack.race") -@safe unittest { - auto list = new shared SList!int; +@("pushBack.race") @safe +unittest { + auto list = new shared SList!int; - auto filler = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..100) { - list.pushBack(i); - } - }).via(ThreadSender()); + auto filler = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 100) { + list.pushBack(i); + } + }).via(ThreadSender()); - whenAll(filler, filler).syncWait(); + whenAll(filler, filler).syncWait(); - list[].walkLength.should == 200; + list[].walkLength.should == 200; } -@("pushFront.adversary") -@safe unittest { - auto list = new shared SList!int; +@("pushFront.adversary") @safe +unittest { + auto list = new shared SList!int; - foreach(i; 0..50) - list.pushFront(1); + foreach (i; 0 .. 50) + list.pushFront(1); - auto filler = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..50) { - list.pushFront(1); - } - }).via(ThreadSender()); - auto remover = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..50) { - list.remove(1); - } - }).via(ThreadSender()); + auto filler = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 50) { + list.pushFront(1); + } + }).via(ThreadSender()); + auto remover = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 50) { + list.remove(1); + } + }).via(ThreadSender()); - whenAll(filler, remover).syncWait(); + whenAll(filler, remover).syncWait(); - list[].walkLength.should == 50; + list[].walkLength.should == 50; } -@("pushBack.adversary") -@safe unittest { - auto list = new shared SList!int; - - auto filler = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..100) { - list.pushBack(1); - } - }).via(ThreadSender()); - auto remover = just(list).then((shared SList!int list) @safe shared { - int n = 0; - while(n < 99) - if (list.remove(1)) - n++; - }).via(ThreadSender()); - - whenAll(filler, remover).syncWait(); - - list[].walkLength.should == 1; +@("pushBack.adversary") @safe +unittest { + auto list = new shared SList!int; + + auto filler = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 100) { + list.pushBack(1); + } + }).via(ThreadSender()); + auto remover = just(list).then((shared SList!int list) @safe shared { + int n = 0; + while (n < 99) + if (list.remove(1)) + n++; + }).via(ThreadSender()); + + whenAll(filler, remover).syncWait(); + + list[].walkLength.should == 1; } -@("remove.race") -@safe unittest { - auto list = new shared SList!int; +@("remove.race") @safe +unittest { + auto list = new shared SList!int; - foreach(i; 0..100) - list.pushFront(i); + foreach (i; 0 .. 100) + list.pushFront(i); - auto remover = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..100) { - if (i % 10 > 4) - list.remove(i); - } - }).via(ThreadSender()); + auto remover = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 100) { + if (i % 10 > 4) + list.remove(i); + } + }).via(ThreadSender()); - whenAll(remover, remover).syncWait(); + whenAll(remover, remover).syncWait(); - list[].walkLength.should == 50; + list[].walkLength.should == 50; } -@("remove.adjacent") -@safe unittest { - auto list = new shared SList!int; - - foreach(_; 0..2) - foreach(i; 0..100) - list.pushFront(i); - - auto remover1 = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..100) { - if (i % 2 == 0) - list.remove(i); - } - }).via(ThreadSender()); - auto remover2 = just(list).then((shared SList!int list) @safe shared { - foreach(i; 0..100) { - if (i % 2 == 1) - list.remove(i); - } - }).via(ThreadSender()); - - whenAll(remover1, remover2, remover1, remover2).syncWait(); - - list[].walkLength.should == 0; +@("remove.adjacent") @safe +unittest { + auto list = new shared SList!int; + + foreach (_; 0 .. 2) + foreach (i; 0 .. 100) + list.pushFront(i); + + auto remover1 = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 100) { + if (i % 2 == 0) + list.remove(i); + } + }).via(ThreadSender()); + auto remover2 = just(list).then((shared SList!int list) @safe shared { + foreach (i; 0 .. 100) { + if (i % 2 == 1) + list.remove(i); + } + }).via(ThreadSender()); + + whenAll(remover1, remover2, remover1, remover2).syncWait(); + + list[].walkLength.should == 0; } diff --git a/tests/ut/concurrency/stream.d b/tests/ut/concurrency/stream.d index c17966a..e847b27 100644 --- a/tests/ut/concurrency/stream.d +++ b/tests/ut/concurrency/stream.d @@ -9,1011 +9,991 @@ import concurrency.thread : ThreadSender; // TODO: it would be good if we can get the Sender .collect returns to be scoped if the delegates are. -@("arrayStream") -@safe unittest { - shared int p = 0; - [1,2,3].arrayStream().collect((int t) shared { p.atomicOp!"+="(t); }).syncWait().assumeOk; - p.should == 6; +@("arrayStream") @safe +unittest { + shared int p = 0; + [1, 2, 3].arrayStream().collect((int t) shared { + p.atomicOp!"+="(t); + }).syncWait().assumeOk; + p.should == 6; } -@("intervalStream") -@safe unittest { - import core.time; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; - import concurrency.scheduler : ManualTimeWorker; +@("intervalStream") @safe +unittest { + import core.time; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; + import concurrency.scheduler : ManualTimeWorker; - auto worker = new shared ManualTimeWorker(); - auto interval = 5.msecs.intervalStream() - .take(2) - .collect(() shared {}) - .withScheduler(worker.getScheduler); + auto worker = new shared ManualTimeWorker(); + auto interval = 5.msecs.intervalStream().take(2).collect(() shared {}) + .withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - worker.timeUntilNextEvent().should == 5.msecs; - worker.advance(5.msecs); + auto driver = justFrom(() shared { + worker.timeUntilNextEvent().should == 5.msecs; + worker.advance(5.msecs); - worker.timeUntilNextEvent().should == 5.msecs; - worker.advance(5.msecs); + worker.timeUntilNextEvent().should == 5.msecs; + worker.advance(5.msecs); - worker.timeUntilNextEvent().should == null; - }); + worker.timeUntilNextEvent().should == null; + }); - whenAll(interval, driver).syncWait().assumeOk; + whenAll(interval, driver).syncWait().assumeOk; } +@("infiniteStream.stop") @safe +unittest { + import concurrency.operations : withStopSource; + shared int g = 0; + auto source = new shared StopSource(); + infiniteStream(5).collect((int n) shared { + if (g < 14) + g.atomicOp!"+="(n); + else + source.stop(); + }).withStopSource(source).syncWait.isCancelled.should == true; + g.should == 15; +} -@("infiniteStream.stop") -@safe unittest { - import concurrency.operations : withStopSource; - shared int g = 0; - auto source = new shared StopSource(); - infiniteStream(5).collect((int n) shared { - if (g < 14) - g.atomicOp!"+="(n); - else - source.stop(); - }) - .withStopSource(source).syncWait.isCancelled.should == true; - g.should == 15; -}; +; -@("infiniteStream.take") -@safe unittest { - shared int g = 0; - infiniteStream(4).take(5).collect((int n) shared { g.atomicOp!"+="(n); }).syncWait().assumeOk; - g.should == 20; +@("infiniteStream.take") @safe +unittest { + shared int g = 0; + infiniteStream(4).take(5).collect((int n) shared { + g.atomicOp!"+="(n); + }).syncWait().assumeOk; + g.should == 20; } -@("iotaStream") -@safe unittest { - import concurrency.stoptoken; - shared int g = 0; - iotaStream(0, 5).collect((int n) shared { g.atomicOp!"+="(n); }).syncWait().assumeOk; - g.should == 10; +@("iotaStream") @safe +unittest { + import concurrency.stoptoken; + shared int g = 0; + iotaStream(0, 5).collect((int n) shared { + g.atomicOp!"+="(n); + }).syncWait().assumeOk; + g.should == 10; } -@("loopStream") -@safe unittest { - struct Loop { - size_t b,e; - void loop(DG, StopToken)(DG emit, StopToken stopToken) { - foreach(i; b..e) - emit(i); - } - } - shared int g = 0; - Loop(0,4).loopStream!size_t.collect((size_t n) shared { g.atomicOp!"+="(n); }).syncWait().assumeOk; - g.should == 6; -} +@("loopStream") @safe +unittest { + struct Loop { + size_t b, e; + void loop(DG, StopToken)(DG emit, StopToken stopToken) { + foreach (i; b .. e) + emit(i); + } + } + + shared int g = 0; + Loop(0, 4).loopStream!size_t.collect((size_t n) shared { + g.atomicOp!"+="(n); + }).syncWait().assumeOk; + g.should == 6; +} + +@("toStreamObject") @safe +unittest { + import core.atomic : atomicOp; -@("toStreamObject") -@safe unittest { - import core.atomic : atomicOp; + static StreamObjectBase!int getStream() { + return [1, 2, 3].arrayStream().toStreamObject(); + } - static StreamObjectBase!int getStream() { - return [1,2,3].arrayStream().toStreamObject(); - } - shared int p; + shared int p; - getStream().collect((int i) @safe shared { p.atomicOp!"+="(i); }).syncWait().assumeOk; + getStream().collect((int i) @safe shared { + p.atomicOp!"+="(i); + }).syncWait().assumeOk; - p.should == 6; + p.should == 6; } +@("toStreamObject.take") @safe +unittest { + static StreamObjectBase!int getStream() { + return [1, 2, 3].arrayStream().toStreamObject(); + } -@("toStreamObject.take") -@safe unittest { - static StreamObjectBase!int getStream() { - return [1,2,3].arrayStream().toStreamObject(); - } - shared int p; + shared int p; - getStream().take(2).collect((int i) shared { p.atomicOp!"+="(i); }).syncWait().assumeOk; + getStream().take(2).collect((int i) shared { + p.atomicOp!"+="(i); + }).syncWait().assumeOk; - p.should == 3; + p.should == 3; } -@("toStreamObject.void") -@safe unittest { - import core.time : msecs; - shared bool p = false; +@("toStreamObject.void") @safe +unittest { + import core.time : msecs; + shared bool p = false; - 1.msecs.intervalStream().toStreamObject().take(1).collect(() shared { p = true; }).syncWait().assumeOk; + 1.msecs.intervalStream().toStreamObject().take(1).collect(() shared { + p = true; + }).syncWait().assumeOk; - p.should == true; + p.should == true; } -@("transform.int.double") -@safe unittest { - shared int p = 0; - [1,2,3].arrayStream().transform((int i) => i * 3).collect((int t) shared { p.atomicOp!"+="(t); }).syncWait().assumeOk; - p.should == 18; +@("transform.int.double") @safe +unittest { + shared int p = 0; + [1, 2, 3].arrayStream().transform((int i) => i * 3).collect((int t) shared { + p.atomicOp!"+="(t); + }).syncWait().assumeOk; + p.should == 18; } -@("transform.int.bool") -@safe unittest { - shared int p = 0; - [1,2,3].arrayStream().transform((int i) => i % 2 == 0).collect((bool t) shared { if (t) p.atomicOp!"+="(1); }).syncWait().assumeOk; - p.should == 1; +@("transform.int.bool") @safe +unittest { + shared int p = 0; + [1, 2, 3].arrayStream().transform((int i) => i % 2 == 0) + .collect((bool t) shared { + if (t) + p.atomicOp!"+="(1); + }).syncWait().assumeOk; + p.should == 1; } -@("scan") -@safe unittest { - shared int p = 0; - [1,2,3].arrayStream().scan((int acc, int i) => acc += i, 0).collect((int t) shared { p.atomicOp!"+="(t); }).syncWait().assumeOk; - p.should == 10; +@("scan") @safe +unittest { + shared int p = 0; + [1, 2, 3].arrayStream().scan((int acc, int i) => acc += i, 0) + .collect((int t) shared { + p.atomicOp!"+="(t); + }).syncWait().assumeOk; + p.should == 10; } -@("scan.void-value") -@safe unittest { - import core.time; - shared int p = 0; - 5.msecs.intervalStream.scan((int acc) => acc += 1, 0).take(3).collect((int t) shared { p.atomicOp!"+="(t); }).syncWait().assumeOk; - p.should == 6; +@("scan.void-value") @safe +unittest { + import core.time; + shared int p = 0; + 5.msecs.intervalStream.scan((int acc) => acc += 1, 0).take(3) + .collect((int t) shared { + p.atomicOp!"+="(t); + }).syncWait().assumeOk; + p.should == 6; } -@("take.enough") -@safe unittest { - shared int p = 0; +@("take.enough") @safe +unittest { + shared int p = 0; - [1,2,3].arrayStream.take(2).collect((int i) shared { p.atomicOp!"+="(i); }).syncWait.assumeOk; - p.should == 3; + [1, 2, 3].arrayStream.take(2).collect((int i) shared { + p.atomicOp!"+="(i); + }).syncWait.assumeOk; + p.should == 3; } -@("take.too-few") -@safe unittest { - shared int p = 0; - - [1,2,3].arrayStream.take(4).collect((int i) shared { p.atomicOp!"+="(i); }).syncWait.assumeOk; - p.should == 6; -} +@("take.too-few") @safe +unittest { + shared int p = 0; -@("take.donestream") -@safe unittest { - doneStream().take(1).collect(()shared{}).syncWait.isCancelled.should == true; + [1, 2, 3].arrayStream.take(4).collect((int i) shared { + p.atomicOp!"+="(i); + }).syncWait.assumeOk; + p.should == 6; } -@("take.errorstream") -@safe unittest { - errorStream(new Exception("Too bad")).take(1).collect(()shared{}).syncWait.assumeOk.shouldThrowWithMessage("Too bad"); +@("take.donestream") @safe +unittest { + doneStream().take(1).collect(() shared {}).syncWait.isCancelled.should + == true; } -@("sample.trigger.stop") -@safe unittest { - import core.time; - 7.msecs.intervalStream() - .scan((int acc) => acc+1, 0) - .sample(10.msecs.intervalStream().take(3)) - .collect((int i) shared {}) - .syncWait().assumeOk; +@("take.errorstream") @safe +unittest { + errorStream(new Exception("Too bad")) + .take(1).collect(() shared {}).syncWait.assumeOk + .shouldThrowWithMessage("Too bad"); } -@("sample.slower") -@safe unittest { - import core.time; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; +@("sample.trigger.stop") @safe +unittest { + import core.time; + 7 + .msecs + .intervalStream() + .scan((int acc) => acc + 1, 0) + .sample(10.msecs.intervalStream().take(3)) + .collect((int i) shared {}) + .syncWait() + .assumeOk; +} + +@("sample.slower") @safe +unittest { + import core.time; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; - shared int p = 0; - import concurrency.scheduler : ManualTimeWorker; + shared int p = 0; + import concurrency.scheduler : ManualTimeWorker; - auto worker = new shared ManualTimeWorker(); + auto worker = new shared ManualTimeWorker(); - auto sampler = 7.msecs - .intervalStream() - .scan((int acc) => acc+1, 0) - .sample(10.msecs.intervalStream()) - .take(3) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .withScheduler(worker.getScheduler); + auto sampler = + 7.msecs.intervalStream().scan((int acc) => acc + 1, 0) + .sample(10.msecs.intervalStream()).take(3).collect((int i) shared { + p.atomicOp!"+="(i); + }).withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - worker.advance(7.msecs); - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 3.msecs; + auto driver = justFrom(() shared { + worker.advance(7.msecs); + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 4.msecs; + worker.advance(3.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 4.msecs; - worker.advance(4.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 6.msecs; + worker.advance(4.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 6.msecs; - worker.advance(6.msecs); - p.atomicLoad.should == 3; - worker.timeUntilNextEvent().should == 1.msecs; + worker.advance(6.msecs); + p.atomicLoad.should == 3; + worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - p.atomicLoad.should == 3; - worker.timeUntilNextEvent().should == 7.msecs; + worker.advance(1.msecs); + p.atomicLoad.should == 3; + worker.timeUntilNextEvent().should == 7.msecs; - worker.advance(7.msecs); - p.atomicLoad.should == 3; - worker.timeUntilNextEvent().should == 2.msecs; + worker.advance(7.msecs); + p.atomicLoad.should == 3; + worker.timeUntilNextEvent().should == 2.msecs; - worker.advance(2.msecs); - p.atomicLoad.should == 7; - worker.timeUntilNextEvent().should == null; - }); + worker.advance(2.msecs); + p.atomicLoad.should == 7; + worker.timeUntilNextEvent().should == null; + }); - whenAll(sampler, driver).syncWait().assumeOk; + whenAll(sampler, driver).syncWait().assumeOk; - p.should == 7; + p.should == 7; } -@("sample.faster") -@safe unittest { - import core.time; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; +@("sample.faster") @safe +unittest { + import core.time; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; - shared int p = 0; - import concurrency.scheduler : ManualTimeWorker; + shared int p = 0; + import concurrency.scheduler : ManualTimeWorker; - auto worker = new shared ManualTimeWorker(); + auto worker = new shared ManualTimeWorker(); - auto sampler = 7.msecs - .intervalStream() - .scan((int acc) => acc+1, 0) - .sample(3.msecs.intervalStream()) - .take(3) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .withScheduler(worker.getScheduler); + auto sampler = + 7.msecs.intervalStream().scan((int acc) => acc + 1, 0) + .sample(3.msecs.intervalStream()).take(3).collect((int i) shared { + p.atomicOp!"+="(i); + }).withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - worker.advance(3.msecs); - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 3.msecs; + auto driver = justFrom(() shared { + worker.advance(3.msecs); + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 1.msecs; + worker.advance(3.msecs); + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 2.msecs; + worker.advance(1.msecs); + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 2.msecs; - worker.advance(2.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 3.msecs; + worker.advance(2.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 2.msecs; + worker.advance(3.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 2.msecs; - worker.advance(2.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 1.msecs; + worker.advance(2.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - p.atomicLoad.should == 3; - worker.timeUntilNextEvent().should == 3.msecs; + worker.advance(1.msecs); + p.atomicLoad.should == 3; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 3; - worker.timeUntilNextEvent().should == 3.msecs; + worker.advance(3.msecs); + p.atomicLoad.should == 3; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 6; - worker.timeUntilNextEvent().should == null; - }); + worker.advance(3.msecs); + p.atomicLoad.should == 6; + worker.timeUntilNextEvent().should == null; + }); - whenAll(sampler, driver).syncWait().assumeOk; + whenAll(sampler, driver).syncWait().assumeOk; - p.should == 6; + p.should == 6; } -@("sharedStream") -@safe unittest { - import concurrency.operations : then, race; +@("sharedStream") @safe +unittest { + import concurrency.operations : then, race; - auto source = sharedStream!int; + auto source = sharedStream!int; - shared int p = 0; + shared int p = 0; - auto emitter = ThreadSender().then(() shared { - source.emit(6); - source.emit(12); - }); - auto collector = source.collect((int t) shared { p.atomicOp!"+="(t); }); + auto emitter = ThreadSender().then(() shared { + source.emit(6); + source.emit(12); + }); + auto collector = source.collect((int t) shared { + p.atomicOp!"+="(t); + }); - race(collector, emitter).syncWait().assumeOk; + race(collector, emitter).syncWait().assumeOk; - p.atomicLoad.should == 18; + p.atomicLoad.should == 18; } -@("throttling.throttleLast") -@safe unittest { - import core.time; - import concurrency.scheduler : ManualTimeWorker; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; +@("throttling.throttleLast") @safe +unittest { + import core.time; + import concurrency.scheduler : ManualTimeWorker; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; - shared int p = 0; - auto worker = new shared ManualTimeWorker(); + shared int p = 0; + auto worker = new shared ManualTimeWorker(); - auto throttled = 1.msecs - .intervalStream(true) - .scan((int acc) => acc+1, 0) - .throttleLast(3.msecs) - .take(4) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .withScheduler(worker.getScheduler); + auto throttled = 1.msecs.intervalStream(true).scan((int acc) => acc + 1, 0) + .throttleLast(3.msecs).take(4).collect((int i) shared { + p.atomicOp!"+="(i); + }).withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 1.msecs; + auto driver = justFrom(() shared { + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 1.msecs; - foreach(expected; [0,0,3,3,3,9,9,9,18,18,18,30]) { - worker.advance(1.msecs); - p.atomicLoad.should == expected; - } + foreach (expected; [0, 0, 3, 3, 3, 9, 9, 9, 18, 18, 18, 30]) { + worker.advance(1.msecs); + p.atomicLoad.should == expected; + } - worker.timeUntilNextEvent().should == null; - }); + worker.timeUntilNextEvent().should == null; + }); - whenAll(throttled, driver).syncWait().assumeOk; + whenAll(throttled, driver).syncWait().assumeOk; - p.atomicLoad.should == 30; + p.atomicLoad.should == 30; } -@("throttling.throttleLast.arrayStream") -@safe unittest { - import core.time; +@("throttling.throttleLast.arrayStream") @safe +unittest { + import core.time; - shared int p = 0; + shared int p = 0; - [1,2,3].arrayStream() - .throttleLast(30.msecs) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .syncWait().assumeOk; + [1, 2, 3].arrayStream().throttleLast(30.msecs).collect((int i) shared { + p.atomicOp!"+="(i); + }).syncWait().assumeOk; - p.atomicLoad.should == 3; + p.atomicLoad.should == 3; } -@("throttling.throttleLast.exception") -@safe unittest { - import core.time; +@("throttling.throttleLast.exception") @safe +unittest { + import core.time; - 1.msecs - .intervalStream() - .throttleLast(10.msecs) - .collect(() shared { throw new Exception("Bla"); }) - .syncWait.assumeOk.shouldThrowWithMessage("Bla"); + 1.msecs.intervalStream().throttleLast(10.msecs).collect(() shared { + throw new Exception("Bla"); + }).syncWait.assumeOk.shouldThrowWithMessage("Bla"); } -@("throttling.throttleLast.thread.arrayStream") -@safe unittest { - import core.time; +@("throttling.throttleLast.thread.arrayStream") @safe +unittest { + import core.time; - shared int p = 0; + shared int p = 0; - [1,2,3].arrayStream() - .via(ThreadSender()) - .throttleLast(30.msecs) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .syncWait().assumeOk; + [1, 2, 3].arrayStream().via(ThreadSender()).throttleLast(30.msecs) + .collect((int i) shared { + p.atomicOp!"+="(i); + }).syncWait().assumeOk; - p.atomicLoad.should == 3; + p.atomicLoad.should == 3; } -@("throttling.throttleLast.thread.exception") -@safe unittest { - import core.time; +@("throttling.throttleLast.thread.exception") @safe +unittest { + import core.time; - 1.msecs - .intervalStream() - .via(ThreadSender()) - .throttleLast(10.msecs) - .collect(() shared { throw new Exception("Bla"); }) - .syncWait.assumeOk.shouldThrowWithMessage("Bla"); + 1.msecs.intervalStream().via(ThreadSender()).throttleLast(10.msecs) + .collect(() shared { + throw new Exception("Bla"); + }).syncWait.assumeOk.shouldThrowWithMessage("Bla"); } -@("throttling.throttleFirst") -@safe unittest { - import core.time; - import concurrency.scheduler : ManualTimeWorker; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; +@("throttling.throttleFirst") @safe +unittest { + import core.time; + import concurrency.scheduler : ManualTimeWorker; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; - shared int p = 0; - auto worker = new shared ManualTimeWorker(); + shared int p = 0; + auto worker = new shared ManualTimeWorker(); - auto throttled = 1.msecs - .intervalStream() - .scan((int acc) => acc+1, 0) - .throttleFirst(3.msecs) - .take(2) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .withScheduler(worker.getScheduler); + auto throttled = 1.msecs.intervalStream().scan((int acc) => acc + 1, 0) + .throttleFirst(3.msecs).take(2).collect((int i) shared { + p.atomicOp!"+="(i); + }).withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - p.atomicLoad.should == 0; + auto driver = justFrom(() shared { + p.atomicLoad.should == 0; - worker.advance(1.msecs); - p.atomicLoad.should == 1; + worker.advance(1.msecs); + p.atomicLoad.should == 1; - worker.advance(1.msecs); - p.atomicLoad.should == 1; + worker.advance(1.msecs); + p.atomicLoad.should == 1; - worker.advance(1.msecs); - p.atomicLoad.should == 1; + worker.advance(1.msecs); + p.atomicLoad.should == 1; - worker.advance(1.msecs); - p.atomicLoad.should == 5; + worker.advance(1.msecs); + p.atomicLoad.should == 5; - worker.timeUntilNextEvent().should == null; - }); - whenAll(throttled, driver).syncWait().assumeOk; + worker.timeUntilNextEvent().should == null; + }); + whenAll(throttled, driver).syncWait().assumeOk; - p.should == 5; + p.should == 5; } -@("throttling.debounce") -@safe unittest { - import core.time; - import concurrency.scheduler : ManualTimeWorker; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; +@("throttling.debounce") @safe +unittest { + import core.time; + import concurrency.scheduler : ManualTimeWorker; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; - shared int p = 0; - auto worker = new shared ManualTimeWorker(); - auto source = sharedStream!int; + shared int p = 0; + auto worker = new shared ManualTimeWorker(); + auto source = sharedStream!int; - auto throttled = source - .debounce(3.msecs) - .take(2) - .collect((int i) shared { p.atomicOp!"+="(i); }) - .withScheduler(worker.getScheduler); + auto throttled = source.debounce(3.msecs).take(2).collect((int i) shared { + p.atomicOp!"+="(i); + }).withScheduler(worker.getScheduler); - auto driver = justFrom(() shared { - source.emit(1); - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 3.msecs; + auto driver = justFrom(() shared { + source.emit(1); + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 1; + worker.advance(3.msecs); + p.atomicLoad.should == 1; - source.emit(2); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 3.msecs; + source.emit(2); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 3.msecs; - source.emit(3); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 3.msecs; + source.emit(3); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(1.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 2.msecs; + worker.advance(1.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 2.msecs; - source.emit(4); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 3.msecs; + source.emit(4); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 3.msecs; - worker.advance(3.msecs); - p.atomicLoad.should == 5; + worker.advance(3.msecs); + p.atomicLoad.should == 5; - worker.timeUntilNextEvent().should == null; - }); - whenAll(throttled, driver).syncWait().assumeOk; + worker.timeUntilNextEvent().should == null; + }); + whenAll(throttled, driver).syncWait().assumeOk; - p.should == 5; + p.should == 5; } -@("slide.basic") -@safe unittest { - [1,2,3,4,5,6,7].arrayStream - .slide(3) - .transform((int[] a) => a.dup) - .toList - .syncWait.value.should == [[1,2,3],[2,3,4],[3,4,5],[4,5,6],[5,6,7]]; +@("slide.basic") @safe +unittest { + [1, 2, 3, 4, 5, 6, 7].arrayStream.slide(3).transform((int[] a) => a.dup) + .toList.syncWait.value.should + == [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]]; - [1,2].arrayStream - .slide(3) - .toList - .syncWait.value.length.should == 0; + [1, 2].arrayStream.slide(3).toList.syncWait.value.length.should == 0; } -@("slide.step") -@safe unittest { - [1,2,3,4,5,6,7].arrayStream - .slide(3, 2) - .transform((int[] a) => a.dup) - .toList - .syncWait.value.should == [[1,2,3],[3,4,5],[5,6,7]]; +@("slide.step") @safe +unittest { + [1, 2, 3, 4, 5, 6, 7].arrayStream.slide(3, 2).transform((int[] a) => a.dup) + .toList.syncWait.value.should + == [[1, 2, 3], [3, 4, 5], [5, 6, 7]]; + + [1, 2].arrayStream.slide(2, 2).transform((int[] a) => a.dup).toList.syncWait + .value.should == [[1, 2]]; - [1,2].arrayStream - .slide(2, 2) - .transform((int[] a) => a.dup) - .toList - .syncWait.value.should == [[1,2]]; + [1, 2, 3, 4, 5, 6, 7].arrayStream.slide(2, 2).transform((int[] a) => a.dup) + .toList.syncWait.value.should + == [[1, 2], [3, 4], [5, 6]]; - [1,2,3,4,5,6,7].arrayStream - .slide(2, 2) - .transform((int[] a) => a.dup) - .toList - .syncWait.value.should == [[1,2],[3,4],[5,6]]; + [1, 2, 3, 4, 5, 6, 7].arrayStream.slide(2, 3).transform((int[] a) => a.dup) + .toList.syncWait.value.should == [[1, 2], [4, 5]]; - [1,2,3,4,5,6,7].arrayStream - .slide(2, 3) - .transform((int[] a) => a.dup) - .toList - .syncWait.value.should == [[1,2],[4,5]]; + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + .arrayStream.slide(2, 4).transform((int[] a) => a.dup).toList.syncWait + .value.should == [[1, 2], [5, 6], [9, 10]]; +} - [1,2,3,4,5,6,7,8,9,10].arrayStream - .slide(2, 4) - .transform((int[] a) => a.dup) - .toList - .syncWait.value.should == [[1,2],[5,6],[9,10]]; +@("toList.arrayStream") @safe +unittest { + [1, 2, 3].arrayStream.toList.syncWait.value.should == [1, 2, 3]; } -@("toList.arrayStream") -@safe unittest { - [1,2,3].arrayStream.toList.syncWait.value.should == [1,2,3]; -} - -@("toList.arrayStream.whenAll") -@safe unittest { - import concurrency.operations : withScheduler, whenAll; - import std.typecons : tuple; - auto s1 = [1,2,3].arrayStream.toList; - auto s2 = [2,3,4].arrayStream.toList; - whenAll(s1,s2).syncWait.value.should == tuple([1,2,3],[2,3,4]); +@("toList.arrayStream.whenAll") @safe +unittest { + import concurrency.operations : withScheduler, whenAll; + import std.typecons : tuple; + auto s1 = [1, 2, 3].arrayStream.toList; + auto s2 = [2, 3, 4].arrayStream.toList; + whenAll(s1, s2).syncWait.value.should == tuple([1, 2, 3], [2, 3, 4]); } @("filter") unittest { - [1,2,3,4].arrayStream - .filter((int i) => i % 2 == 0) - .toList - .syncWait - .value.should == [2,4]; + [1, 2, 3, 4].arrayStream.filter((int i) => i % 2 == 0).toList.syncWait.value + .should == [2, 4]; } @("cycle") unittest { - "-/|\\".cycleStream().take(6).toList.syncWait.value.should == "-/|\\-/"; + "-/|\\".cycleStream().take(6).toList.syncWait.value.should == "-/|\\-/"; } -@("flatmap.concat.just") -@safe unittest { - import concurrency.sender : just; - - [1,2,3].arrayStream - .flatMapConcat((int i) => just(i)) - .toList - .syncWait - .value - .should == [1,2,3]; +@("flatmap.concat.just") @safe +unittest { + import concurrency.sender : just; + + [1, 2, 3].arrayStream.flatMapConcat((int i) => just(i)).toList.syncWait + .value.should == [1, 2, 3]; } -@("flatmap.concat.thread") -@safe unittest { - import concurrency.sender : just; - import concurrency.operations : via; +@("flatmap.concat.thread") @safe +unittest { + import concurrency.sender : just; + import concurrency.operations : via; - [1,2,3].arrayStream - .flatMapConcat((int i) => just(i).via(ThreadSender())) - .toList - .syncWait - .value - .should == [1,2,3]; -} + [1, 2, 3].arrayStream.flatMapConcat((int i) => just(i).via(ThreadSender())) + .toList.syncWait.value.should == [1, 2, 3]; +} -@("flatmap.concat.error") -@safe unittest { - import concurrency.sender : just, ErrorSender; - import concurrency.operations : via; +@("flatmap.concat.error") @safe +unittest { + import concurrency.sender : just, ErrorSender; + import concurrency.operations : via; - [1,2,3].arrayStream - .flatMapConcat((int i) => ErrorSender()) - .collect(()shared{}) - .syncWait - .assumeOk - .shouldThrow(); -} - -@("flatmap.concat.thread.on.thread") -@safe unittest { - import concurrency.sender : just; - import concurrency.operations : via; - - [1,2,3].arrayStream - .flatMapConcat((int i) => just(i).via(ThreadSender())) - .toList - .via(ThreadSender()) - .syncWait - .value - .should == [1,2,3]; -} - -@("flatmap.latest.just") -@safe unittest { - import concurrency.sender : just; - - [1,2,3].arrayStream - .flatMapLatest((int i) => just(i)) - .toList - .syncWait - .value - .should == [1,2,3]; -} - -@("flatmap.latest.delay") -@safe unittest { - import concurrency.sender : just, delay; - import concurrency.operations : via, onTermination; - import core.time; - - import std.stdio; - [1,2,3].arrayStream - .flatMapLatest((int i) => just(i).via(delay(50.msecs))) - .toList - .via(ThreadSender()) - .syncWait - .value - .should == [3]; -} - -@("flatmap.latest.error") -@safe unittest { - import concurrency.sender : just, ErrorSender; - import concurrency.operations : via; - - [1,2,3].arrayStream - .flatMapLatest((int i) => ErrorSender()) - .collect(()shared{}) - .syncWait - .assumeOk - .shouldThrow(); -} - -@("flatmap.latest.justfrom.exception") -@safe unittest { - import concurrency.sender : justFrom; - - import core.time; - - 1.msecs - .intervalStream() - .flatMapLatest(() => justFrom(() { throw new Exception("oops"); })) - .collect(()shared{}) - .syncWait - .assumeOk - .shouldThrow(); -} - -@("flatmap.latest.exception") -@safe unittest { - import concurrency.sender : VoidSender; - - import core.time; - - 1.msecs - .intervalStream() - .flatMapLatest(function VoidSender(){ - throw new Exception("oops"); - }) - .collect(()shared{}) - .syncWait - .assumeOk - .shouldThrow(); -} - -@("flatmap.latest.intervalStream.overlap.delay") -@safe unittest { - import concurrency.sender : delay; - import core.time; - - 1.msecs - .intervalStream() - .take(2) - .flatMapLatest(() => 2.msecs.delay()) - .collect(()shared{}) - .syncWait - .assumeOk(); -} - -@("flatmap.latest.intervalStream.intervalStream.take") -@safe unittest { - import concurrency.sender : delay; - import concurrency.scheduler : ManualTimeWorker; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; - import core.time; - - import core.atomic; - shared int p; - - auto worker = new shared ManualTimeWorker(); - auto sender = 5.msecs - .intervalStream() - .take(2) - .flatMapLatest(() shared { - return 1.msecs - .intervalStream(true) - .take(5) - .collect(() shared { p.atomicOp!"+="(1); }); - }) - .collect(()shared{}) - .withScheduler(worker.getScheduler); - - auto driver = justFrom(() shared { - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 5.msecs; - - worker.advance(5.msecs); - p.atomicLoad.should == 1; - worker.timeUntilNextEvent().should == 1.msecs; - - worker.advance(1.msecs); - p.atomicLoad.should == 2; - - worker.advance(1.msecs); - p.atomicLoad.should == 3; + [1, 2, 3].arrayStream.flatMapConcat((int i) => ErrorSender()) + .collect(() shared {}).syncWait.assumeOk.shouldThrow(); +} - worker.advance(1.msecs); - p.atomicLoad.should == 4; +@("flatmap.concat.thread.on.thread") @safe +unittest { + import concurrency.sender : just; + import concurrency.operations : via; - worker.advance(1.msecs); - p.atomicLoad.should == 5; + [1, 2, 3].arrayStream.flatMapConcat((int i) => just(i).via(ThreadSender())) + .toList.via(ThreadSender()).syncWait.value.should == [1, 2, 3]; +} - worker.advance(1.msecs); - p.atomicLoad.should == 6; +@("flatmap.latest.just") @safe +unittest { + import concurrency.sender : just; - worker.advance(1.msecs); - p.atomicLoad.should == 7; + [1, 2, 3].arrayStream.flatMapLatest((int i) => just(i)).toList.syncWait + .value.should == [1, 2, 3]; +} - worker.advance(1.msecs); - p.atomicLoad.should == 8; +@("flatmap.latest.delay") @safe +unittest { + import concurrency.sender : just, delay; + import concurrency.operations : via, onTermination; + import core.time; - worker.advance(1.msecs); - p.atomicLoad.should == 9; + import std.stdio; + [1, 2, 3].arrayStream.flatMapLatest((int i) => just(i).via(delay(50.msecs))) + .toList.via(ThreadSender()).syncWait.value.should == [3]; +} - worker.advance(1.msecs); - p.atomicLoad.should == 10; +@("flatmap.latest.error") @safe +unittest { + import concurrency.sender : just, ErrorSender; + import concurrency.operations : via; - worker.timeUntilNextEvent().should == null; - }); - whenAll(sender, driver).syncWait().assumeOk; + [1, 2, 3].arrayStream.flatMapLatest((int i) => ErrorSender()) + .collect(() shared {}).syncWait.assumeOk.shouldThrow(); +} + +@("flatmap.latest.justfrom.exception") @safe +unittest { + import concurrency.sender : justFrom; + + import core.time; + + 1.msecs.intervalStream().flatMapLatest(() => justFrom(() { + throw new Exception("oops"); + })).collect(() shared {}).syncWait.assumeOk.shouldThrow(); +} + +@("flatmap.latest.exception") @safe +unittest { + import concurrency.sender : VoidSender; + + import core.time; + + 1.msecs.intervalStream().flatMapLatest(function VoidSender() { + throw new Exception("oops"); + }).collect(() shared {}).syncWait.assumeOk.shouldThrow(); +} + +@("flatmap.latest.intervalStream.overlap.delay") @safe +unittest { + import concurrency.sender : delay; + import core.time; + + 1.msecs.intervalStream().take(2).flatMapLatest(() => 2.msecs.delay()) + .collect(() shared {}).syncWait.assumeOk(); +} + +@("flatmap.latest.intervalStream.intervalStream.take") @safe +unittest { + import concurrency.sender : delay; + import concurrency.scheduler : ManualTimeWorker; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; + import core.time; + + import core.atomic; + shared int p; + + auto worker = new shared ManualTimeWorker(); + auto sender = 5.msecs.intervalStream().take(2).flatMapLatest(() shared { + return 1.msecs.intervalStream(true).take(5).collect(() shared { + p.atomicOp!"+="(1); + }); + }).collect(() shared {}).withScheduler(worker.getScheduler); + + auto driver = justFrom(() shared { + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 5.msecs; + + worker.advance(5.msecs); + p.atomicLoad.should == 1; + worker.timeUntilNextEvent().should == 1.msecs; + + worker.advance(1.msecs); + p.atomicLoad.should == 2; + + worker.advance(1.msecs); + p.atomicLoad.should == 3; + + worker.advance(1.msecs); + p.atomicLoad.should == 4; + + worker.advance(1.msecs); + p.atomicLoad.should == 5; - p.atomicLoad.should == 10; + worker.advance(1.msecs); + p.atomicLoad.should == 6; + + worker.advance(1.msecs); + p.atomicLoad.should == 7; + + worker.advance(1.msecs); + p.atomicLoad.should == 8; + + worker.advance(1.msecs); + p.atomicLoad.should == 9; + + worker.advance(1.msecs); + p.atomicLoad.should == 10; + + worker.timeUntilNextEvent().should == null; + }); + whenAll(sender, driver).syncWait().assumeOk; + + p.atomicLoad.should == 10; } -@("flatmap.latest.intervalStream.intervalStream.sample") -@safe unittest { - import concurrency.sender : delay; - import concurrency.scheduler : ManualTimeWorker; - import concurrency.operations : withScheduler, whenAll; - import concurrency.sender : justFrom; - import core.time; - - import core.atomic; - shared int p; - - auto worker = new shared ManualTimeWorker(); - auto sender = 5.msecs - .intervalStream() - .take(2) - .flatMapLatest(() shared { - return 1.msecs - .intervalStream(true) - .scan((int i) => i + 1, 0) - .sample(2.msecs.intervalStream()) - .take(10) - .collect((int i) shared { p.atomicOp!"+="(1); }); - }) - .collect(()shared{}) - .withScheduler(worker.getScheduler); +@("flatmap.latest.intervalStream.intervalStream.sample") @safe +unittest { + import concurrency.sender : delay; + import concurrency.scheduler : ManualTimeWorker; + import concurrency.operations : withScheduler, whenAll; + import concurrency.sender : justFrom; + import core.time; - auto driver = justFrom(() shared { - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 5.msecs; + import core.atomic; + shared int p; - worker.advance(5.msecs); - p.atomicLoad.should == 0; - worker.timeUntilNextEvent().should == 1.msecs; + auto worker = new shared ManualTimeWorker(); + auto sender = 5.msecs.intervalStream().take(2).flatMapLatest(() shared { + return 1 + .msecs.intervalStream(true).scan((int i) => i + 1, 0) + .sample(2.msecs.intervalStream()).take(10).collect((int i) shared { + p.atomicOp!"+="(1); + }); + }).collect(() shared {}).withScheduler(worker.getScheduler); - worker.advance(1.msecs); - p.atomicLoad.should == 0; + auto driver = justFrom(() shared { + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 5.msecs; - worker.advance(1.msecs); - p.atomicLoad.should == 1; + worker.advance(5.msecs); + p.atomicLoad.should == 0; + worker.timeUntilNextEvent().should == 1.msecs; - worker.advance(1.msecs); - p.atomicLoad.should == 1; + worker.advance(1.msecs); + p.atomicLoad.should == 0; - worker.advance(1.msecs); - p.atomicLoad.should == 2; + worker.advance(1.msecs); + p.atomicLoad.should == 1; - worker.advance(1.msecs); - p.atomicLoad.should == 2; + worker.advance(1.msecs); + p.atomicLoad.should == 1; - worker.advance(2.msecs); - p.atomicLoad.should == 3; + worker.advance(1.msecs); + p.atomicLoad.should == 2; - worker.advance(2.msecs); - p.atomicLoad.should == 4; + worker.advance(1.msecs); + p.atomicLoad.should == 2; - worker.advance(2.msecs); - p.atomicLoad.should == 5; + worker.advance(2.msecs); + p.atomicLoad.should == 3; - worker.advance(2.msecs); - p.atomicLoad.should == 6; + worker.advance(2.msecs); + p.atomicLoad.should == 4; - worker.advance(2.msecs); - p.atomicLoad.should == 7; + worker.advance(2.msecs); + p.atomicLoad.should == 5; - worker.advance(2.msecs); - p.atomicLoad.should == 8; + worker.advance(2.msecs); + p.atomicLoad.should == 6; - worker.advance(2.msecs); - p.atomicLoad.should == 9; + worker.advance(2.msecs); + p.atomicLoad.should == 7; - worker.advance(2.msecs); - p.atomicLoad.should == 10; + worker.advance(2.msecs); + p.atomicLoad.should == 8; - worker.advance(2.msecs); - p.atomicLoad.should == 11; + worker.advance(2.msecs); + p.atomicLoad.should == 9; - worker.advance(2.msecs); - p.atomicLoad.should == 12; + worker.advance(2.msecs); + p.atomicLoad.should == 10; - worker.timeUntilNextEvent().should == null; - }); - whenAll(sender, driver).syncWait().assumeOk; + worker.advance(2.msecs); + p.atomicLoad.should == 11; - p.atomicLoad.should == 12; + worker.advance(2.msecs); + p.atomicLoad.should == 12; + + worker.timeUntilNextEvent().should == null; + }); + whenAll(sender, driver).syncWait().assumeOk; + + p.atomicLoad.should == 12; } -@("deferStream.function") -@safe unittest { - import concurrency.stream.defer; - static auto getSender() @safe { - import concurrency.sender; - return just(1); - } - deferStream(&getSender).take(3).toList().syncWait().value.should == [1,1,1]; +@("deferStream.function") @safe +unittest { + import concurrency.stream.defer; + static auto getSender() @safe { + import concurrency.sender; + return just(1); + } + + deferStream(&getSender).take(3).toList().syncWait().value.should + == [1, 1, 1]; } -@("deferStream.callable") -@safe unittest { - import concurrency.stream.defer; - static struct S { - auto opCall() shared @safe { - import concurrency.sender; - return just(1); - } - } - shared S s; - deferStream(s).take(3).toList().syncWait().value.should == [1,1,1]; +@("deferStream.callable") @safe +unittest { + import concurrency.stream.defer; + static struct S { + auto opCall() shared @safe { + import concurrency.sender; + return just(1); + } + } + + shared S s; + deferStream(s).take(3).toList().syncWait().value.should == [1, 1, 1]; } -@("cron.timeTillNextMinute.Always") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; +@("cron.timeTillNextMinute.Always") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; - auto spec = Always().Spec; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 1.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 1.seconds; + auto spec = Always().Spec; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 1.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 1.seconds; } -@("cron.timeTillNextMinute.Exact") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; +@("cron.timeTillNextMinute.Exact") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + + auto spec = Exact(5).Spec; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 35.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 34.minutes + 1.seconds; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should + == 5.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should + == 1.seconds; +} + +@("cron.timeTillNextMinute.Every.basic") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + + auto spec = Every(5).Spec; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 5.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 4.minutes + 1.seconds; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should + == 5.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should + == 1.seconds; +} + +@("cron.timeTillNextMinute.Every.offset") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + + auto spec = Every(5, 3).Spec; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 3.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 2.minutes + 1.seconds; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should + == 3.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should + == 3.minutes + 1.seconds; +} + +@("cron.timeTillNextMinute.Each") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + + auto spec = Each([1, 15, 19, 44]).Spec; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 14.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 13.minutes + 1.seconds; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should + == 1.minutes; + spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should + == 10.minutes + 1.seconds; +} + +@("cron.timeTillNextTrigger.Always") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; - auto spec = Exact(5).Spec; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 35.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 34.minutes + 1.seconds; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should == 5.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should == 1.seconds; + auto spec = CronSpec(Spec(Always()), Spec(Always())); + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 1.minutes; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 1.seconds; } -@("cron.timeTillNextMinute.Every.basic") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; +@("cron.timeTillNextTrigger.5.over.every.hour") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; - auto spec = Every(5).Spec; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 5.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 4.minutes + 1.seconds; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should == 5.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should == 1.seconds; + auto spec = CronSpec(Spec(Always()), Spec(Exact(5))); + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should + == 35.minutes; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should + == 34.minutes + 1.seconds; } -@("cron.timeTillNextMinute.Every.offset") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; +@("cron.timeTillNextTrigger.5.over.5.hour") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + import std.datetime.timezone : UTC; + + auto spec = CronSpec(Spec(Exact(5)), Spec(Exact(5))); + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 5, 5, 0), UTC())) + .should == 24.hours; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC())) + .should == 18.hours + 35.minutes; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59), UTC())) + .should == 18.hours + 34.minutes + 1.seconds; +} - auto spec = Every(5, 3).Spec; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 3.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 2.minutes + 1.seconds; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should == 3.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should == 3.minutes + 1.seconds; -} - -@("cron.timeTillNextMinute.Each") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; - - auto spec = Each([1,15,19,44]).Spec; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 14.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 13.minutes + 1.seconds; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 0, 0))).should == 1.minutes; - spec.timeTillNextMinute(SysTime(DateTime(2018, 1, 1, 10, 4, 59))).should == 10.minutes + 1.seconds; -} - -@("cron.timeTillNextTrigger.Always") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; - - auto spec = CronSpec(Spec(Always()), Spec(Always())); - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 1.minutes; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 1.seconds; -} - -@("cron.timeTillNextTrigger.5.over.every.hour") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; - - auto spec = CronSpec(Spec(Always()), Spec(Exact(5))); - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0))).should == 35.minutes; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59))).should == 34.minutes + 1.seconds; -} - -@("cron.timeTillNextTrigger.5.over.5.hour") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; - import std.datetime.timezone : UTC; - - auto spec = CronSpec(Spec(Exact(5)), Spec(Exact(5))); - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 5, 5, 0), UTC())).should == 24.hours; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC())).should == 18.hours + 35.minutes; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59), UTC())).should == 18.hours + 34.minutes + 1.seconds; -} - -@("cron.timeTillNextTrigger.5.over.every.2.hours") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; - import std.datetime.timezone : UTC; - - auto spec = CronSpec(Spec(Every(2)), Spec(Exact(5))); - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 6, 5, 0), UTC())).should == 2.hours; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 5, 5, 0), UTC())).should == 1.hours; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC())).should == 1.hours + 35.minutes; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59), UTC())).should == 1.hours + 34.minutes + 1.seconds; -} - -@("cron.timeTillNextTrigger.every.5.over.every.2.hours") -@safe unittest { - import concurrency.stream.cron; - import core.time; - import std.datetime : SysTime, DateTime; - import std.datetime.timezone : UTC; - - auto spec = CronSpec(Spec(Every(2)), Spec(Every(5))); - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 6, 5, 0), UTC())).should == 5.minutes; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 5, 5, 0), UTC())).should == 1.hours; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC())).should == 5.minutes; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59), UTC())).should == 4.minutes + 1.seconds; - spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 59, 59), UTC())).should == 1.hours + 5.minutes + 1.seconds; +@("cron.timeTillNextTrigger.5.over.every.2.hours") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + import std.datetime.timezone : UTC; + + auto spec = CronSpec(Spec(Every(2)), Spec(Exact(5))); + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 6, 5, 0), UTC())) + .should == 2.hours; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 5, 5, 0), UTC())) + .should == 1.hours; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC())) + .should == 1.hours + 35.minutes; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59), UTC())) + .should == 1.hours + 34.minutes + 1.seconds; +} + +@("cron.timeTillNextTrigger.every.5.over.every.2.hours") @safe +unittest { + import concurrency.stream.cron; + import core.time; + import std.datetime : SysTime, DateTime; + import std.datetime.timezone : UTC; + + auto spec = CronSpec(Spec(Every(2)), Spec(Every(5))); + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 6, 5, 0), UTC())) + .should == 5.minutes; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 5, 5, 0), UTC())) + .should == 1.hours; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 0), UTC())) + .should == 5.minutes; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 30, 59), UTC())) + .should == 4.minutes + 1.seconds; + spec.timeTillNextTrigger(SysTime(DateTime(2018, 1, 1, 10, 59, 59), UTC())) + .should == 1.hours + 5.minutes + 1.seconds; } diff --git a/tests/ut/concurrency/thread.d b/tests/ut/concurrency/thread.d index 890e1a4..d7c1f1b 100644 --- a/tests/ut/concurrency/thread.d +++ b/tests/ut/concurrency/thread.d @@ -8,86 +8,97 @@ import concurrency.receiver; import unit_threaded; import core.atomic : atomicOp; -@("stdTaskPool") -@safe unittest { - import std.process : thisThreadID; - static auto fun() @trusted { - return thisThreadID; - } - auto pool = stdTaskPool(2); - - auto task = justFrom(&fun); - auto scheduledTask = task.on(pool.getScheduler); - - task.syncWait.value.should == thisThreadID; - scheduledTask.syncWait.value.shouldNotEqual(thisThreadID); +@("stdTaskPool") @safe +unittest { + import std.process : thisThreadID; + static auto fun() @trusted { + return thisThreadID; + } + + auto pool = stdTaskPool(2); + + auto task = justFrom(&fun); + auto scheduledTask = task.on(pool.getScheduler); + + task.syncWait.value.should == thisThreadID; + scheduledTask.syncWait.value.shouldNotEqual(thisThreadID); } -@("stdTaskPool.scope") -@safe unittest { - void disappearScheduler(StdTaskPoolProtoScheduler p) @safe; - void disappearSender(Sender)(Sender s) @safe; +@("stdTaskPool.scope") @safe +unittest { + void disappearScheduler(StdTaskPoolProtoScheduler p) @safe; + void disappearSender(Sender)(Sender s) @safe; - auto pool = stdTaskPool(2); + auto pool = stdTaskPool(2); - auto scheduledTask = VoidSender().on(pool.getScheduler); + auto scheduledTask = VoidSender().on(pool.getScheduler); - // ensure we can't leak the scheduler - static assert(!__traits(compiles, disappearScheduler(pool.getScheduler))); + // ensure we can't leak the scheduler + static assert(!__traits(compiles, disappearScheduler(pool.getScheduler))); - // ensure we can't leak a sender that scheduled on the scoped pool - static assert(!__traits(compiles, disappearSender(scheduledTask))); + // ensure we can't leak a sender that scheduled on the scoped pool + static assert(!__traits(compiles, disappearSender(scheduledTask))); } -@("stdTaskPool.assert") -@system unittest { - import std.exception : assertThrown; - import core.exception : AssertError; - auto pool = stdTaskPool(2); - just(42).then((int i) => assert(i == 99, "i must be 99")).via(pool.getScheduler.schedule()).syncWait.assertThrown!(AssertError)("i must be 99"); +@("stdTaskPool.assert") @system +unittest { + import std.exception : assertThrown; + import core.exception : AssertError; + auto pool = stdTaskPool(2); + just(42) + .then((int i) => assert(i == 99, "i must be 99")) + .via(pool.getScheduler.schedule()) + .syncWait + .assertThrown!(AssertError)("i must be 99"); } -@("ThreadSender.assert") -@system unittest { - import std.exception : assertThrown; - import core.exception : AssertError; - just(42).then((int i) => assert(i == 99, "i must be 99")).via(ThreadSender()).syncWait.assertThrown!(AssertError)("i must be 99"); +@("ThreadSender.assert") @system +unittest { + import std.exception : assertThrown; + import core.exception : AssertError; + just(42) + .then((int i) => assert(i == 99, "i must be 99")).via(ThreadSender()) + .syncWait.assertThrown!(AssertError)("i must be 99"); } -@("localThreadWorker.assert") -@system unittest { - import std.exception : assertThrown; - import core.exception : AssertError; - just(42).then((int i) => assert(i == 99, "i must be 99")).syncWait.assertThrown!(AssertError)("i must be 99"); +@("localThreadWorker.assert") @system +unittest { + import std.exception : assertThrown; + import core.exception : AssertError; + just(42).then((int i) => assert(i == 99, "i must be 99")).syncWait + .assertThrown!(AssertError)("i must be 99"); } -@("ThreadSender.whenAll.assert") -@system unittest { - import std.exception : assertThrown; - import core.time : msecs; - import core.exception : AssertError; - auto fail = just(42).then((int i) => assert(i == 99, "i must be 99")).via(ThreadSender()); - auto slow = delay(100.msecs); - whenAll(fail,slow).syncWait.assertThrown!(AssertError)("i must be 99"); +@("ThreadSender.whenAll.assert") @system +unittest { + import std.exception : assertThrown; + import core.time : msecs; + import core.exception : AssertError; + auto fail = just(42).then((int i) => assert(i == 99, "i must be 99")) + .via(ThreadSender()); + auto slow = delay(100.msecs); + whenAll(fail, slow).syncWait.assertThrown!(AssertError)("i must be 99"); } -@("dynamicLoadRaw.getThreadLocalExecutor") -@safe unittest { - import concurrency.utils; - dynamicLoadRaw!concurrency_getLocalThreadExecutor.should.not == null; +@("dynamicLoadRaw.getThreadLocalExecutor") @safe +unittest { + import concurrency.utils; + dynamicLoadRaw!concurrency_getLocalThreadExecutor.should.not == null; } -@("nested.intervalStream") -@safe unittest { - import core.time : msecs; - import concurrency.stream : intervalStream, take; +@("nested.intervalStream") @safe +unittest { + import core.time : msecs; + import concurrency.stream : intervalStream, take; + + static auto interval() { + return intervalStream(1.msecs).take(90).collect(() shared {}); + } - static auto interval() { - return intervalStream(1.msecs).take(90).collect(() shared {}); - } - auto sender = justFrom(() => interval().syncWait()).via(ThreadSender()); + auto sender = justFrom(() => interval().syncWait()).via(ThreadSender()); - auto d = delay(10.msecs); + auto d = delay(10.msecs); - whenAll(interval, sender, interval, sender, interval, sender, d, d, d).syncWait().assumeOk; + whenAll(interval, sender, interval, sender, interval, sender, d, d, d) + .syncWait().assumeOk; } diff --git a/tests/ut/concurrency/utils.d b/tests/ut/concurrency/utils.d index 2b015c1..a7ebfca 100644 --- a/tests/ut/concurrency/utils.d +++ b/tests/ut/concurrency/utils.d @@ -2,53 +2,56 @@ module ut.concurrency.utils; import concurrency.utils; -@("isThreadSafeFunction") -@safe unittest { - auto local = (int i) => i*2; - static assert(isThreadSafeFunction!local); +@("isThreadSafeFunction") @safe +unittest { + auto local = (int i) => i * 2; + static assert(isThreadSafeFunction!local); - int j = 42; - auto unsharedClosure = (int i) => i*j; - static assert(!isThreadSafeFunction!unsharedClosure); + int j = 42; + auto unsharedClosure = (int i) => i * j; + static assert(!isThreadSafeFunction!unsharedClosure); - shared int k = 13; - auto sharedClosure = (int i) shared => i*k; - static assert(isThreadSafeFunction!sharedClosure); + shared int k = 13; + auto sharedClosure = (int i) shared => i * k; + static assert(isThreadSafeFunction!sharedClosure); - static int system() @system { return 42; } - auto systemLocal = (int i) => i * system(); - static assert(!isThreadSafeFunction!systemLocal); + static int system() @system { + return 42; + } - auto systemSharedClosure = (int i) shared => i * system() * k; - static assert(!isThreadSafeFunction!systemSharedClosure); + auto systemLocal = (int i) => i * system(); + static assert(!isThreadSafeFunction!systemLocal); - auto trustedSharedClosure = (int i) shared @trusted => i * system() * k; - static assert(isThreadSafeFunction!trustedSharedClosure); + auto systemSharedClosure = (int i) shared => i * system() * k; + static assert(!isThreadSafeFunction!systemSharedClosure); + + auto trustedSharedClosure = (int i) shared @trusted => i * system() * k; + static assert(isThreadSafeFunction!trustedSharedClosure); } -@("isThreadSafeCallable.no.safe") -@safe unittest { - static struct S { - void opCall() shared @system { - } - } - static assert(!isThreadSafeCallable!S); +@("isThreadSafeCallable.no.safe") @safe +unittest { + static struct S { + void opCall() shared @system {} + } + + static assert(!isThreadSafeCallable!S); } -@("isThreadSafeCallable.no.shared") -@safe unittest { - static struct S { - void opCall() @safe { - } - } - static assert(!isThreadSafeCallable!S); +@("isThreadSafeCallable.no.shared") @safe +unittest { + static struct S { + void opCall() @safe {} + } + + static assert(!isThreadSafeCallable!S); } -@("isThreadSafeCallable.yes") -@safe unittest { - static struct S { - void opCall() @safe shared { - } - } - static assert(isThreadSafeCallable!(shared S)); +@("isThreadSafeCallable.yes") @safe +unittest { + static struct S { + void opCall() @safe shared {} + } + + static assert(isThreadSafeCallable!(shared S)); } diff --git a/tests/ut/concurrency/waitable.d b/tests/ut/concurrency/waitable.d index 2f52749..5e06b95 100644 --- a/tests/ut/concurrency/waitable.d +++ b/tests/ut/concurrency/waitable.d @@ -6,65 +6,74 @@ import concurrency.data.queue.waitable; import concurrency : syncWait; struct Node { - int payload; - shared Node* next; + int payload; + shared Node* next; } auto intProducer(Q)(Q q, int num) { - import concurrency.sender : just; - import concurrency.thread; - import concurrency.operations; + import concurrency.sender : just; + import concurrency.thread; + import concurrency.operations; - auto producer = q.producer(); - return just(producer, num).then((shared WaitableQueueProducer!(MPSCQueue!Node) producer, int num) shared { - foreach(i; 0..num) - producer.push(new Node(i+1)); - }).via(ThreadSender()); + auto producer = q.producer(); + return just(producer, num) + .then( + (shared WaitableQueueProducer!(MPSCQueue!Node) producer, + int num) shared { + foreach (i; 0 .. num) + producer.push(new Node(i + 1)); + }) + .via(ThreadSender()); } auto intSummer(Q)(Q q) { - import concurrency.operations : withStopToken, via; - import concurrency.thread; - import concurrency.sender : justFrom, just; - import concurrency.stoptoken : StopToken; - import core.time : msecs; + import concurrency.operations : withStopToken, via; + import concurrency.thread; + import concurrency.sender : justFrom, just; + import concurrency.stoptoken : StopToken; + import core.time : msecs; - return just(q).withStopToken((StopToken stopToken, Q q) shared @safe { - int sum = 0; - while (!stopToken.isStopRequested()) { - if (auto node = q.pop(100.msecs)) { - sum += node.payload; - } - } - while (true) { - if (auto node = q.pop(100.msecs)) - sum += node.payload; - else - break; - } - return sum; - }).via(ThreadSender()); + return just(q).withStopToken((StopToken stopToken, Q q) shared @safe { + int sum = 0; + while (!stopToken.isStopRequested()) { + if (auto node = q.pop(100.msecs)) { + sum += node.payload; + } + } + + while (true) { + if (auto node = q.pop(100.msecs)) + sum += node.payload; + else + break; + } + + return sum; + }).via(ThreadSender()); } -@("single") -@safe unittest { - import concurrency.operations : race, stopWhen; - import core.time : msecs; +@("single") @safe +unittest { + import concurrency.operations : race, stopWhen; + import core.time : msecs; - auto q = new WaitableQueue!(MPSCQueue!Node)(); - q.intSummer.stopWhen(intProducer(q, 50000)).syncWait.value.should == 1250025000; - q.empty.should == true; + auto q = new WaitableQueue!(MPSCQueue!Node)(); + q.intSummer.stopWhen(intProducer(q, 50000)).syncWait.value.should + == 1250025000; + q.empty.should == true; } -@("race") -@safe unittest { - import concurrency.operations : race, stopWhen, whenAll; +@("race") @safe +unittest { + import concurrency.operations : race, stopWhen, whenAll; - auto q = new WaitableQueue!(MPSCQueue!Node)(); - q.intSummer.stopWhen(whenAll(intProducer(q, 10000), - intProducer(q, 10000), - intProducer(q, 10000), - intProducer(q, 10000), - )).syncWait.value.should == 200020000; - q.empty.should == true; + auto q = new WaitableQueue!(MPSCQueue!Node)(); + q + .intSummer + .stopWhen(whenAll(intProducer(q, 10000), intProducer(q, 10000), + intProducer(q, 10000), intProducer(q, 10000), )) + .syncWait + .value + .should == 200020000; + q.empty.should == true; } diff --git a/tests/ut/ut_runner.d b/tests/ut/ut_runner.d index 9789ab1..6ff970d 100644 --- a/tests/ut/ut_runner.d +++ b/tests/ut/ut_runner.d @@ -1,22 +1,21 @@ import unit_threaded; -int main(string[] args) -{ - return args.runTests!( - // "ut.concurrency.fork", - "ut.concurrency.sender", - "ut.concurrency.nursery", - "ut.concurrency.operations", - "ut.concurrency.pressure", - "ut.concurrency.pressure2", - "ut.concurrency.stream", - "ut.concurrency.slist", - "ut.concurrency.scheduler", - "ut.concurrency.thread", - "ut.concurrency.utils", - "ut.concurrency.mpsc", - "ut.concurrency.waitable", - "ut.concurrency.asyncscope", - "concurrency.timingwheels", - ); +int main(string[] args) { + return args.runTests!( + // "ut.concurrency.fork", + "ut.concurrency.sender", + "ut.concurrency.nursery", + "ut.concurrency.operations", + "ut.concurrency.pressure", + "ut.concurrency.pressure2", + "ut.concurrency.stream", + "ut.concurrency.slist", + "ut.concurrency.scheduler", + "ut.concurrency.thread", + "ut.concurrency.utils", + "ut.concurrency.mpsc", + "ut.concurrency.waitable", + "ut.concurrency.asyncscope", + "concurrency.timingwheels", + ); }