d-markey / squadron

Multithreading and worker thread pool for Dart / Flutter, to offload CPU-bound and heavy I/O tasks to Isolate or Web Worker threads.
https://pub.dev/packages/squadron
MIT License
79 stars 0 forks source link

Will every worker in a pool share the same resource ? #29

Closed sabin26 closed 9 months ago

sabin26 commented 9 months ago

I was wondering if the workers in a pool share the same client, or each worker will have its own client in the following code. If they do not share the same client, then I will have to manually call the closeConnection() method by listening when the worker is removed in the workerpool in which case the callback should be called before the worker is actually removed.

SquadronService(baseUrl: '/services')
class HttpService {
  HttpService() : _client = Client();

  late final BaseClient _client;

  @SquadronMethod()
  Future<void> closeConnection() async {
    _client.close();
  }
}

Also, will the client be initialized when a worker pool is started or will it defer until the worker is started?

d-markey commented 9 months ago

Resources are per worker, this is enforced by Dart's memory model (remember, it relies on isolation :-) ).

Your client will be initialized when the worker is started.

But there's currently no way to programmatically dispose of a service when its worker is stopped. I suppose you could use Dart's Finalizer class to do the same, until Squadron provides a way to automatically dispose of resources allocated by the service class when the worker is stopped.

My 2 cents: in your implementation, late has no effect because the client is initialized at construction time. late is only a hint for a non-nullable field so that Dart does not complain that a field is not initialized. It will also allow for setting the field in your code even though it's marked final. You should try to avoid resorting to late in your code because it can lead to runtime errors if proper initialization is not carefully respected. Here, you're safe because _client will be set before the class constructor body gets executed. You could also have written final BaseClient _client = Client();, the effect will be the same.

sabin26 commented 9 months ago

Modified my actual implementation to remove the late keyword as per your suggestion and will try using Finalizer class tomorrow. Thank you ! I got the answer to my question. Closing the issue for now :)

sabin26 commented 9 months ago

I tried to use Finalizer class but could not confirm that its callback is actually being triggered. I started and stopped worker multiple times and kept the application at idle for some decent time to test this.

@SquadronService(baseUrl: '/services')
class HttpService {
  HttpService() : _client = Client() {
    _finalizer.attach(this, _client, detach: this);
  }

  final Client _client;

  static final _finalizer = Finalizer<Client>((final baseClient) {
    print('I am closing the connection...'); // I do not see this on the console when debugging
    baseClient.close();
  });

  @SquadronMethod()
  Future<void> closeConnection() async {
    _client.close();
    _finalizer.detach(this); // I tested both conditions: with and without this line.
  }
}

Could it be that it is used inside a worker and needs to be implemented differently? Maybe the variable _finalizer is itself garbage collected?

d-markey commented 9 months ago

Hello, that's the idea indeed but I won't be able to help you on this one... I've played with finalizers just a little bit and remember it is not straightforward. Also, I used them in the main isolate only, but yours are running in background threads that may be killed by Squadron -- that plus the fact that "No promises are made that the callback will ever be called" (https://api.flutter.dev/flutter/dart-core/Finalizer-class.html) so I really don't know if this behavior is normal or not!

Also, if you're testing on Web, keep in mind that "print()" functions called by the worker will print to the browser's console and the browser console only. It won't be visible in your debugger output.

sabin26 commented 9 months ago

I also read the docs on Finalizer and it does not guarentee that callback will be called. I looked into NativeFinalizer which guarentees it but its only for native resources via ffi. Maybe i will initialize and close the http client per request and not keep it hanging around.

I tested it on windows native platform (not web).

d-markey commented 9 months ago

I've added ServiceInstaller to support this use case.

If your service needs initialization just after the worker thread has started, implement ServiceInstaller.install(). If it needs to cleanup resources just before the worker thread is stopped, implement ServiceInstaller.uninstall().

squadron_builder does not support this yet, so don't jump on that feature right now, but it's coming. I'll keep you posted when squadron_builder is ready (hopefully next week).

sabin26 commented 9 months ago

If we are initializing resources separately with ServiceInstaller, shouldn't the variables be marked with late keyword?

d-markey commented 9 months ago

I've just ran a test and squadron_builder needs no change to support the new ServiceInstaller interface. So, it's working out of the box!

Regarding resource initialization, I would only implement the install() method if setting up the service requires asynchronous operations, that cannot be awaited in the constructor. Otherwise, I'd use the constructor for initialization. In your case, I'd stick with the constructor and avoid the late keyword.

What's most interesting is the uninstall() method where you'll be able to dispose of resources acquired by the service.

Have fun!

d-markey commented 9 months ago

Hello,

I realize I've been a bit harsh against the late keyword. It's not only syntactic sugar (to tell Dart you'll be handling initialization in situations where the code analyzer cannot verify that you do), but it also has lazy semantics eg. to defer initialization until the result is effectively used.

I would still avoid having the declaration and the initialization at two different places. If you can initialize the field on the same line where it is declared, it makes it easy to assess that field initialization will never be forgotten.


class HttpService {
  HttpService();

  late final BaseClient _client = Client(); // _client will not be initialized before the first time it is used
}
sabin26 commented 9 months ago

I worked around to use a single http client per request. No more late initialization, no more cleanup to perform (when the request is completed, client is closed). There will be a new client for new request.