// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT License. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Subjects; namespace System.Reactive.Linq.ObservableImpl { internal sealed class GroupByUntil : Producer, GroupByUntil._> { private readonly IObservable _source; private readonly Func _keySelector; private readonly Func _elementSelector; private readonly Func, IObservable> _durationSelector; private readonly int? _capacity; private readonly IEqualityComparer _comparer; public GroupByUntil(IObservable source, Func keySelector, Func elementSelector, Func, IObservable> durationSelector, int? capacity, IEqualityComparer comparer) { _source = source; _keySelector = keySelector; _elementSelector = elementSelector; _durationSelector = durationSelector; _capacity = capacity; _comparer = comparer; } protected override _ CreateSink(IObserver> observer) => new(this, observer); protected override void Run(_ sink) => sink.Run(_source); internal sealed class _ : Sink> { private readonly object _gate = new(); private readonly object _nullGate = new(); private readonly CompositeDisposable _groupDisposable = new(); private readonly RefCountDisposable _refCountDisposable; private readonly Map> _map; private readonly Func _keySelector; private readonly Func _elementSelector; private readonly Func, IObservable> _durationSelector; private ISubject? _null; public _(GroupByUntil parent, IObserver> observer) : base(observer) { _refCountDisposable = new RefCountDisposable(_groupDisposable); _map = new Map>(parent._capacity, parent._comparer); _keySelector = parent._keySelector; _elementSelector = parent._elementSelector; _durationSelector = parent._durationSelector; } public override void Run(IObservable source) { _groupDisposable.Add(source.SubscribeSafe(this)); SetUpstream(_refCountDisposable); } private ISubject NewSubject() { var sub = new Subject(); return Subject.Create(new AsyncLockObserver(sub, new Concurrency.AsyncLock()), sub); } public override void OnNext(TSource value) { TKey key; try { key = _keySelector(value); } catch (Exception exception) { Error(exception); return; } var fireNewMapEntry = false; ISubject writer; try { // // Note: The box instruction in the IL will be erased by the JIT in case T is // a value type. In fact, the whole if block will go away and we'll end // up with nothing but the GetOrAdd call below. // // See GroupBy for more information and confirmation of this fact using // the SOS debugger extension. // if (key == null) { lock (_nullGate) { if (_null == null) { _null = NewSubject(); fireNewMapEntry = true; } writer = _null; } } else { writer = _map.GetOrAdd(key, NewSubject, out fireNewMapEntry); } } catch (Exception exception) { Error(exception); return; } if (fireNewMapEntry) { var group = new GroupedObservable(key, writer, _refCountDisposable); var durationGroup = new GroupedObservable(key, writer); IObservable duration; try { duration = _durationSelector(durationGroup); } catch (Exception exception) { Error(exception); return; } lock (_gate) { ForwardOnNext(group); } var durationObserver = new DurationObserver(this, key, writer); _groupDisposable.Add(durationObserver); durationObserver.SetResource(duration.SubscribeSafe(durationObserver)); } TElement element; try { element = _elementSelector(value); } catch (Exception exception) { Error(exception); return; } // // ISSUE: Rx v1.x shipped without proper handling of the case where the duration // sequence fires concurrently with the OnNext code path here. In such a // case, the subject can be completed before we get a chance to send out // a new element. However, a resurrected group for the same key won't get // to see the element either. To guard against this case, we'd have to // check whether the OnNext call below lost the race, and resurrect a new // group if needed. Unfortunately, this complicates matters when the // duration selector triggers synchronously (e.g. Return or Empty), which // causes the group to terminate immediately. We should not get stuck in // this case, repeatedly trying to resurrect a group that always ends // before we can send the element into it. Also, users may expect this // base case to mean no elements will ever be produced, so sending the // element into the group before starting the duration sequence may not // be a good idea either. For the time being, we'll leave this as-is and // revisit the behavior for vNext. Nonetheless, we'll add synchronization // to ensure no concurrent calls to the subject are made. // writer.OnNext(element); } private sealed class DurationObserver : SafeObserver { private readonly _ _parent; private readonly TKey _key; private readonly ISubject _writer; public DurationObserver(_ parent, TKey key, ISubject writer) { _parent = parent; _key = key; _writer = writer; } public override void OnNext(TDuration value) { OnCompleted(); } public override void OnError(Exception error) { _parent.Error(error); Dispose(); } public override void OnCompleted() { if (_key == null) { ISubject? @null; lock (_parent._nullGate) { @null = _parent._null; _parent._null = null; } @null?.OnCompleted(); } else { if (_parent._map.Remove(_key)) { _writer.OnCompleted(); } } _parent._groupDisposable.Remove(this); } } public override void OnError(Exception error) { Error(error); } public override void OnCompleted() { // // NOTE: A race with OnCompleted triggered by a duration selector is fine when // using Subject. It will transition into a terminal state, making one // of the two calls a no-op by swapping in a DoneObserver. // ISubject? @null; lock (_nullGate) { @null = _null; } @null?.OnCompleted(); foreach (var w in _map.Values) { w.OnCompleted(); } lock (_gate) { ForwardOnCompleted(); } } private void Error(Exception exception) { // // NOTE: A race with OnCompleted triggered by a duration selector is fine when // using Subject. It will transition into a terminal state, making one // of the two calls a no-op by swapping in a DoneObserver. // ISubject? @null; lock (_nullGate) { @null = _null; } @null?.OnError(exception); foreach (var w in _map.Values) { w.OnError(exception); } lock (_gate) { ForwardOnError(exception); } } } } }