-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Support streams of objects that need cleanup #52221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Possible related wishlist item: if |
I can see that the You generally cannot know whether a stream event is received. There is an asynchronous gap between sending and it being received, and the receiver can always cancel in that gap. The one thing that I think can be made completely certain is a synchronous single-subscription stream controller. I'd probably build an abstraction on top of that, with the ability to give feedback on whether an event is delivered or not, rather than add extra overhead to the normal stream controller. import "dart:async";
import "dart:collection";
/// Stream which allows registering events, to ensure delivery.
///
/// Creates a stream by calling [source] with a `register` function.
/// Events sent by the stream can be registered using the `register` function,
/// and if the stream is cancelled before the event has been delivered,
/// the `undelivered` function is called for each undelivered registered event.
///
/// Not all events need to be registered, but registered events *must* be sent
/// in the same order they are registered.
/// The `register` function returns the event again, to allow writing, e.g.,
/// `yield register(event);`.
///
/// Example usage:
/// ```dart
/// /// Allocates chunks of memory every second as long as anyone wants them.
/// Stream<Allocation> allocations() => ensureDelivery((register) async* {
/// while (true) {
/// await Future.delayed(const Duration(seconds: 1));
/// yield register(Memory.allocateChunk()); // Must not be forgotten
/// }
/// }, undelivered: Memory.freeChunk);
/// ```
/// If a listener cancels the allocations stream after a chunk has been
/// allocated, but before it has been delivered, then the chunk is passed
/// to `Memory.freeChunk` when the stream has been cancelled.
Stream<T> ensureDelivery<T>(Stream<T> Function(T Function(T) register) source,
{required void Function(T) undelivered}) {
final c = StreamController<T>(sync: true);
c.onListen = () {
final sentEvents = Queue<T>();
final subscription = source((T event) {
sentEvents.add(event);
return event;
}).listen(null);
c
..onPause = subscription.pause
..onResume = subscription.resume
..onCancel = () => subscription.cancel().whenComplete(() {
while (sentEvents.isNotEmpty) {
undelivered(sentEvents.removeFirst());
}
});
subscription
..onData((T value) {
if (sentEvents.isNotEmpty && identical(value, sentEvents.first)) {
sentEvents.removeFirst();
}
c.add(value);
})
..onError(c.addError)
..onDone(c.close);
};
return c.stream;
} |
I think I oversimplified my reduced example so that it no longer clearly shows the leak, and probably cannot leak as-is on VM (but does leak on dart2js due to #48749) so long as the consumer behaves like import 'package:async/async.dart';
class MyAllocatedResource {
static int unclosedInstances = 0;
MyAllocatedResource() {
++unclosedInstances;
}
void close() {
--unclosedInstances;
}
}
Stream<MyAllocatedResource> multiOpen() async* {
yield MyAllocatedResource();
}
void main() async {
await for (final resource in StreamGroup.merge([
multiOpen(),
multiOpen(),
])) {
try {
break;
} finally {
resource.close();
}
}
print('unclosedInstances: ${MyAllocatedResource.unclosedInstances}'); // 1
} where an event is dropped because both async generators get to their |
Also in case it helps, on Discord user @abitofevrything graciously surveyed the current behavior and deviations from the spec on VM and dart2js: https://gist.github.com/abitofevrything/56e5d0b7b5c3bc25462bca5f334c4cb2 |
Even with a correctly implemented That's an inherent design in the That's not easily changed. I wouldn't even know where to start. It is a place where So maybe we should allow Just for the record, as specified (I hope, that was the intent), a If the subscription is paused after the synchronous event delivery returns, then the The function can also deliver the event asynchronously, and then it must suspend the |
Uh oh!
There was an error while loading. Please reload this page.
Streams are great for primitives and simple data, but they can be hostile against objects that need cleanup. Parts of the stream contract allow buffering and discarding events, which can leave a gap in the chain of custody and let objects that need cleanup leak. While such objects could be served using a callback scheme or a custom controller/sink/stream trinity, it becomes unfortunate to reimplement so much and lose
async*
/await for
syntax.Example use case:
In cases where the event is discarded, the resource leaks.
This can be worked around without abandoning the use of
Stream
, but it would be nice for this use case to be first-class. Workarounds are also complicated by bugs/inconsistencies in theasync*
implementation.My current workaround looks like this. I may try to punt the allocation into
onListen
to try to avoid theyield*
pitfall described below, but differences in error handling (try
/catch
vs. forwarding toStream
) may hinder that further.Two issues in particular complicate workarounds:
yield
on a cancelledasync*
behaves differently on VM and weblanguage/async_star/throw_in_catch_test
to correct async* semantics caused dart2js failure. #48749 (web)yield
when associated stream is cancelled #49451 (VM; closed, but same as the above)StreamController
to clearonCancel
and deliver objects atomically.yield*
acts like return, it can drop the RHS stream without ever listening to it. While this is less egregious than a full-on leak, it does not seem to adhere to the spec and complicates the syncStreamController
workaround as the case where the entire stream is dropped needs to be handled separately.yield*
of the aboveStreamController
in atry
/finally
, or deferring allocation untilonListen
.Related use cases:
Possibly related:
async*
#48695Versions/platforms:
The text was updated successfully, but these errors were encountered: