Articles

running tasks in order with a task based sequence manager

The job of running a certain sequence of logic where the next task is dependent on the completion of the previous one is a recurring thing in software development. An example of this is an asynchronous queue of which we wrote in an earlier post right here. Sequence management is something that is invented time and time again by lots of developers but there is hardly any good complete sequencing solution out there. We have fixed this by releasing our nl.dpdk.commands.tasks package which features an incredibly powerful solution for managing sequences of virtually anything in actionscript.
Do you want to make your life simpler? Then read on…

To run a sequence of tasks most of us would create a chain of methods that call one another to achieve a certain effect. This is hard to follow throughout your code and not easy to adjust or rearrange. Here’s an example of this, a bad way of doing sequencing:

//functions depending on each other and having knowledge of each other
//thereby making them not reausable and highly dependent.
//The order of things is defined in the function call to the next function in the function that is currently running.
 
function a():void{
    //do something..
    //....
    //call the next method in a sequence of method calls
    b();
}
 
function b():void{
    //do something else
    //....
    //call the next method in a sequence of method calls
    c();
}
//etceterea

As you can see the previous example has a lot of flaws:

  • the code for the sequence is decentralized and therefore hard to follow and maintain.
  • because of bad structuring it’s hard to adjust the sequence
  • hard to maintain the codebase when changes appear
  • the statements above all mean there is low cohesion
  • it’s hard to dynamically create such a sequence (eg: via xml)
  • a method does something but also needs to have knowledge that is part of a sequence since it has to call the next method in line. It does two things, which is a bad thing. It should have one and only one function
  • for a different sequence, you would have to reinvent the whole thing and write new or copy-paste code
  • logic for the sequence is mixed throughout the different functions, it is not encapsulated in an object.
  • Each and every developer in a team will use his or her own way of doing it, thereby making projects in a team of developers harder. There is no standard solution for doing things
  • Imagine throwing in a couple of asynchronous method calls (getting xml data, loading in flv files etc.) and see the logic getting more and more difficult because of event listeners and the like.

another example where you can see this kind of logic, with functions calling each other is in our previous post on the DrupalService, where we first needed to connect before we could log in before we could get some data from the content manegement system. This is obviously a sequence and it obviously a hard to follow chain of events.

So we wanted to create a solution for this problem.

To have an implementation that is clean and easy to use and reuse, that does not suffer from the flaws we described above, we need to seperate the sequencing from the task we want to achieve. We also need to make sure that it is easy to add custom written tasks to a sequence. To achieve that we need to have clearly defined interfaces for our Task and our Sequence. We also need to make it so that developers have the power of a sequencing framework while making it easy to write their own classes without making them have to write a lot of code. You could use hooks as a mechanism to achieve that.

Furthermore, it is good OOP practice to make sure that an object has loose coupling, meaning objects shouldnt’t be too much dependent on each other. Most of the time this goal is reached by not relying on the inner workings of an object another object is is dependent on, as in the example above of one function calling another function. This can be done by having an object provide a good interface so that other objects can use an object, get information from it and make the object do it’s job. In the case of sequences this means that an object should not have to know it is part of a sequence but at the sime time provide an api to have a client of that object be able to do useful stuff with it.

As an example: a preloader should not know that it is in a sequence and that after the preloading is done it has to call another object itself. If that were the case, the preloader would have high coupling with the other object and reusability would go down. It would be better if the preloader provided methods to be queried if it is done, maybe through events like a Preloader.DONE event or just a simple isDone() method that can be called on the preloader (push vs. pull). In that way, another, specialized object can use a preloader to maybe start it, stop it and get information from it and that object would have the knowledge that is is used for a sequence. This specialized ‘task’ object would have a tight coupling with the object it is manipulating (in this case the preloader) but the knowledge of an object that it is inside a sequence is now outside of the object that shouldn’t be responsible for it (the preloader) and moved to the object that should be responsible for it (the task). Furthermore, when you are in the habit of reusing classes and working with interfaces, it is very easy to make reusable tasks to be used in multiple projects thereby fullfilling one of the promises of good OOP: reusability of code and software.

Guess what, we have done just that in our nl.dpdk.commands.tasks package ;)

