luna-rs / luna

A scalable and efficient Runescape server targeting revision #377
MIT License
155 stars 41 forks source link

Dynamically reloading plugins, or "hotfixing" #13

Closed lare96 closed 8 years ago

lare96 commented 8 years ago

Plugins are interpreted on startup and wrapped inside EventListeners, which are then added to EventListenerPipelines. Those pipelines are notified of any Events which are then subsequently sent through the pipeline to be intercepted by each listener (unless the event is terminated by one of the listeners before the traversal can complete).

With that said, all that would have to be done would be to discard all pipelines and re-interpret all of the plugins which would then reconstruct the pipelines with the newest plugin code.

Last time I tried it, there seemed to be some sort of classpath issue with the Scala compiler/interpreter. I never really got around to looking into it, but this is a feature that I really want added in the near future.

lare96 commented 8 years ago

I actually forgot that I had this piece of code in CommandMessageReader

// Has to be done in Java because of classpath conflicts.
if (name.equals("reloadplugins")) {
    // TODO: implement some sort of additional cache that will allow for 
    // "hot fixes" without potentially interrupting gameplay. for now just clear 
    // plugin cache and do async reload of bootstrap
    LunaContext ctx = player.getContext();
    PluginManager plugins = ctx.getPlugins();
    GameService service = ctx.getService();
    plugins.clear();
    service.execute(new PluginBootstrap(ctx));
    return null;
}

I tested it and it works okay, once the plugins reload.

The actual issue isn't the classpath issue. In order to do this dynamically it has to be asynchronous, but once you dispose of all of the pipelines you have to wait for the asynchronous computation to complete before any code that uses plugins works again.

Solution -> Set some sort of global flag (needs to be AtomicBoolean or externally synchronized) indicating that a hotfix is happening, ensuring that multiple hotfixes cannot happen simeltaneously -> Begin re-interpreting plugins asynchronously, do not dispose of pipelines -> Use ListenableFuture (returned by GameService.execute) to execute a chunk of code once the interpretation has finished -> The code should have a reference to the new pipelines from the updated plugins. All that would have to be done is to use GameService.sync to execute code on the game thread, that would replace all of the old pipelines with the new ones in a single, synchronous, sweep -> Log indication of the hotfix being completed

Potential problem A server full of content is going to have tons, probably even over 100 plugins. Why should every single plugin have to be reloaded just to make a quick hotfix in a single plugin?

I could track when each plugin was last modified, and then only reload the ones that were updated. But then how do I know which EventListeners to discard from the pipeline? And when the listener is discarded, the order of the listeners in the pipeline will be different when it's added again which (might?) be an issue.

So with that said I'll probably just stick with reloading all of the plugins at once because it's way simpler and hotfixes aren't implemented that often for it to be an issue.

lare96 commented 8 years ago

Works perfectly! Done in commits {ff505ff069e32a17531523ad0299373aeaf99c3d, 07017e6a3475a109efc1b505100c23d39e742ef8, c542f65a3c65bea9c9a33e2a67da3f74470c2d41, and 06ba2764f86c88163ec8e9fd9ed09f547afb59e1, 84e2c5e492f162e671191f21196b2880a7475f53, 6f2a6a02e1d105838086323a29fc3020bda28b40}

Still going to be looking for a way to only hotfix specific plugins, though I'll be making a separate issue for that