5

I'm trying to design an asynchronous component. Requirements on this component are :

  • Component might receive events at any point in time
  • Component might start a long-running operation and wait for its result. This operation is executed by outside system.
  • Long running operation might be cancelled, either by the component or externally
  • Events might come while long-running operation is running, which might start the long-running operation again, possibly cancelling previous invocation
  • Components needs to decide what to do with event that comes during long-running operation, if to ignore it, queue it or something else
  • Component might start different operations in reaction to different events
  • Component can have multiple "backing values", where each has current value and its change request is an event component needs to react to
  • It must be possible to unit test the component in isolation (this is important!)
  • There will be multiple different components, some might share behaviors. For example ability to turn the component on/off.

So far I have tried/thought of:

  • Reactive observables
    • This is what is used right now
    • I really like how testing can be done in virtual time
    • Long running operations are represented as function that transforms "request" observable into "response" observable
    • But it rest of the team considers this solution too complex
  • Plain objects with methods for event handlers
    • Overall simple, but some constructions might be complex
    • Doesn't require additional libraries
    • Testing is not straightforward, as it is required to explicitly write every response to event instead of saying "for every request, respond in 5 ticks" like in reactive
  • Tasks with async/await
    • I don't know how would it be possible to implement situation that happens when event happens during long-running operation and how to test this situation, which is why I choose reactive instead
  • Actor framework (Akka)
    • This seems to be best fit for my requirements
    • But I don't like how testing is made, as it is using real time
    • And I believe Akka is way overkill for this use case, as we don't need per-component threading and the whole instrumentations to create actors and communicate between them
    • And I'm afraid my team would consider it too complex too

What I'm looking for are suggestions of possible other solutions, I might not have thought of. Or additional advantages/disadvantages of suggested solutions.

6
  • 3
    "The rest of the team considers this solution too complex": A new solution or technology may seem too complex to people who are not familiar with it, so a potentially better solution is dismissed just because a worse one looks more familiar. Maybe you should explain the ideas above to your team using example code, and then let them explain to you why they consider the first solution too complex. This will force them to at least understand it before they dismiss it and to provide a more precise reason against it: "too complex" is IMO a bit too generic.
    – Giorgio
    Commented Mar 15, 2017 at 6:02
  • @Giorgio While that would be best solution to my problems, I would like to ignore it for now. Because reasons, that would be OT on this site.
    – Euphoric
    Commented Mar 15, 2017 at 6:49
  • 1
    It must be possible to unit test the component in isolation (this is important!) - start writing test before implementation. During writing tests you will sit in "chair" of consumer of you component, which gives you ideas of what kind of approach can fit your requirements
    – Fabio
    Commented Apr 16, 2017 at 5:19
  • @Fabio That is general way of how I do things normally. But there might be design for my solution that is not in "local optima" that is achieved using that approach. And I'm looking for that design. Also, what you said is how I created current solution using observables.
    – Euphoric
    Commented Apr 16, 2017 at 6:06
  • 1
    If possible, you might split this into a couple of components. One to manage the long running operations, and one to react to ad hoc events. The "solution being too complex" issue is a symptom of one thing trying to do too much. You can have the ad hoc component reference and interact with the long running operation component since it seems that they are related like that. Commented Jun 13, 2017 at 12:35

2 Answers 2

1

I think observable is the correct pattern.

Promise/Task/Future/Deferred with async/await is best suited when you have a single event (or branching logic chain of events) to follow. You will need a lot of additional work to make it fit your situation.

Most implementations of that use a generator*/IEnumerable/iterator under the covers anyway, with await becoming yield new Promise/yield return new Task and the calling coding being converted to an enumerator of promises/tasks.

Rx/observable is better suited where events can fire repeatedly and you want to treat them as a collection iterated over time.

You could roll your own class to handle this (possibly using async/await) but you'll have to solve a lot of the same problems that Rx already has while not saving on any complexity.

2
  • This is what I assumed in the beginning. But then, the behaviors of components became quite complicated and it was not possible to simply implement them as composition of observables. So I had to resort to hacks, like saving/reading fields outside the observable composition chain.
    – Euphoric
    Commented Mar 15, 2017 at 13:43
  • @euphoric - you'll have to post some examples for specific help with that. I find no plan survives contact with the enemy, so sometimes you just need hacky code. Get it code reviewed and see if anyone with fresh insight can help.
    – Keith
    Commented Mar 15, 2017 at 14:54
0

Your solution might be too complex because it's doing too much within itself. If, for example, you split long running task (AKA job) management into its own component, you can limit the interaction to managing those tasks. Similarly, the ad hoc event handling component might need to use the job manager, but limiting testing to those events can make it a bit easier to work with. I've taken that approach myself with a couple projects.

The way you handle the testing is by a couple well chosen interfaces. The implementation can be your Reactive observables, or a custom implementation, depending on your true needs. The IJob interface could look something like this:

public interface IJob
{
    string Id { get; }
    bool IsRunning { get; }
    Task Handle { get; }
    void Cancel();
    // and anything else you need.
}

You can also make a typed version to deal with Tasks that return a value.

The bottom line is that you might only need one of the long running task management components in your system. You can test all the interactions with this component easily, and you can test the component itself fairly easily. Since the Handle property exposes a Task you can easily have code that simply will await the job to finish.

I haven't worked with Reactive Observables, but you can provide a Task handle connected to a simple result fairly easily using a TaskCompletionSource. Inside the job you can either set the result or the error state when you need to explicitly, and outside code is none the wiser. The TaskCompletsionSource has a Task property that can be used for the Handle above.

You can have as many "ad hoc" components as you need, and as long as they have a consistent way of managing the long jobs, it should simplify the design into something that everybody can understand.

3
  • How is IJob different from Task? And how does it handle the case where job is "interrupted" but not cancelled by another job?
    – Euphoric
    Commented Jun 13, 2017 at 14:11
  • IJob will be able to hold the references to the CancellationTokenSource, establish the protocol to pause/etc. without spreading that logic around the rest of your system. It's self-contained, so there is more to it than just the Task. I think it's important to point out not all tasks can be paused (for example using an external process with no signaling hooks). This same interface can work with external processes (something I have done to convert audio for example). The consumer of the job is ignorant of how the job is actually executed. Commented Jun 13, 2017 at 14:19
  • I really don't think the idea of "long running operation" is applicable here. I was thinking more like actor model, where there are multiple actors sending asynchronous messages between each other. I just find existing actor frameworks way too overkill for my situation.
    – Euphoric
    Commented Jun 13, 2017 at 15:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.