Using an Rx-inspired event system

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.

I don’t think you know how this works. It is possible to ‘register’ and ‘unregister’ very easily with a filter:

server.events.playerMove()
.filter(new Fn<PlayerMoveEvent, Boolean>() {
  public Boolean call(PlayerMoveEvent event) {
    return event.getPosition().getX() > 0;
  }
}).map(new Fn<PlayerMoveEvent, Vector>() {
  public Vector call(PlayerMoveEvent event) {
    return event.getPlayer().getPosition();
  }
}).forEach(new Fn<Event, Void>(){
  public Void call(Vector v) {
    doSomething(v);
  }
});

Filter takes a function that returns a boolean and if that function returns true for a given value, the filter passes. The above filter only ‘registered’ when a move location’s x value is greater than zero. In that way there is no need for ‘registering’ or ‘unregistering’.

Thanks for the clarification! I actually did and do know what you mean and totally agree. That’s what I was trying to express with the more dynamic system of my above post. Now rereading it I can see where the confusion comes from - sorry about not being more specific about it. That might be due to the fact that English is not my first language and/or it being 5:45 am right now.

@kenzierocks

Yeah, you can do “the cool lambda stuff” with jdk6. A Lambda expression is just syntax sugar for a functional interface. Java 8 lambdas just allow less typing. In java6, you would just create a new anonymous class and override the method of the interface.

The real reason to switch to Java 8 is interfaces with a default implementation. Which sk89q has said that he wants this functionality… Okay ? So switch to java8 then. No big deal. Servers have to upgrade. That’s not a problem.

Also, IntelliJ IDEA hides all the ugly new Interface() blah stuff for you.