The task based sequence manager in actionscript 3 consists of a number of classes of which the two most important ones are Sequence and Task. These will provide you with all the power to run serial sequences and execute tasks out of the box.

  • A Sequence is a class that can hold multiple tasks that need to be executed serially. It should be used as a sequence manager, and can hold instances of subclasses of Task.
  • A Task is a base class that has all the functionality to work in a sequence or standalone and contains logic for the job it is specialized to do. The only logic you will need to create is a small subclass of Task to make everything work. This allows you to write highly reusable code that can be used in a sequence. Most of the time, a Task will be configured via it’s constructor.

The whole package also contains a number of ready made subclasses of Task also, some of wich have some very powerful features out of the box, which we will explain with examples later on.

  • TimeDelayTask: a task that waits a certain time before it’s done and allows the next task in the sequence to continue.
  • FrameDelayTask: a task that waits a certain number of frames before it’s done.
  • CallBackTask: a task that calls a callback method. You can also add parameters to be used for the callback method.
  • TimelineTask: a task that takes a movieclip and a framelabel as it’s constructor’s argument. It will continue when the movieclip has reached the framelabel specified. It has an optional timeout.
  • ConditionalTask: this is a task that allows conditional branching in a sequence. It is one of the killer features as it allows one sequence to wait for one or more other (possible asynchronous) tasks to complete before the sequence continues. Complex conditional logic and flow management can be created for your sequence.
  • CompositeTask: a Task that executes all the tasks that have been added to it at once. This is useful for executing several separate tasks at once.
  • CommandTask: a Task that wraps an instance of an ICommand (which can be a concrete Command or another Task or even a Sequence since both Task and Sequence implements the ICommand interface) and executes it. When making heavy use of the command pattern in your application this is a super way of reusing those commands in sequences.
  • SequenceTask: whoa, hold on! this is a difficult name :) a SequenceTask is a Task that wraps a Sequence, so it can be used to form nested (!) sequences of tasks inside a Sequence.

Finally, we have two event classes to manage events that come from the sequence and the individual tasks in a sequence:

  • TaskEvent: these are dispatched by every task (handled automatically in your own task subclasses when you write them) to handle the starting (TaskEvent.START), ending (TaskEvent.DONE) and possible errors (TaskEvent.ERROR) in your concrete task. You can specify your own error messages from your task subclass.
  • SequenceEvent: these are dispatched by the sequence and can be used to control the flow of the sequence by listening to the SequenceEvent.NEXT task when you want to know when the next task will fire, SequenceEvent.DONE when you want to know when all tasks are finished and SequenceEvent.ERROR and SequenceEvent.ERROR_NON_BLOCKING for errors that might occur in the tasks in the sequence.

We will guide you through the features of sequences with some examples. At the end of all the examples, you should be able to create complex sequences of tasks yourself.

let’s start with a simple example to demonstrate the basics.

//create a new sequence that will hold tasks
var sequence: Sequence = new Sequence();
//add a task that waits 5 frames to finish
 sequence.add(new FrameDelayTask(5));
