using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace Wabbajack.Common.CSP { /// /// An almost 1:1 port of Clojure's core.async channels /// public class ManyToManyChannel : AChannel { public const int MAX_QUEUE_SIZE = 1024; private RingBuffer>> _takes = new RingBuffer>>(8); private RingBuffer<(Handler>, TIn)> _puts = new RingBuffer<(Handler>, TIn)>(8); private IBuffer _buf; private Func, TIn, bool> _add; private Action> _finalize; private Func _converter; volatile bool _isClosed = false; public ManyToManyChannel(Func converter) { _buf = null; _add = null; _finalize = null; _converter = converter; } public ManyToManyChannel(Func converter, Func, TIn, bool> add, Action> finalize, IBuffer buffer) { _buf = buffer; _add = add; _finalize = finalize; _converter = converter; } private static bool IsActiveTake(Handler> handler) { return handler.IsActive; } private static bool IsActivePut((Handler>, TIn) input) { return input.Item1.IsActive; } /// /// Tries to put a put into the channel /// /// /// /// (result_type, w) public override (AsyncResult, bool) Put(TIn val, Handler> handler) { Monitor.Enter(this); if (_isClosed) { Monitor.Exit(this); return (AsyncResult.Completed, false); } if (_buf != null && !_buf.IsFull && !_takes.IsEmpty) { var put_cb = LockIfActiveCommit(handler); if (put_cb != null) { var is_done = _add(_buf, val); if (!_buf.IsEmpty) { var take_cbs = GetTakersForBuffer(); if (is_done) Abort(); Monitor.Exit(this); foreach (var action in take_cbs) { Task.Run(action); } return (AsyncResult.Completed, true); } if (is_done) Abort(); Monitor.Exit(this); return (AsyncResult.Closed, false); } Monitor.Exit(this); return (AsyncResult.Canceled, false); } var (put_cb2, take_cb) = GetCallbacks(handler, _takes); if (put_cb2 != null && take_cb != null) { Monitor.Exit(this); Task.Run(() => take_cb(true, _converter(val))); return (AsyncResult.Completed, true); } if (_buf != null && !_buf.IsFull) { if (LockIfActiveCommit(handler) != null) { if (_add(_buf, val)) { Abort(); } Monitor.Exit(this); return (AsyncResult.Completed, true); } Monitor.Exit(this); return (AsyncResult.Canceled, true); } if (handler.IsActive && handler.IsBlockable) { if (_puts.Length >= MAX_QUEUE_SIZE) { Monitor.Exit(this); throw new TooManyHanldersException(); } _puts.Unshift((handler, val)); } Monitor.Exit(this); return (AsyncResult.Enqueued, true); } public override (AsyncResult, TOut) Take(Handler> handler) { Monitor.Enter(this); Cleanup(); if (_buf != null && !_buf.IsEmpty) { var take_cb = LockIfActiveCommit(handler); if (take_cb != null) { var val = _buf.Remove(); var (is_done, cbs) = GetPuttersForBuffer(); if (is_done) Abort(); Monitor.Exit(this); foreach (var cb in cbs) Task.Run(() => cb(true)); return (AsyncResult.Completed, val); } Monitor.Exit(this); return (AsyncResult.Canceled, default); } var (take_cb2, put_cb, val2, found) = FindMatchingPut(handler); if (take_cb2 != null && put_cb != null) { Monitor.Exit(this); Task.Run(() => put_cb(true)); return (AsyncResult.Completed, _converter(val2)); } if (_isClosed) { if (_buf != null && found) _add(_buf, val2); var has_val = _buf != null && !_buf.IsEmpty; var take_cb3 = LockIfActiveCommit(handler); if (take_cb3 != null) { var val = has_val ? _buf.Remove() : default; Monitor.Exit(this); return has_val ? (AsyncResult.Completed, val) : (AsyncResult.Closed, default); } Monitor.Exit(this); return (AsyncResult.Closed, default); } if (handler.IsBlockable) { if (_takes.Length >= MAX_QUEUE_SIZE) { Monitor.Exit(this); throw new TooManyHanldersException(); } _takes.Unshift(handler); } Monitor.Exit(this); return (AsyncResult.Enqueued, default); } public override bool IsClosed => _isClosed; public override void Close() { Monitor.Enter(this); Cleanup(); if (_isClosed) { Monitor.Exit(this); return; } _isClosed = true; if (_buf != null && _puts.IsEmpty) _finalize(_buf); var cbs = _buf == null? new List() : GetTakersForBuffer(); while (!_takes.IsEmpty) { var take_cb = LockIfActiveCommit(_takes.Pop()); if (take_cb != null) cbs.Add(() => take_cb(false, default)); } Monitor.Exit(this); foreach (var cb in cbs) Task.Run(cb); } private (Action, Action, TIn, bool) FindMatchingPut(Handler> handler) { while (!_puts.IsEmpty) { var (found, val) = _puts.Peek(); var (handler_cb, put_cb, handler_active, put_active) = LockIfActiveCommit(handler, found); if (handler_active && put_active) { _puts.Pop(); return (handler_cb, put_cb, val, true); } if (!put_active) { _puts.Pop(); continue; } return (null, null, default, false); } return (null, null, default, false); } private (bool, List>) GetPuttersForBuffer() { List> acc = new List>(); while (!_puts.IsEmpty) { var (putter, val) = _puts.Pop(); var cb = LockIfActiveCommit(putter); if (cb != null) { acc.Add(cb); } var is_done = _add(_buf, val); if (is_done || _buf.IsFull || _puts.IsEmpty) return (is_done, acc); } return (false, acc); } private void Cleanup() { _takes.Cleanup(IsActiveTake); _puts.Cleanup(IsActivePut); } private (T1, T2) GetCallbacks(Handler handler, RingBuffer> queue) { while (!queue.IsEmpty) { var found = queue.Peek(); var (handler_cb, found_cb, handler_valid, found_valid) = LockIfActiveCommit(handler, found); if (handler_valid && found_valid) { queue.Pop(); return (handler_cb, found_cb); } if (handler_valid) { queue.Pop(); } else { return (default, default); } } return (default, default); } private void Abort() { while (!_puts.IsEmpty) { var (handler, val) = _puts.Pop(); var put_cb = LockIfActiveCommit(handler); if (put_cb != null) { Task.Run(() => put_cb(false)); } } _puts.Cleanup(x => false); Close(); } private List GetTakersForBuffer() { List ret = new List(); while (!_buf.IsEmpty && !_takes.IsEmpty) { var taker = _takes.Pop(); var take_cp = LockIfActiveCommit(taker); if (take_cp != null) { var val = _buf.Remove(); ret.Add(() => take_cp(true, val)); } } return ret; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static T LockIfActiveCommit(Handler handler) { lock (handler) { return handler.IsActive ? handler.Commit() : default; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (T1, T2, bool, bool) LockIfActiveCommit(Handler handler1, Handler handler2) { if (handler1.LockId < handler2.LockId) { Monitor.Enter(handler1); Monitor.Enter(handler2); } else { Monitor.Enter(handler2); Monitor.Enter(handler1); } if (handler1.IsActive && handler2.IsActive) { var ret1 = (handler1.Commit(), handler2.Commit(), true, true); Monitor.Exit(handler1); Monitor.Exit(handler2); return ret1; } var ret2 = (default(T1), default(T2), handler1.IsActive, handler2.IsActive); Monitor.Exit(handler1); Monitor.Exit(handler2); return ret2; } public class TooManyHanldersException : Exception { public override string ToString() { return $"No more than {MAX_QUEUE_SIZE} pending operations allowed on a single channel."; } } } }