Using an Rx-inspired event system

It may be too early to talk about this, but I still think a useful discussion to have. I think we can all agree that the Bukkit and Forge event systems are both verbose and somewhat outdated. I would propose that we switch to a system based on Rx, because it means less complexity and also more power.

By Rx I mean Rx-inspired, we don’t have to use RxJava. Here’s an example of listening to an event:

server.events.playerMove().on(new Callback<PlayerMoveEvent>() {
  public void call(PlayerMoveEvent event) {
    // do something here
  }
});

Things like event priority or order would be handled by filtering the stream:

server.events.playerMove().order(Order.LAST).on(...);

Because playerMove has type Observable<PlayerMoveEvent>, we can ‘map’ over it to create a new stream and do something based on that:

Observable<Vector> positions = 
  server.events.playerMove().map(new Fn<PlayerMoveEvent, Vector>() {
    public Vector call(PlayerMoveEvent event) {
      return event.getPlayer().getPosition();
    }
  });

This can of course be made far less verbose by using the new Java 8 lambdas and such.

The server events bus is just one big interface:

public interface ServerEvents {
  ...
  public Observable<PlayerMoveEvent> playerMove();
  ...
}

But of course we can add an API for additional event buses and stuff like that.

These are all really contrived examples and the names are by no means final, but I hope you get my reasons.

8 Likes

I can see this having merit. The one thing I am honestly concerned about with the event system would be priority. The Bukkit priority system was horrible and often confusing. The number of problems that were caused by everyone just jamming their crap into Priority.MONITOR was unreal, simply because people didn’t understand the system.

I like the order system that you propose with real world naming conventions assumed based on your example. Order.FIRST through Order.LAST with various ones thrown into the mix in the middle.

2 Likes

Would this have the same performance compared to the current annotation-based system?

I’m not sure how performant the current annotation-based system is, but even if there is a drop in performance it won’t be noticeable. Existing, similar tools like RxJava have very little performance drops already, and if we use that or base the design on that there shouldn’t be a problem. I know of several large companies like Netflix using RxJava and it seems to work and scale for them fine. A minecraft server doesn’t need nearly as much in terms of performance.

1 Like

Funny you should mention an Rx-based event model. I started writing rxjava-bukkit just before The Meltdown. GitHub - rmichela/rxjava-bukkit: rxjava extensions for Bukkit

It’s a full RxJava adaptation with scheduler on top of Bukkit. I was really excited about how easy it made complex event handling. I think Rx is the right way to go for event handling. You get so much awesome functionality for nearly free in Rx, and the event-driven nature of Minecraft is super conducive to Rx.

2 Likes

It’s an interesting idea, but really I don’t see the advantage here over annotation based event systems. The annotation system is fast, dynamic, and it doesn’t require you to subscribe inside of a method it just subscribes all annotated methods in a class, I’d say that’s pretty quick and painless.

I’m not saying there aren’t advantages to the system you’re proposing, but I can’t really see it – aside from cool lambda stuff. If you could create an example of a case where this is significantly more useful, that would be great though.

You’re exactly right – this new system allows for ‘cool lambda stuff’.

By the way, this by no means has to replace the existing events system. It could be hooked in so you can use annotations as well as the Rx-inspired system.

The reason it is used because of clarity and flexibility. There’s less time spent on dealing with events and more on composing streams. It’s just an alternative programming model that I think is better enough to merit a switch.

Unfortunately the cool lambda stuff can’t happen with Sponge, because it’s stuck on JRE 6

Sponge target JAVA 6,
Most people are not familiar with Java7/8 features.
Maybe rxEvenrs can be a extra lib?

There was a suggestion on the IRC channel about task chains. This would be where you create a sequence of steps and can submit it as a unit to the scheduler. That seems pretty cool.

Each stage of the task chain would pass its result to the next stage.

Java 6 is not a problem, it’s just not as clean to use as in Java 8. But the API can be identical. That said, there is no reason not to add another API (using reflection) for clean Java 6 based event listeners.

1 Like

Something like this? It combines events/observables with a threading model:
Observable positions =