//add callback task to the sequence of tasks, it will run after the previous task has finished and will trace some output
sequence.add(new CallBackTask(trace, "we have just waited 5 frames");
//new delay
sequence.add(new FrameDelayTask(3));
sequence.add(new CallBackTask(trace, "this message will be traced after 3 more frames!");
//add listener for the ending of the sequence (implement onSequenceDone as an event listener)
sequence.addEventListener(SequenceEvent.DONE, onSequenceDone);
//start the sequence and watch the magic happen by calling it's execute() method.
sequence.execute();

What just happened? well, after we built up the sequence of tasks we wanted to execute, we fired it up with a call to sequence.execute(). Then the following stuff happened in order: first there was a delay of 5 frames, then a trace that outputted text that said “we have just waited 5 frames”, then there was an additional delay of 3 frames, then a trace that said so and finally the event listener onSequenceDone was fired to let the client of Sequence know that all Tasks have been executed succesfully.

here’s another example of sequences that should fail as a whole when a task has an error and sequences that should not fail as a whole when a task has an error. We put two sequences into thow seperate tasks called SequenceTask and we put that into the master sequence to have nested sequences:

//a crucial sequence that should fail when something goes wrong (specified in constructor)
var crucial: Sequence = new Sequence(true);
crucial.add(new LoaderTask("veryImportant.swf");
crucial.add(new LoaderTask("weNeedThis.swf");
crucial.add(new VeryComplexTask());
crucial.add(new LoaderTask("andThisToo.swf");
 
//an optional sequence. when something goes wrong, we don't mind (check the 'false' in the constructor)
var optional: Sequence = new Sequence(false);
optional.add(new LoaderTask("unimportant.swf");
optional.add(new LoaderTask("boringStuff.swf");
 
//now, put the previously created crucial and optional sequences in two SequenceTask instances
var crucialTask: Task = new SequenceTask(crucial);
var optionalTask: Task = new SequenceTask(optional);
 
//create the master sequence that should fail when something important fails
var master: Sequence = new Sequence(true);
master.add(new CallBackTask(trace, "starting the first task"));
master.add(optionalTask);
master.add(new CallBackTask(trace, "optional stuff loaded, maybe all, maybe some, maybe nothing.."));
master.add(crucialTask);
master.add(new CallBackTask(trace, "all crucial stuff loaded, were done!"));
 
//we want to do additional logic when the sequence fails or succeeds, so listen to some events.
master.addEventListener(SequenceEvent.ERROR, onError);
master.addEventListener(SequenceEvent.DONE, onDone);
 
//now start executing the tasks!
master.execute();
 
//when unimportant.swf fails to load, the rest of the tasks and sequences will continue and eventually, onDone will be called
//when weNeedThis.swf fails to load, onError will be triggered and the rest of the stuff will not be handled.

That was a little more complex, but we have achieved a lot. Once you have a conceptual understanding of Tasks and Sequences a lot of things will make sense and in our case, you wonder why you have ever gone without sequences. As you can see from the example, we have a sequence that must succeed, these might be different swf files that will build all the screens in your application. When one does not load, it will be a fatal error. There is also a sequence that must not succeed per definition. This might be the loading of some swf files that just hold a little eye candy. The application can go on without them. To make sure that the important stuff doesn’t fail in a sequence because the unimportant stuff fails to load, we create a seperate SequenceTask, a Task that holds a reference to a Sequence. Since we can specify in the constructor of a Sequence if it can tolerate errors from a subtask, we create a Sequence that is tolerant of errors (the optional one) and a sequence that is not tolerant of errors (the crucial one).
By now you can guess what that means: a sequence that is tolerant of errors will continue with executing it’s tasks even if one (or many, or all) task in that sequence generates an error (for example: a loading of a file failed). A sequence that is not tolerant of errors will stop executing it’s tasks when only one of the tasks fails.
By wrapping a Sequence in a SequenceTask, that Task will generate an error depending on the kind of Sequence it contains: no error (thus succes) when it is a non blocking sequence and an error when it is a blocking sequence!

yet another example, this time it’s about sharing data between two seperate tasks. We can do this by using a form of dependency injection.

//initially empty list, that will hold domain objects, in our case of nl.dpdk.user.User,
//that holds some data about registered users
var userList: List = new LinkedList(); 
 
//task that will load users from xml and will populate an externally provided list with User objects
var userTask: Task = new UserCreationTask("users.xml", userList);
//remember, at this point, the list is still empty.
//pass a reference of the list to the widgetTask, that will create a widget with the User data specified in the list.
var widgetTask: Task = new UserWidgetTask(userList);
 
//create a complex sequence of tasks that incrementally need more information
var sequence: Sequence = new Sequence();
sequence.add(userTask);
//... etc.
seqence.add(widgetTask);
//fire it up!
sequence.execute();
//as the sequence of tasks progresses, widgetTask will have a reference to a userList
//It will be initially empty, but is filled by other tasks before widgetTask itself is executed.
//in this way, we can share data between tasks without them knowing of each other.

So now we have control over nested sequences, errors or no errors in tasks, sharing data between tasks (see previous examples) we still miss one thing that is enormously convenient, Sequences that respond to conditional logic or conditional branching such as: only do the next task A when both task C and Task Z have finished, or, make sure these conditions are satisfied, if not and a certain time passes, take another flow path. Yesss! We also have a solution for that use case :p. The trick is to use multiple sequences in parallel and to use a specialized Task called ConditionalTask.

//the main sequence of our logic flow
var master: Sequence = new Sequence();
//create two other sequences that are both independent of the master sequence and each other.
var branch1: Sequence = new Sequence();
var branch2: Sequence = new Sequence();
//insert a concrete Task subclass that does an asynchronous thing (maybe a remote procedure call)
branch1.add(new SomeStrangeAsynchronousTask());
//create a local variable to hold a reference to diverse tasks
//we will be creating that will be fed to a ConditionalTask
var task: Task;
//the first task we make creates a delay of 50 frames before it is done
task = new FrameDelayTask(50);
//add this task to the branch1 sequence, remember branch1 already has a task
//that does some asynchronous stuff and so this task will be executed in the sequence
//after the asynch task has finished.
branch1.add(task);
//add the task to the master sequence *inside* a ConditionalTask.
//this conditional task can only finish when the FrameDelayTask is done.
master.add(new ConditionalTask(task));
//create another task, this time a task that waits for a preloader movieclip animation to finish on frame label "preloader_finished"
task = new TimeLineTask(preloader_mc, "preloader_finished");
//add the timelinetask to the other sequence referred to by the branch2 variable
branch2.add(task);
//add another task to branch2 that will execute immediately after the preloader finishes.
//it fires the method 'postPreloaderAnimation'.
branch2.add(new CallBackTask(postPreloaderAnimation);
//add the TimelineTask for the preloader to the master sequence also *inside* a conditional task.
//this conditional task can only finish when the TimelineTask has finished
master.add(new ConditionalTask(task));
//add callback task to master sequence of tasks,
// a method that will be called when all the previous tasks in the master sequence have finished.
//in this case, the previous tasks will be two conditional tasks,
//both of whom are dependent on tasks in a different/independent sequence.
master.add(new CallBackTask(doSomethingSpecial);
master.addEventListener(SequenceEvent.DONE, onSequenceDone);
//start all sequences!
master.execute();
branch1.execute();
branch2.execute();
//the end result will be very nice!
//The master sequence will finish when and only when both the preloader animation has finished
//AND the asynchronous task and the FrameDelay of 50 frames have finished.
//it doesn't matter which of the two task finishes first,
//since the task after both ConditionalTasks in the master sequence can only execute when
//BOTH have finished.
//The second conditional task can be 'primed' when the preloader has finished
//(marked for immediate finish when it is executed)
//while the task before that (the conditional task that waits for the framedelay task to finish)
//still has to wait on the framedelaytask.
//it can also be the other way around:
//the framedelay might be finished before the preloader animation finishes.
//in that case the first conditional task will execute and finish immediately
//but the second conditional task will only finish when it gets the signal from the preloader animation task.
//when both tasks are done, the dependent ConditionalTasks will have both executed
//and the next Task will execute, in this case the CallBackTask that will call a method
//that everything has been done an we can startup the rest of what we want to do in our application.
//Also keep in mind, that the "postPreloaderAnimation" method will be called
//right after the preloader finishes,
//as it is in a seperate sequence from the master sequence.
//Thus we can have processes running in parallel.

Oke, that was somewhat complex, but is demonstrates the power of Sequences and Tasks. By now, you should be able to follow what is going on. By reviewing the Tasks that come delivered in the tasks package (mentioned above) you can possibly find some interesting use cases. I hope by now you are excited and ready to begin building your own Task subclasses.

This is were the real power is :) We have only provided you with the tools to do sequences and now you can build tasks that suit your need. To build a Task yourself, you need to do 5 simple things:

  1. Create a class that extends nl.dpdk.commands.tasks.Task and put any relevant data in there via it’s constructor and store it in private variables.
  2. override the method executeTaskHook() and implement your specific Task logic there. Add event listeners here on objects you have available (passed in via the constructor) when you need a service of them and set everything in motion.
  3. when all logic is executed as it should have, call the protected method done()
  4. when all logic is not executed as it should have, call the protected method fail() with your custom error message. The error message itself will be available via both the TaskEvent and the SequenceEvent.
  5. when your Task requires a cleanup after it is done, override the method destroyTaskHook() and implement your cleaning logic there. You should remove event listeners here if you have them and any references to objects you created or used.
  6. Oke, an optional sixth: provide an overriden toString() method, it facilitates debugging :)

Wow, how easy can it be? That is all, really :) the done() and fail() methods you can call from your custom subclassed Task take care of everything that has to be done for sequencing and event dispatching. We achieve this effect by using two design patterns: hooking and the template method.

The next example shows you the implementation of our CallBackTask, which will not generate an error, but only used done() when it has finished. If you’re wondering what this does, maybe you will remember the old actionscript 2 days when there was a thing called Delegate. It had a method called Delegate.create(scope, callback) and there were some implementations out there that also allowed you to give it some extra arguments. Well, this is the juiced up actionscript 3 variant!

public class CallBackTask extends Task {
	private var callBack : Function;
	private var args: Array;
 
	/**
	 * @param callBack the method that will be called when executing this task.
	 * @param args any number of arguments that will be passed to the callback function when it is called.
	 *
	 */
	public function CallBackTask(callBack: Function, ...args) {
		this.callBack = callBack;
		this.args = args;
	}
 
	override protected function executeTaskHook() : void {
		callBack.apply(null, args);
		done();
	}
 
	override protected function destroyTaskHook() : void {
		callBack = null;
		args = null;
	}
 
	override public function toString() : String {
		return "CallBackTask";
	}
}

From this example and the tips above you have all that is needed to start creating your own tasks. Remember that the example above is NOT asynchronous, but the power of sequencing is most of the time in using asynchronous things, such as loading stuff, waiting for user interaction, doing animations, using timers etcetera etcetera. Be imaginative and creative and lots of use cases will come up. Our own library of useful and reusable Tasks is rapidly and constantly growing and writing custom sequencing stuff is definitely in the past for us now.

We use Sequences, so should you!

To wrap this up, here are some tips and tricks on tasks and sequences:

  • Each Sequence will call destroy() on a task right after the SequenceEvent.NEXT event has dispatched. Therefore, the event handler can be used on an intact Task. The sequence also removes the Tasks it has, so it has no history, it destroys them and Tasks can only be used ONCE! If you need to reuse a Task, just build a new one. If you need to use a Sequence more than once, use a factory method that creates the sequence for you with all the relevant tasks in it.
  • Both Task and Sequence implement ICommand and therefore they can be used as Commands. an example of this would be to in a button in which you could insert a command. You could also insert a Sequence or a Task in that button.
  • When ending a sequence, you do not specifically have to listen to the SequenceEvent.DONE. Just use a CallBackTask as the last task inserted in the sequence. The callback function specified in the task will be the end of the sequence and you can take appropiate action there. It saves you an eventlistener.
  • Go crazy with subclassing tasks. You can use it for anything. Just remember to provide a Task with relevant information in it’s constructor so it has some context in which to operate. For example, pass in an object that knows how to get some data somewhere and call a method on that object in the overriden executeTaskHook() method. For example: put a nl.dpdk.services.RemotingProxy object in the Task to get some remote data.
  • use sequence.getTasks() to get a List of all Tasks remaining to be executed. This is especially useful when you listen to SequenceEvent.NEXT. In the event handler for the event you can get the Task that was just executed via event.getTask() or sequence.getCurrent(). You could pause the sequence, insert a new Task in the List you got from sequence.getTasks(), you can get data from the Task that was just executed, remove Tasks from the sequence etcetera etcetera. Please take a good look at this when you are a Task/Sequence power user :)
  • Make sure you really call done() and/or fail() from inside your subclass, if you do not do this, the sequence flow will not continue!
  • You can also listen to specific TaskEvents. There is nothing stopping you to listen to those events. You can use it to listen to certain types of tasks, maybe when you want to get some data out of those tasks and put all the data in a List. Or something completely different :) There are lots of use cases out there… A TaskEvent.DONE or TaskEvent.ERROR is dispatched and will be picked up by the object listening to the event. There is one tricky thing. Events are dispatched to their listeners in an order that is not fixed (according to adobe documentation) in the order in which they were added, so add the eventlistener to the Task before the task is run by the sequence .If the Sequence picks up the event before another event listener, the Sequence that holds the task will follow through the whole routine of managing Tasks and will destroy the task before you get a chance to access the Task. See the next point if you want to avoid this problem
  • When wanting to manage data from a task, it is a very good trick to pass a reference to a callback method in the Task constructor. Call the method when the time is right and use the data from the Task as the parameter for the method. To see an example of this, check out nl.dpdk.services.gephyr.tasks.DrupalNodeTask
  • A Task should be responsible for doing a simple thing. It is very easy to write tasks that use other tasks, as demonstrated by SequenceTask and ConditionalTask. You can stack Tasks and use them in combination to build new more powerful tasks.
  • A Task should only work once, just like in real life, make sure to clean it up when it is done doing it’s thing in the destroyTaskHook() method.

Thanks for your attention. Please provide us with some feedback or questions about this topic if you have it, it would be greatly appreciated!

Happy coding :)

8 Responses to “running tasks in order with a task based sequence manager”


  1. 1 Thijs Broerse

    Nice job. Looks really great. I am definitely going to try this out.

    But shouldn’t there be an ITask interface, so you can create a custom Task without extending the regular Task?

  2. 2 rolf vreijdenberger

    Hi Thijs,

    thanks! I hope you’ll do some nice stuff with it.

    There is no ITask interface, since you NEED to subclass/extend Task. Task itself has all the logic to be able to run it in a sequence. By subclassing Task and by overriding two methods, you only have to implement a little bit of logic and you cannot make any mistakes when you write your own subclasses, since the Task superclass takes care of all the necessary actions needed.

    If you study the Task class you will see that it uses template methods, a technique where you specify a sequence of method calls inside a method (the template) and override only selected methods in the template method. This makes sure an order of calls is preserved (and thereby the logic), while allowing subclasses to introduce additional logic.

  3. 3 Bart

    It’s looking good, can be very usefull. But to be able to use this in a more complicated environment a pause/resume feature would be great (or you’d get task/sequence-completion events from timed/framed tasks while everything else is paused and that will get very messy).

  4. 4 rolf vreijdenberger

    Hi Bart,
    actually, you can pause the Sequence at any time via sequence.pause().
    The currently executing task will finish and dispatch a TaskEvent when done, but the sequence will not continue. So your application state can react to Sequences but also the other way around. The use case you describe is taken care of :)

  5. 5 Benoît de Raemy

    Hi,
    First of all, thank you very much!! Your classes has been more than helpful to handle my data between my Drupal back-end and my flex front-end. I just had to add a comment service to your DrupalService and everything worked fine.
    There’s only one little problem : as the name you use for the sequence classes is the same one as one used by the flex framework to handle some effects (mx.effects:Sequence), it “can’t resolve a multiname reference unambiguously”… Is there a way to get around this problem ? Do I have to rename the hundreds of reference “sequence” in my npdk package (and, if yes, is there a way to handle that automatically)? Or is there an other way to make sure my compiler doesn’t mix up between the two classes? Or, may be, you could be really-really cool to flex developer and fix that directly in your package…
    Anyway, thank you again for your great job (and generosity) and I’m looking forward to showing you very soon what I achieved with your help (it’ll be peanuts to you, but as a beginner in flex and AS3, I’m very proud of it).

  6. 6 Rolf Vreijdenberger

    Hi Benoit,
    thanks, glad that you can use our stuff, make sure to show us what you created!

    as for the flex issue, we’ll get into that and try to fix it, but renaming Sequence is not an option for us as it is conceptually the best name for what it does.
    As you might guess, our Sequence is a lot more powerful and universally applicable than what flex does, since it is only focused on tweening effects.

    THat said, it should not be a problem since the compiler should resolve that it is in a different package. The ambiguity might be because of missing imports inside the package itself.
    As I said, we’ll fix the issue soon.

    In the meantime, if you do not use Sequences or Task with drupal, you could remove the tasks package in the nl.dpdk.services.gephyr package.

  7. 7 Benoît de Raemy

    Hi Rolf,
    Thank you very much, but I figure it out (as often in such case, I should have thought harder before bothering others for nuts) : all I had to do is to declare my ambiguous classes in extenso (with its “fully qualified class name”) in my mxml file each time I use it, and not only through an import of the whole class :
    For example, the variable :
    private var sequence: nl.dpdk.commands.tasks.Sequence;

    And the constructor:
    sequence = new nl.dpdk.commands.tasks.Sequence();

    That way, Flex compiler knows exactly to which classes it refers, and is still free to get automatically the path for class references that are part of its “native” packages.

  8. 8 Chris

    This is gold. I wrote my own very simple sequence based function queue then stumbled onto this. You opened my eyes to what is more of a design pattern and what you have done obviously has its benefits. This will change the way I code everything from now on for the better, cheers!

  1. 1 On using sequences and tasks with the DrupalService at dpdk Open Source
  2. 2 Wait for user input in your sequence at dpdk Open Source
  3. 3 a configurable, pluggable, thread safe prioritized bulk loader with object management at dpdk Open Source

Leave a Reply