renatoathaydes / actors

Actor Model library for Dart.
BSD 2-Clause "Simplified" License
47 stars 4 forks source link

Question: How to carry external state (timezone info or a database connection) into the actor? #15

Closed thumbert closed 1 year ago

thumbert commented 1 year ago

Hi,

I'm exploring your package. Looks great. I'm running into an issue and want to understand if it's normal or there is a work around. See the example below. I'm using the package 'timezone'.

class DaySkipper with Handler<int,dynamic> {
  @override
  FutureOr handle(int message) {
    // initializeTimeZones();
    var dt = TZDateTime(getLocation('America/New_York'), 2023);
    print(dt.add(Duration(days: message)));
  }
}

Future<void> main() async {
  initializeTimeZones();

  final actor = Actor(DaySkipper());
  await actor.send(3);
  await actor.close();
}

This code will fail because the the actor doesn't see the timezone database initialized although I do it in the main thread. If I explicitly initialize it in the handle method, it works. Is this how it should work?

Related to this situation, if I have a database connection and I open it in the main thread, from inside the actor the database connection does not appear to be open. It doesn't make sense to open and close the connection on each actor. How to deal with this situation?

Thanks for any suggestions, Tony

renatoathaydes commented 1 year ago

Is this how it should work?

Yes, because the standard Actor always runs in a Dart Isolate. The name Isolate comes from the fact that it runs on an almost completely isolated environment: there's no memory sharing, no global state sharing, between isolates. The only way to share information is by passing messages around.

In your case, you should ensure the initialization method is called on all handlers. I believe you can do that in the constructor to avoid having to check on every handle invocation.

Alternatively, you could try to make the timezone itself part of the message you send to the actor.

renatoathaydes commented 1 year ago

It doesn't make sense to open and close the connection on each actor.

Why doesn't it make sense? An Actor seems to be a great place to keep a database connection pool. It has a lifetime, just like a connection pool.

Perhaps what does not make sense is to start a large number of Actors, each with its own connection pool. But that's not how Actors in Dart are supposed to be used... you probably shouldn't start more Actors than you have CPUs available, unless the actors are indepdendent of each other, in which case that would be fine. But if an Actor holds a stateful object like a connection pool, then you want to have only a few open at any given time: and that's ok. Perhaps use an ActorGroup in such case.

thumbert commented 1 year ago

Thanks. I will explore some more.

thumbert commented 1 year ago

Hi,

Here's a minimal example using a database connection that I can't make it to work. I use mongodb via the package mongo_dart.

class FileHandler with Handler<String, int> {
  FileHandler();
  late Db db;
  @override
  Future<int> handle(String dateTime) async {
    await Future.delayed(Duration(seconds: 1));
    var x = <String, dynamic>{
      'datetime': dateTime,
      'values': List.generate(100, (index) => Random().nextInt(100)),
    };
    var collection = db.collection('actors');
    await collection.insert(x);
    print('Finished with $dateTime');
    return 0;
  }
}

Future<void> main() async {
  var withActor = true;

  var db = Db('mongodb://127.0.0.1/test');
  await db.open();
  var handler = FileHandler()..db = db;

  if (withActor) {
    final actor = Actor(handler);
    await actor.send('2023-01-01');
    await actor.close();
  } else {
    await handler.handle('2023-01-01');
  }

  await db.close();
}

I get the error message

Unhandled exception:
Invalid argument(s): Illegal argument in isolate message: (object extends NativeWrapper - Library:'dart:io' Class: _NativeSocket@14069316)
#0      Isolate._spawnFunction (dart:isolate-patch/isolate_patch.dart:399:25)
#1      Isolate.spawn (dart:isolate-patch/isolate_patch.dart:379:7)
#2      ActorImpl.spawn (package:actors/src/isolate/isolate_actor.dart:23:20)
#3      new Actor (package:actors/src/actors_base.dart:122:16)
#4      main (file:///.../test/db/update_dbs_test.dart:60:19)
<asynchronous suspension>

If I just use the handler directly, the code works fine.

What do I need to do to make my simple example work?

Thanks, Tony

renatoathaydes commented 1 year ago

The Db object must be created in the Handler's constructor, as I had said before. You cannot "set" it fom the main Isolate as you're doing.

The simple rule is: never keep an instance of a Handler around. The pattern should always be:

final actor = Actor(< create handler here >);
renatoathaydes commented 1 year ago

Try something like this:

class FileHandler with Handler<String, int> {
  Future<Db> _init() async {
    var db = Db('mongodb://127.0.0.1/test');
    await db.open();
    return db;
  });

  Db? _db;

  FileHandler();
  @override
  Future<int> handle(String dateTime) async {
    var db = _db;
    if (db == null) {
      db = await _init();
      _db = db;
    }
    final db = await _db;
    await Future.delayed(Duration(seconds: 1));
    var x = <String, dynamic>{
      'datetime': dateTime,
      'values': List.generate(100, (index) => Random().nextInt(100)),
    };
    var collection = db.collection('actors');
    await collection.insert(x);
    print('Finished with $dateTime');
    return 0;
  }
}

Future<void> main() async {
  var withActor = true;

  if (withActor) {
    final actor = Actor(FileHandler());
    await actor.send('2023-01-01');
    await actor.close();
  } else {
    await handler.handle('2023-01-01');
  }

  await db.close();
}

This lazily initializes the Db on first usage, avoiding initializing it outside the Actor's Isolate.

thumbert commented 1 year ago

Many thanks for the example. I finally get it. Not sure why I wasn't understanding you the first time. I've also noticed the close method for a Handler, where I can close my db object. Very exciting!