Observable<Vector> positions = 
  server.events.playerMove().map(new Fn<PlayerMoveEvent, Vector>() {
    public Vector call(PlayerMoveEvent event) {
      return event.getPlayer().getPosition();
    }
  })
  .async() // threadpool
  .map(new Fn<Vector, Vector>() {
    public Vector call(Vector vector) {
      return /* do complex slow math */
    }
  })
  .sync() // main thread
  .on(new Callback<Vector>() {
    public void call(Vector vector) {
      /* apply result on the main thread */
    }
  });

I would love this. Best of both worlds. And if someone were using Java 8 (I know some big servers do), this would be the identical code:

Observable<Vector> positions = 
  server.events.playerMove().map(event -> event.getPlayer().getPosition())
  .async() // threadpool
  .map(vector-> /* do complex slow math */)
  .sync() // main thread
  .on(vector /* apply result on the main thread*/ ));

(Examples as Gist: Event & Threading model proposal (Usege example) · GitHub)

1 Like

That’s a pretty cool idea, but it’s not what I was thinking. The API seems a bit clouded. I should explain a bit more.
By default the observable is ‘executed’ using the game loop scheduler. This schedules the observable to compute in the same tick, and is a good behavior for most cases.

There should be a Scheduler interface. One for the game loop(e.g. server.scheduler.loop), one for ‘asynchronous’ computation server.scheduler.async that is outside the game loop, and finally a delayed game loop one server.scheduler.delayed.

I’ll explain. Once the middle action is done(with the slow math), we’re no longer in the same tick of the game loop! That is what the server.scheduler.loop scheduler is. Remember that at runtime the whole observable chain gets resolved, so loop will refer to the resolved tick, which is a good default behavior, but not what we want. In essense, loop always references the scheduler in the current tick. If we tried to execute something in the loop scheduler after that tick has passed, we’ll get some sort of error. Instead, we wish to use the server.scheduler.delayed scheduler, which schedules our computation for the next available tick.

If we wish to do computation in a different scheduler, that’s what we tell the observable. Your example is pretty similar, but instead becomes:

Observable<Vector> positions = 
  server.events.playerMove().map(new Fn<PlayerMoveEvent, Vector>() {
    public Vector call(PlayerMoveEvent event) {
      return event.getPlayer().getPosition();
    }
  })
  .schedule(server.scheduler.async) // for async computation
  .map(new Fn<Vector, Vector>() {
    public Vector call(Vector vector) {
      return /* do complex slow math */
    }
  })
  .schedule(server.scheduler.delayed) 
  // put it back in the game loop as early as possible
  .on(new Callback<Vector>() {
    public void call(Vector vector) {
      // apply result on the main thread
    }
  });

And I agree that with Java 8 callbacks and functions will be super sweet. :slight_smile:

1 Like

I thought about that, and yours is more generic. There might be a little trade off between clarity and genericity. The either-sync-or-async model is somewhat easier to teach (always sync when API), though in essence it’s the same.

With regards to performance, the current annotation system assumes that events are registered once and used many times.

When you register a handler, it causes internal arrays to update (including possible resizing). This is potentially slow, but it means that when the event fires, it is iterating over an array of handlers, so is fast.

With regards to MONITOR, the plan is to state what is each priority is recommended for and include some (possible enforced) rules about what you can do in each stage.

1 Like

Honestly I think this would only confuse people who want to get into this after using the annotation based system for so long.

Personally I like annotations too, I’ll gladly admit I’m no expert #NoobPowered :smirk_cat:

Indeed it is slow to add handlers and register them, but remember that this only happens once when the plugin first runs. The performance deficit should be minimal unless the plugin developer is going crazy and scheduling all the handlers at ‘game time’, so to speak.

Note that there is nothing preventing additional support for annotation-based, which just hooks into CPS-based.

Fair enough then, my lack of knowledge can live with that :stuck_out_tongue:

For me as a Plugin developer, all the above ideas sound good - each with it’s own strengths. Genereally, I would prefear a more dynamic approach with easy registering and unregistering “on the run”. I was often in the situation of only needing certain Listeners for a certain time period (during a game, while s.o. was setting something up, etc.).
My throughts about priorities: I did in fact like the Bukkit priority system. It was pretty straight forward, once you knew what “low” and “high” meant. The bigest problem was, that many plugin developers started modifying on “monitor”, which resulted in unexpected bahaviour. Maybe a new System could implement some mechanism to prevent any modifications on a monitor level.