Resource TaskChain - Proper Async Operations and more!

Discussion in 'Spigot Plugin Development' started by Aikar, Oct 30, 2016.

  1. asyncParallel is initially what lead to the queue idea.
    For that, i would go with this style:

    .asyncParallel(FirstTask<T> tasks...)

    style

    so:

    .asyncParallel((foo) -> { doStuff }, (foo) -> { do stuff }, (foo) -> { doStuff })

    One step in a chain pipeline should expect to complete before a next step would run.

    Now on sync -- this wont work. Sync tasks are expected to run on the main thread. You can only do 1 thing on a thread at a time, so can't run it in parallel.

    Now, you could block the main and suspend it while all tasks in the queue run async, but that will corrupt the API Design of TaskChain in that a sync task is expected to be API safe, which it no longer will be in that design.

    I think an .async(task1, task2, task3) style (dropping the parallel word to avoid new terminology to understand)


    As for TPS oriented method, TaskChain is no longer Minecraft bound. TC Core has no concept of TPS.

    an syncQueue that only processes 1 at a time on main, and provide a TaskChain.backOffQueue(5 /* game units*/) or backOffQueue(5, TimeUnit.SECONDS);

    This would provide the same results.

    You would need to do your for(;;) 3x iteration before hand to build a queue in the previous task ,then pass it to a queue task to process.

    This would accomplish your goal.

    the asyncQueue would be configurable concurrency, default to processors yeah.
     
    #21 Aikar, Nov 3, 2016
    Last edited: Nov 3, 2016
  2. @Aikar
    It uses ticks for the delay (or seconds with TimeUnit). That's a concept of it. Unless you plan on removing tick based delay?

    Also, building the queue beforehand and using a 1 tick delay isn't really the same. Forgetting the massive overhead from queuing each block change as a separate task, executing one task each tick would be incredibly slow. You could possibly leave it up to the developer to decide how long to run tasks for e.g. they could allocate 20ms each tick (or second?) for running tasks.

    If you don't want this to have any concept of minecraft, that's fine. I just think it'd be a lot more useful at reducing boilerplate if it were minecraft specific rather than being a generic library.
     
  3. The TPS comments was about "The split method will check if there is time to continue running the task without dropping tps, otherwise it will halt execution of the thread until the next tick."

    I'm saying in your queue processor, you would do the logic "if TPS is < X, call TaskChain.backOffQueue(5)" for 5 ticks.


    And for queeuing block changes, you suggested a triple for loop and do code in, but the example split wouldnt be possible or you would iterate the same x/y/z each time. There is no stack suspension capability


    But you could do Queue<MyQueuedBlock> where MyQueuedBlock contains a Location object and the update to perform

    then you would for 3x and do queue.add(new MyQueuedBlock(block.getLocation(), blockData));

    then the queue processor will look up the block and apply the update.
     
  4. @Aikar
    Okay, the backOffQueue makes more sense now. Also, the split method I described is definitely possible. The task is on its own thread which is run while the main thread is blocked (similar to how the parallel tasks would be, except there's only one task being run). To pause execution the split() method would wait, and unblock the main thread. Then notify to resume. I have something similar in fawe.
     
  5. That still isn't perfectly safe to run in parallel as then 2 tasks could modify same block .

    Also even single task on alternative thread is going to trigger async catchers , which if main is blocked , will crash the server so that isn't safe to do either .
     
  6. @Aikar
    It's just one task at a time, nothing would be run in parallel. I was just comparing it to how it would block the main thread in the same way as parallel tasks.

    AsyncCatcher.enabled = false; (then re-enable when done)
    It wouldn't crash the server, just throw the exception if it's not the main thread. Perfectly safe so long as the main thread is blocked (and you aren't doing unsafe stuff in parallel).
     
  7. Sure, but TC core has to remain game agnostic, but you could make a wrapper in your plugin to manage the async catcher.

    But all of that isn't really necessary if we go the build a queue and process the queue route.

    The reason it has to be Game Agnostic is that it also will support Sponge and Forge, and really any java based game server.

    It forces me to be more disciplined about API design and avoiding 'hacks' and implementation details.


    I think you could pull off this logic you desire yourself in userland though using .syncCallback().

    pass a task to sync callback, that wont call next.run() until the queue is flushed.

    Then that task can schedule sync schedulers that opens a thread (if one is not already pending), then does Thread.currentThread(), to get the main thread, and passes it to the new thread so the new thread can communicate back to main.


    Then main will do AsyncCatcher.enabled = false, calculate how long you can safely wait, then mainThread.wait() to block

    The task thread can then do its thing, and the split method (better name: yieldIfNeeded()) would mainThread.notify() to wake main back up
    and then taskThread.wait() to suspend itself.

    Then back on main it would reschedule another sync task to repeat the logic (reusing the task thread) and reawake taskThread with notify and then suspend main again.

    You could make 1 util method handle all that logic, and then you can do:

    Plugin.newChain().syncCallback(staggeredBlockingQueue(task)).execute();
     
  8. I could maybe see something around that in core under a design like this:

    GameInterface needs to implement preSuspend and postSuspend for each interfacing library, default to nothing.

    Bukkit can override to turn off AsyncCatcher (unfun part: thats Spigot not Bukkit)

    The yield part should make yield() default yield, and the then BukkitTaskChainUtil can provide a yieldIfLowTPS() method that does conditional yielding.

    This isn't something I see myself adding, so I welcome a PR if you think you can pull it off.
     
  9. would kill for a resources section! :(
     
    • Like Like x 1
  10. I may live stream tonight more work on this, for a new Futures based API, stream: https://stream.emc.gs

    Adding support for futures based API instead of callbacks for Async Executing tasks, and then the ability to collect futures, so you can advance the chain when multiple futures complete.

    thinking like
    Code (Java):

    () -> {
    //... 4 futures
    return collect(future1, future2, future3, future4);
    }
     
    This will return <T> TaskChain<List<T>>

    allowing the next task to have a list of the responses of all of the futures.
     
  11. Looks pretty Promising ;)
     
    • Winner Winner x 2
    • Like Like x 1
    • Like Like x 2
  12. Ok, Did a ton of work on the futures API last night and I think it's ready.

    What is the Futures API you may ask? Otherwise known as a "Promise", In Java8 the API is called "CompleteableFuture"

    It's where you pass an object, that you promise will be "Completed" (Filled In) later, and the code you pass that object to can listen for when it is completed.

    This is an alternate fashion to the already existing AsyncExecuting "Callback" style API, but gets much more powerful with how you can control the chain.

    For example, in this new API, we already provide a way to return a list of Futures, and TC will wait for all of them to finish for you and return all results to the next task.

    In Callback style, that would of been extremely messy to do with lots of boilerplate in the users code.

    See the current and likely final state here: https://github.com/aikar/TaskChain/compare/futures - Examples file was updated with examples of using it!

    ------

    Also in the next API version: Data Wrappers!

    TaskChain has always been about a single return type. If you needed to return multiple values, you would need to create your own data object, or use the Task Map to store the values to be retrieved later.

    We now provide up to 6 data objects so you can return 6 responses from a Task to be passed to the next.

    See diffs: https://github.com/aikar/TaskChain/compare/[email protected]{2}...master (This link will break once I commit again >_>)

    Both of these, as well as experimental support for Sponge (6.0-SNAPSHOT API) will be released in 3.4.0, potentially tonight.
     
    • Like Like x 1
  13. #36 Aikar, Nov 14, 2016
    Last edited: Nov 14, 2016
  14. Looks good, im so lazy to import my plugins to java 8 :p
     
  15. MiniDigger

    Supporter

    ehrm, you dont need to do anything. just change your compiler level. this enables you to use Java 8 features and removes Java 7 support. but you don't need to replace all your loops to streams if you update to Java 8.
     
    • Agree Agree x 1
  16. Which if more plugin developers start requiring Java 8, more server owners will update / force their host to update or provide 8.

    Think of it this way, all those who haven't updated are at a security risk. You are helping society by getting people off of an end of life java, at the same time as making yourself happier with nicer development options.

    It's a win/win :)
     
    • Like Like x 1

Share This Page