|
|
-
Two things have happened which made me think I needed a second blog:
- My job has changed slightly so I've got a few non-workflow things to say now.
- I wanted a place where I didn't feel obligated to stick to work related topics.
Let me introduce to you http://SharedMemory.spaces.live.com. It's just a place for me to post the interesting non-workflow things that I come across at work as well as any other random technical thoughts that come to mind.
Enjoy.
Nate
|
-
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=488698&SiteID=1&mode=1
There was a great set of questions that showed up on the forum the other day and I've finally gotten around to answering them all. I was planning on posting the answers in this blog, but instead I'll just reference back to the forum post.
Summary: * What is persisted with a workflow instance and what state am I in after a computer crash? Queues, the internal list of scheduled items, and the binary serialization of the workflow tree are all persisted. Reloading an instance after a workflow crashes gives you the exact state you had at the last persistence point.
* I want a single activity which delegates work out to the host and then waits for notification that the work is done. Should I use the ExternalDataExchangeService or a custom IEventActivity? If it must be a single activity then I would recommend a custom IEventActivity to enable use of the solution in the StateMachine and in EventDrivens in general. This is a prime scenario for ExternalDataExchangeService, but the requirement for a single activity to do the work makes that a less than viable solution.
* How do I get persistence of the workflow to take part in the same transaction as cleanup of a database caused by an inbound event? Batching is your friend ... see the post for details of batching.
* What are the implications of dropping correlation from the Correlated Service Sample? In short, if you drop correlation you will risk getting a runtime exception. If two activities are waiting on the same event at the same time then we will throw an exception when the second activity tries to "handle" the already handled message.
|
-
Windows Workflow Foundation (WF) ships with two out of box modes of passivation (also referred to as dehydration and unloading) of a workflow. Passivation is the process by which a workflow's state is saved to the database AND the workflow is removed from memory for the time being.
Out of Box Support
Unfortunately our support is very binary and only related to the idle time of a workflow instance. Our base persistence service class, WorkflowPersistenceService, defines a boolean method UnloadOnIdle which will be called when a workflow goes Idle. If this method returns true, then the instance in question will be unloaded (persisted and removed from memory). If, however, the method returns true then the instance will remain in-memory.
This is Less Than Ideal
WF has done a wonderful job providing extensibility points. WF has also done a great job not enforcing our semantics on the user. You want to write your own persistence then feel free ... serialize the workflows however you want and store them wherever you please. You don't like either of our out of box threading models ... go ahead and write your own. Our definition of ParallelActivity doesn't suite you ... create a parallel which does what you desire.
Here, however, we have locked you in. We have a method which you must implement on the persistence service which returns true or false. Either you want the idle instance to unload right then, or you don't. But what about advanced policy? What about "unload after 20 minutes of idle time", or "unload when the instance hits a specific point", or "unload after 20 minutes in memory regardless of idling"?
Custom Passivation Policy
Luckily, just because we locked you into having to think about our UnloadOnIdle concept, we didn't lock you into using it. From here on out assume that we always return false from UnloadOnIdle.
Unload After 20 Minutes Idle
Let's create a new service which derives from WorkflowRuntimeService. WorkflowRuntimeService is a base class which, on start up, will get a reference to the WorkflowRuntime so that you can safely access it. In our custom service (UnloadIn20Service) we'll override the Start method, call base, and then subscribe to WorkflowIdled:
override void Start() { base.Start(); WorkflowRuntime.WorkflowIdled += OnWorkflowIdled; }
Now, let's assume that we've written a collection with the following behavior:
- Items in the list are pairs of workflow instance IDs and DateTimes
- The list is sorted on the DateTime such that the earliest DateTime is at the head of the list
- There is always an active timer which will expire at the DateTime specified by the item at the head of the list
- There is an event TimeExpired (void(Guid))which is publicly exposed by the collection and raised any time the timer expires.
- When the timer expires the item at the head of the list is removed
In short, a priority queue which notifies us when a timeout has expired for an instance. Our OnWorkflowIdled now looks like (assume we have already subscribed to TimeExpired):
if (idledWorkflows.Contains(e.WorkflowInstance.InstanceId) idledWorkflows.Remove(e.WorkflowInstance.InstanceId); idledWorkflows.Add(e.WorkflowInstance.InstanceId, DateTime.Now.AddMinutes(20));
Our OnTimeExpired handler:
WorkflowRuntime.GetWorkflow(instId).TryUnload();
Now, with a few lines of code we have created a service which we can add to the runtime which will manage unloading workflows after 20 minutes of idle time. Note that by using TryUnload instead of Unload we are guaranteed that if the workflow is NOT currently idle then we will not actually unload the instance. TryUnload will return false in that case and is a no-op. Unload, in contrast, will block until the instance can be unloaded (not in a TransactionScopeActivity and the scheduler is not currently running an item - note that items are allowed to be on the scheduler queue) and then follow through with the passivation.
So, a quick walkthrough ... a workflow goes Idle and it is added to the list. If it is the next one to "expire" then it a timer will be created by our list for that instance. When the timer expires the service will attempt to unload the instance, but only if it is not currently executing. In the case that it executes and then goes idle again before the 20 minutes are up then the old expiration is removed from the list and a new expiration is added.
Unload After 20 Minutes In-Memory
This is actually the same implementation as the last one except for two changes. First, subscribe to the WorkflowLoaded event. This will notify you when an instance is loaded into memory. If you want newly created workflows to have the same behavior then you also should hook the handler to the WorkflowCreated event as this is another mechanism by which a workflow can find its way into memory.
Second, change the call to TryUnload to a call to Unload instead. This will make sure that the instance is unloaded regardless of whether or not it has more processing which it could do.
Unload At a Specific Point in the Workflow
Well, my fingers are getting tired, so I'm going to give this one a superficial overview. In this case it would make sense to write a tracking service. Many people have the incorrect view that writing a custom tracking service is too much work, but this is not the case. In fact, I will put a post up on writing custom tracking services next time I write.
The technique here would be to create a tracking channel which is aware of 1) the instance being tracked (this data is passed when the tracking channel is requested) and 2) the workflow runtime itself. The tracking channel could then wait for a specific message (ActivityTrackingRecord, WorkflowTrackingRecord, UserTrackingRecord) and cause the workflow runtime to unload the instance.
Conclusion
WF's extensibility model let's you do almost anything you can imagine. Today I walked through a few examples of custom passivation policies which can be implemented using the framework provided by WF. As noted above, next time I'll talk about writing a custom tracking service to dispel the myth that it is "too hard" ... I think the problem stems from the number of similar methods which must be overridden and the two class structure of a tracking service. As always, questions and comments are welcome.
|
-
QUESTION: Can I use DefaultWorkflowSchedulerService instead of ManualWorkflowSchedulerService in my IIS hosted WorkflowRuntime?
ANSWER: Do so at your own risk. It's like swimming in an unguarded pool or skydiving ...
Using the DefaultWorkflowSchedulerService in an IIS host (specifically ASP.NET) is one of those good/bad scenarios depending on point of view. Windows Workflow Foundation (WF) has no qualms about using the DefaultWorkflowSchedulerService in an ASP.NET hosted scenario and everything will work just fine from a WF point of view. The problem is that ASP.NET does NOT like people using up all its threads.
Our ManualWorkflowSchedulerService was written specifically for the ASP.NET scenario to make sure that we were running on the minimal number of threads. The call to RunWorkflow is an explicit "gifting" of the current thread (the request thread in the IIS case) to run as much of a specific workflow instance as possible. When you introduce the DefaultWorkflowSchedulerService you are, by default, allowing as many as 4 threads to run workflow instances.
Is that a bad thing? In most cases, no. Don't quote me on these numbers, but .NET supports something like 25 threads per processor per Process. If you are doing anything with Transactions, then at least one of those threads needs to be "available" to avoid deadlock in some cases. The number of threads ASP.NET requires depends on the situation - if you are running a single web service in an isolated AppPool then I can't imagine ever running into an issue.
So, what's the "official word" - the statement that covers everyone's butt and will hopefully steer users to a design that is free from danger?
First, examine your scenario. Ask yourself why you need to be doing the processing in the background (non-request thread)? Is all processing tied to a request? Does the processing take very long? Is new processing ever driven by a "delay" in the workflow or some event not tied to an inbound request?
If you are doing a lot of processing then it is often better to model this as a windows service. Using Windows Communication Foundation (WCF) you can directly expose your windows service as a consumable web service. Alternately you can use WCF or .NET Remoting (last generation technology at this point) to communicate between your current web service and your windows service.
If you aren't doing much processing and it is all tied to the request, then consider using the ManualScheduler. While this will add additional latency to your responses, it can be considered a constant value as opposed to the unpredictable response times you'll see when you've got an unknown number of active workflow instances vying for processor time.
In general, try to avoid using the DefaultWorkflowSchedulerService in ASP.NET/IIS. If you do use it, use it with caution and expect that there may be a few unknown problems which pop up during stress. There is always an alternate design and in general the tradeoffs will be maintainability/complexity versus request/response speed versus stability under stress.
|
-
Ever wonder why this.delayActivity1.TimeoutDuration sometimes doesn't change the timeout duration? How come this.callExternalMethodActivity1.ParameterBindings["(ReturnValue)"] isn't giving you the value you expect in some scenarios? How is it possible that sometimes this.GetActivityByName("foo") does not equal my sender for one of foo's events?
The answer to all of these questions is: spawned contexts.
One of the most powerful and most easily misunderstood concepts in Windows Workflow Foundation (WF) is that of new contexts executing cloned activities. Before we even define a spawned context, let's go back to the beginning ...
The Beginning
One of the binding qualities of the activity state transition diagram is that there are no transitions from Closed back to Executing. You can only get to Executing through an Initialized activity and you can only get to Initialized from a brand new instance.
How do we handle looping activities then? Replicator, While, and CAG all give the impression of executing the same activity (or set of activities) multiple times. The answer is by creating a new context and cloning the template activity (explained later).
ActivityExecutionContext Interlude
First, let's fully understand the ActivityExecutionContext. This object is passed to all scheduled calls either as a specific parameter (Execute, Cancel, HandleFault) or as the sender (QueueItemAvailable handler, StatusChanged handler). The ActivityExecutionContext provides the activity writer with an interface for controling activity execution (hence the first two words in the name) while giving the runtime enough control to enforce the rules of the WF engine.
But what about the "Context" part of the name? A context in WF is a sphere of execution. There is a root activity for each context and only activities which exist in that context can be executed in that context. In short, a context is a mechanism used by the runtime to determine on which set of activities to enforce rules.
One important note is that the ActivityExecutionContext is merely a short-lived expression of the underlying context - every scheduled call to an activity method gets a new instance of the ActivityExecutionContext object which has been configured specifically for that activity. You'll notice, however, that the Guid associated with the context does not change.
Cloning Activities
Whenever a single activity needs to be executed multiple times it must be cloned. Contexts are the mechanism provided to the activity writer for making this happen. The code looks like this:
ActivityExecutionContext childContext = currentContext.ExecutionContextManager.CreateExecutionContext(childActivity);
This code will cause a new context, childContext, to be created with a root activity which is a clone of childActivity. Note that this is a deep cloning so if childActivity is a composite activity then its entire tree is cloned as well. Consider that we have a custom activity called WorkflowRoot which clones its only child activity using the above code. Visually, we now have the following tree of contexts:
RootContext | WorkflowRoot (1) | childActivity (1) | grandChildActivity (1) - childContext childActivity (2) grandChildActivity (2)
What?
Looking at the above diagram there are several questions which come up. Let's try to deal with a couple of easy ones first:
- Are changes to childActivity(1) or childActivity(2) reflected in the other instance?
No. Once cloned these instances have no connection. Changes to the template will affect future clones and changes to the clone will affect its own execution, but changes to one will not affect the other.
- What is the return value of childActivity(2).Parent?
WorkflowRoot(1). The activities inside a new context do not know that they are not part of the rest of the tree. The Parent property of the context's root activity still points to the original parent. It is only when walking down the tree that the context's are noticeable. For example, WorkflowRoot(1).Activities[0] will always return childActivity(1) and never childActivity(2). Said another way, childActivity(2).Parent.Activities[0] == childActivity(1). This is strange at first glance, but this soon becomes natural.
Scenarios
While Loop with Delay
Consider the following workflow:
WhileActivity DelayActivity
Not very useful, I'll admit, but it is handy for this demonstration. Now, if we have implemented this as a code only workflow, we'll probably have some field defined on our root called delayActivity1. Let's say that we want to change the delay amount each time through the loop, so we subscribe to the InitializeTimeoutDuration event with the following code:
// WRONG CODE this.delayActivity1.TimeoutDuration = TimeSpan.FromSeconds(iterationCount);
Assuming iterationCount is a variable that is incremented each time through the loop, we expect to see: delay 1 second, delay 2 seconds, delay 3 seconds, etc. This, however, is not what we see. Instead we get: delay 0 seconds, delay 1 seconds, delay 2 seconds, etc.
The reason is that the WhileActivity is spawning a new context when it executes the child. So, each iteration looks like this:
RootContext | WhileActivity(1) | Delay(1) - childContext Delay(1 + iterationCount)
this.delayActivity1 ALWAYS refers to Delay(1) and therefore we are updating the template every time InitializeTimeoutDuration is called. That means we are always one timeout amount behind ... Delay(2) is about to execute with TimeoutDuration set to 0 seconds and we update the template to 1 seconds. Delay(3) is created with a 1 second timeout because it is just a clone of the template at that point in time.
Some new code for InitializeTimeoutDuration:
// RIGHT CODE ((DelayActivity)sender).TimeoutDuration = TimeSpan.FromSeconds(iterationCount);
This time we will see the following: delay 1 seconds, delay 2 seconds, delay 3 seconds, etc. Here we have updated the cloned value instead of the template. Note that for ALL events subscribed to in code beside the sender will be the actual instance of the activity which is currently running.
Therefore, the sender above will always be the right one even if the delay is not in a context spawning activity. If you want to avoid issues, learn to access activity properties in a context safe way (like using the sender objects) so that you do it right when it counts. If the delay weren't in a context spawning activity then the WRONG CODE and the RIGHT CODE would be equivalent, but if the delay is in a context spawning activity then the WRONG CODE will never work.
Replicator and GetActivityByName
Replicator Sequence CallExternalMethodActivity HandleExternalEventActivity
The above workflow is a common pattern for replicated user tasks. The CallExternalMethodActivity notifies the user of the task and the HandleExternalEventActivity gets an event when the task is complete. Let's say that we're going to assign 3 tasks for UserA, UserB, and UserC so our replicator will initailize itself with the collection {"UserA", "UserB", "UserC"}. Assuming that the user name is the correlation parameter, our ChildInitialized handler might look like:
// WRONG CODE CallExternalMethodActiivty act = this.GetActivityByName("createTask1") as CallExternalMethodActivity; act.ParameterBindings["userName"].Value = e.InstanceData;
The code above will not work as expected. Let's look at why by examining the contexts created:
RootContext | Replicator (1) | Sequence(1) | CallExternalMethodActivity(1) | HandleExternalEventActivity(1) - childContext1 (e.InstanceData = "UserA") | Sequence(2) | CallExternalMethodActivity(2) | HandleExternalEventActivity(2) - childContext2 (e.InstanceData = "UserB") | Sequence(3) | CallExternalMethodActivity(3) | HandleExternalEventActivity(3) - childContext3 (e.InstanceData = "UserC") Sequence(4) CallExternalMethodActivity(4) HandleExternalEventActivity(4)
"this" in our code snippet refers to the root workflow which exists in the RootContext. When we call GetActivityByName and pass the CallExternalMethodActivity's name we will get the instance that is in the root context - CallExternalMethodActivity(1). What we want is the one in the current context so the code should look like:
// RIGHT CODE CallExternalMethodActiivty act = e.Activity.GetActivityByName("createTask1", true) as CallExternalMethodActivity; act.ParameterBindings["userName"].Value = e.InstanceData;
Note the two changes - first we use e.Activity instead of this. e.Activity is the clone of the replicator's template (Sequence(2-4)). Second, we have passed the parameter true to GetActivityByName. This tells the method to look only in the context of the activity on which it was called. This keeps the method from walking into other parts of the tree and returning the RootContext instance.
Conclusion
Hopefully this post eases some confusion around contexts and doesn't make it worse. Please post comments if you want clarifications on anything written above or if you want more information about one topic or another. I will write a separate entry at some point to discuss how to manage contexts you create in custom activities.
|
-
QUESTION: How do I get access to a transaction inside my workflow?
ANSWER: First of all, this post has nothing to do with flowing a transaction into a workflow instance. That is a completely different topic which we might cover at some other time.
TransactionScopeActivity
With that out of the way, there are two ways to get access to a transaction inside of your workflow. The first is through the TransactionScopeActivity activity. In simplest terms, a System.Transactions.Transaction is created at the start of the TransactionScopeActivity and every activity inside the TransactionScopeActivity will have a valid Transaction.Current.
At the close of the TransactionScopeActivity the transaction is either completed or aborted. Consider the following psuedo-code:
using (TransactionScope scope = new TransactionScope()) { DoSomething(); DoSomethingElse(); scope.Complete(); }
The equivalent workflow would look like:
<TransactionScopeActivity> <DoSomething> <DoSomethingElse> </TransactionScopeActivity>
In both cases if an exception is thrown which crosses the scope's boundary, the transaction is aborted. In workflow terms this means that if the exception does not find a matching handler on an activity INSIDE the transaction scope then the transaction is aborted.
Another similarity is that Transaction.Current is set on the thread in both cases. For the workflow it means that the DoSomething and DoSomethingElse activities will both find a transaction when executing any signal methods (Execute, Close, Cancel, HandleFault but NOT Initialize) as well as any handlers for status changes or queue item arrival.
Batching
While the rules of batching are enough to be a topic in their own right, we will touch on the beginnings of batching here. Stay tuned for future posts on this ...
Without getting into details, the following code will add a work item to the batch:
WorkflowEnvironment.WorkBatch.Add(somePendingWork, someWorkItem);
The somePendingWork object above must implement the IPendingWork interface and the someWorkItem is just a piece of data on which to do work; think of it the same way as you would the object state which you can optionally pass to ThreadPool.QueueUserWorkItem.
At the next commit point in the workflow, the runtime will call somePendingWork.Commit and pass a transaction and someWorkItem as parameters. Transaction.Current will also be set before the Commit method is called so that auto-enlisting calls will work correctly. Throwing an exception from the Commit method will cause the ENTIRE workflow persistence to fail and the exception will be thrown into the workflow for handling. If the Commit method returns without error this will be consider a successful vote for completion of the transaction. As long as everything in the batch succeeds, including persistence of the workflow itself, the transaction will be completed.
Once again, this is the simple version of how batching works. There are two other methods on IPendingWork as well as several rules about batch merging, batch compression, and batch clearing which will be covered in another post.
Conclusion
In conclusion, there are two ways to get a transaction in your workflow. You can model the transaction in your flow using the TransactionScopeActivity which works in a manner analogous to System.Transactions.TransactionScope. The other option is to delay the work until the next commit point and borrow the persistence transaction using batching.
|
-
QUESTION: What happens to a workflow instance I've created but never started?
ANSWER: Calling WorkflowRuntime.CreateWorkflow() will give you back a WorkflowInstance which, in essence, is a proxy to a runtime instance in the Created state. But what happens to that instance if it is never started?
Upon calling CreateWorkflow you have caused the runtime to create an in-memory instance of the workflow. This includes calling Initialize on the entire workflow tree as well as readying runtime resources like tracking channels. This in-memory instance will not "unload on idle" because the Created state is technically not idle ... only something that is running can become idle.
That means that the in-memory instance will remain in memory, consuming resources, until one of the following happens:
- WorkflowInstance.Start is called - If the workflow is started then it will begin to process and, as such, is subject to whatever unloading policy the host has implemented.
- WorkflowInstance.Abort is called - Abort causes the in-memory instance to be removed without any side effects on persistence. Therefore, if you have not forced your Created workflow to persist by calling Unload, you can safely call Abort to free up the resources and return to the same state as if you had never created the instance in the first place.
- WorkflowInstance.Unload is called - This will cause the Created instance to unload to the persistence service. The in-memory instance will be destroyed. Note that an unloaded, Created workflow will NOT start running when the runtime is started using our out of box SqlWorkflowPersistenceService. Only unblocked Started workflows will automatically continue processing at startup.
- WorkflowInstance.Terminate is called - This will cause the in-memory instance to be destroyed and the persistence service to be notified that the instance has been terminated. If the Created instance was never unloaded, then this will have the same net effect as Abort, but if it had been unloaded then Terminate should be called if you really want to get rid of the unloaded instance.
- WorkflowRuntime.Stop is called - When the WorkflowRuntime is stopped all the workflows are unloaded. That means that the Created workflow instance will be persisted just as if WorkflowInstance.Unload had been called.
- The process is exited without WorkflowRuntime.Stop being called - This is the same as Abort. If the instance was never persisted then the in-memory version is lost and it was as though the instance were never created. If the instance had been persisted then it can be retrieved and started at a later time.
In short, if you've created a workflow instance which you don't want to consume any more resources, call Terminate on it. This will make sure that it is taken out of memory as well as cleaned up from any persistence service.
|
-
Here are a few tips and tricks for using the ReplicatorActivity successfully. It is a powerful activity which, when approached from the correct point of view, can be relatively easy to use.
Definitions
seed value - a value added to either of the ReplicatorActivity's child collections. This value is associated with a single instance of the replicator's child.
template activity - the direct child of the replicator is referred to as the template activity. At replicator startup one instance of the template activity will be run for each seed value in the child data collection.
Tips and Tricks
Use your activity properties! Oftentimes users are confused as to where best to store the seed value. It is with great dismay that the question, "Do I have to create a custom activity just to store the value?" is asked. The answer is no, you don't have to create a custom activity just for value storage in some of the most common cases.
First, if your replicator is executing in Sequence and not Parallel then you can store the variable wherever you like, even outside the scope of the replicator. Despite the fact that the object containing the variable will not be cloned once per instance since you are executing in sequence, you will always only have one copy of the template activity active at any given time.
Second, consider what you are doing with the value? Chances are that the value you are using has some significance to one or more of the activities inside the template activity. If this is the case, then store the value directly on that activity's property. If more than one activity needs to access the data, then store it on one of them and use an ActivityBind to reference it on the rest of them.
Rarely you will find that the seed value is only used within the handler for a CodeActivity or that it must undergo some serious computation before it is settable to any of the activity properties. If this is the case, then you will need to create a custom activity which will act as your template activity. This should, however, be the rare case - perhaps instead your activities should be written to accept the seed value instead of some transformed version!
When is the UntilCondition evaluated? The UntilCondition is evaluated in two places; it is evaluated once before any activities are executed and once after each instance of the template activity closes. Note that if it returns true when the replicator first starts executing then the replicator will close immediately and will not execute any children. Also, any children still executing when the UntilCondition evaluates to true will be cancelled.
How can I tell which child just closed when the UntilCondition is evaluated? Unfortunately, there is no built in mechanism for this; the UntilCondition does not accept parameters and there is no global nor ReplicatorActivity defined accessor for this kind of data. There is, however, a suitable workaround for determining the newly completed activity.
At a child's completion the replicator will first raise the ChildCompleted event and then evaluate the UntilCondition. Since these two actions occur in the same method and Windows Workflow Foundation guarantees a single thread per workflow instance (see post on Parallelism) you can be sure that no other chlidren will execute between the event handler running and the condition evaluation.
That said, some identifying data from the completed child can be stored in a well known location (variable on the root activity, property of some other activity, etc) during the ChildCompleted handler and read from the location by the UntilCondition.
|
-
QUESTION: Does a ParallelActivity start one thread for each branch? How many threads will it use for processing?
ANSWER: The short answers are "no" and "1". If you've heard it before, then these answers make sense. If not, then read on for an explanation of parallelism in WF.
To promote programming simplicity for both the custom activity and workflow writer, WF has chosen to make the guarantee that only one .NET thread will be executing any portion of a workflow at any given time. This means that the handler for your CodeActivity, the Execute method for your custom activity, and the sequence in your EventHandlerActivity will never have to worry that some other part of the workflow is executing at the same time. If you think about it a bit you will see that this greatly simplifies programming WF applications and is one of the few reasons why writing complex custom activities is a surprisingly simple task.
Isn't that bad?
The usual knee-jerk reaction is outcry about the loss of multi-threaded parallelism. Isn't that a step backward? The answer is no, this is not a step backward. First, we are only stating that there is a single threaded nature within a single instance of a workflow. This means that if you have two instances of the same workflow executing simultaneously then they will exhibit true multi-threaded parallelism.
Second, let's remember the nature of the workflow beast. Workflows are meant to coordinate tasks, both human and computer, in an event driven world over an unknown amount of time. The workflow itself processes in short bursts with long periods of dormancy in between. For example, a workflow might send an e-mail requesting that a task be performed and then persist to the database. Only when the task is now complete will the workflow come back to life and process some more ... and this processing should be limited to deciding what action should be taken next and delegating that to some external source whether that be a human or a service added to the WorkflowRuntime.
This behavior of a single workflow instance means that true parallelism is wholy unnecessary. If the workflow assigns 10 tasks in parallel then it is highly unlikely that there will be a processing bottleneck when collating the results which return scattered across time. While I've got no hard evidence to back this up, it is my opinion that a single thread per instance actually improves the performance of WF as opposed to hindering it when considering the reduced complexity in activity execution code, the lack of neccessity for locking constructs, and the burst processing nature of workflow.
How it works
WF is a scheduled environment. Abstractly, you can consider that every instance of a workflow has its own scheduler which is just a queue of delegates. The scheduler simply loops through a 2 step sequence: dequeue the next delegate and call it. If there are no more items on the queue then the workflow is idle. If we consider that the scheduler just has one thread and invokes the delegates synchronously then we see where the single threaded guarantee comes from.
During the execution of a delegate (like Activity.Execute) there are several occurrences which can cause new items to be added to the scheduler queue. ActivityExecutionContext.ExecuteActivity() can be used to schedule a child's execution, throwing an exception will cause the runtime to schedule the HandleFault method for the activity, calling Activity.Invoke<>() will cause the specified delegate to be scheduled, and returning a value of ActivityExecutionStatus.Closed will cause the runtime to schedule the OnClosed method. These are just some of the triggers which cause new items to be added to the queue.
Extended ParallelActivity walkthrough
The ParallelActivity, when executed, will schedule the Activity.Execute method for each of its direct children and subscribe to the Closed event for each child. The result is that the scheduler queue will look something like this (first item to be dequeued is on left): {child1.Execute, child2.Execute, child3.Execute}
Calling child1.Execute might result in a DelayActivity's Execute to be added to the queue: {child2.Execute, child3.Execute, delay1.Execute}
Now consider if child2 contains a single CodeActivity and child3 is empty: {delay1.Execute, code1.Execute, child3.OnClose}
Up to this point we have had purely interleaved execution. Draw it out and remember that in normal execution an activity will have Execute schedule, then it will schedule any work it needs to do, then it will have OnClose scheduled, and then anyone listening to the Closed event will be scheduled. Knowing this you can walk through almost any chain of activities.
Back to our queue, we will next see the delay disappear because it has added a timer to the workflow, we will see the code activity execute, and the third child will process its close: {code1.OnClose, parallel.OnChildClosed(child3)}
Without drawing it out, let's say that the delay was a long enough one to let child2 close as well before the timer is fired. The result will be that the ParallelActivity's Closed handler will determine that the parallel cannot yet close because it has an outstanding executing child and the scheduler will run out of items in the queue and mark the workflow as idle. The next exciting thing to happen is the timer will schedule a callback for the delay: {delay1.OnTimer}
Again, this will cause child1.OnClose to be scheduled which will cause parallel.OnChildClosed(child1) to be scheduled which, finally, will result in parallel.OnClose being scheduled.
The important thing to notice is that with non-blocking activities we get predictable interleaved execution. But, as soon as we add a blocking activity, the delay in our case, we get execution that approaches real world workflow scenarios. Imagine that each branch has an event on which it is waiting ... the execution is no longer just interleaved, but whichever branch's event fires first gets executed first. Unless all of the branches receive their events simultaneously, we get parallel processing with a single thread of execution.
|
-
QUESTION: Can I return ActivityExecutionStatus.Faulting from a signal method to "fault" the activity?
ANSWER: As a general rule only the current status and Closed are valid return values from any of the signal methods. Therefore:
Execute() – Return values of ActivityExecutionStatus.Executing and Closed valid. Cancel() – Return values of ActivityExecutionStatus.Canceling and Closed valid. HandleFault() – Return values of ActivityExecutionStatus.Faulting and Closed valid. Compensate() – Return values of ActivityExecutionStatus.Compensating and Closed valid.
If an invalid value is returned from a singal method then the runtime will throw an InvalidOperationException which will propagate asynchronously just like any other exception occuring within the workflow.
If you are in any signal method and would like to “fault” the activity then simply throw an exception from the method. For example:
- Execute() is called on the activity and the activity logic determines there is an error. An exception is thrown from execute.
- The runtime will then capture the exception and pass it to the HandleFault() method of that activity. If HandleFault returns a value of Faulting then the runtime will simply continue processing the scheduler queue expecting that at some later point the activity will signal closed (see below). If HandleFault returns Closed then the runtime knows that the activity has done what it needs to handle the fault.
- If the activity in question is composite and has a FaultHandlersActivity with a matching FaultHandler then the exception will be considered handled and will be passed to the appropriate FaultHandler. Otherwise, the exception will be passed to the parent activity’s HandleFault method and the cycle will repeat until either the fault has found a FaultHandlerActivity which matches or has left the scope of the workflow causing it to Terminate.
When would an activity want to return Faulting? Consider the case where ParallelActivity has its HandleFault method called. The parallel may have one or more currently executing branches which do not yet know about the exception. At this time, the parallel will attempt to cancel the children by subscribing to the closed status change and calling ActivityExecutionContext.CancelActivity for each child. The return value from HandleFault will be ActivityExecutionStatus.Faulting because the parallel should not closed until all of its children have cancelled successfully. Once the parallel has received close notification from each child it can then call ActivityExecutionContext.CloseActivity() to signal its own completion and cause the asynchronous exception handling to resume.
|
-
QUESTION: How do I manage resources (like sockets) which are held by an activity?
ANSWER: System.Workflow.ComponentModel.Activity has several overridable methods defined, but there is a newer subset of these methods which I like to refer to as "Lifetime" methods. These are methods that bookend the activity's life from some point of view. They are:
OnActivityExecutionContextLoad / OnActivityExecutionContextUnload / Dispose
Initialize (also a signal method) / Uninitialize
Short Version
The short version is that OnActivityExecutionContextLoad and OnActivityExecutionContextUnload bookend the activity's memory lifetime as far as the Windows Workflow Foundation (WF) runtime is concerned. OnActivityExecutionContextLoad will be called just after an activity is loaded into the runtime's memory and OnActivityExecutionContextUnload will be called just before an activity is persisted to the database.
OnActivityExecutionContextLoad also has a component of being paired with Dispose to represent the activity's memory lifetime as it applies to the CLR. Activities which run almost never have their constructors called directly but instead these activities are deserialized from a stream. That means that OnActivityContextLoad is really the only safe place to do object creation time resource allocation. Dispose is called before an object is released for garbage collection by the runtime and could be used for NonSerialized activity fields which need to be closed, disposed, or cleaned up in any other way.
Initialize and Uninitialize are the markers for an activity's execution lifetime. For the rest of this paragraph any reference to an "activity instance" will have NOTHING to do with its CLR lifetime. Since workflows are unloaded and loaded numerous times during execution a single "activity instance" could actually have been executed through the use of dozens of CLR object instances of the activity. That said, before an "activity instance" will take part in any of the protocol signals it will be initialized; this includes the initialization of cloned activities which execute in new contexts. Once an "activity instance" is done taking part in the protocol it will have Uninitialize called.
Long Version
Before an activity does anything (IE – when it is loaded into memory within the runtime’s scope) OnActivityExecutionContextLoad will be called. Initialize is called on every activity in the workflow tree at workflow Create time. Initialize is also called for every clone in a context spawning activity when the clone is spawned. You can tell that you are a cloned activity because IsDynamicActivity will be set to true.
OnActivityExecutionContextUnload is called before an activity is saved to the database. This means that if the workflow is unloading then this method will be called … additionally if a context is being saved for future compensation then this method will be called on all contained activities. Note that PersistOnClose does not cause OnActivityExecutionContextUnload to be called for the workflow as a whole because the workflow object is not being jettisoned from memory.
Dispose is called when we are done with the .NET object representing the activity. So, in the workflow unload case, we will call OnActivityExecutionContextUnload immediately followed by Dispose. In the case of PersistOnClose we will NOT call Dispose on any activities. Only when the activity is being removed from memory will Dispose be called.
Uninitialize has two cases. 1) The activity does not have any compensation responsibilities. 2) The activity has to remain “valid” for compensation. Essentially, Uninitialize is the WF approximation of .NETs Dispose … when we think an activity will never be called upon to take part in the activity protocol again then we will Uninitialize it. This shows up as a new flag on ActivityExecutionResult.
For activities which fall in category (1) Uninitialize is called immediately after the activity closes. Any primitive activity which does not implement ICompensatableActivity has this behavior. Additionally, any composite activity which has NO children, grandchildren, great grandchildren, etc. which implement ICompensatable will be uninitialized immediately as well. However, if an activity or any of its children or sub children implement ICompensatableActivity then the activity will NOT be Uninitialized until the compensatable activity has either been compensated or has been determined that it cannot ever be compensated (the workflow terminates or completes). For example, if I have the following:
While Sequence Code MyCompensatableActivity
Code will uninitialize immediately. MyCompensatableActivity, Sequence, and While will NOT be able to uninitialize until either MyCompensatable activity compensates or the workflow completes and disposes of the compensatable activity.
Finally, the reason for the caveat that Uninitialize is an “approximation” of .NET’s Dispose is because you can execute disposed activities again. It is possible to “rerun” an activity which has executed and closed in another context. Using a Page Flow example, imagine that you have an activity which represents Page3. The user enters data into Page3 which you save as fields on your activity. They then proceed to Page4 causing WF to close Page3 and then call Uninitialize. It is possible to model the user pressing the Back button by simply creating a new context and passing the closed Page3 object from the old context as the template activity. This will cause the Page3 object to have Initialize called again, but all of the data will be intact just as it was left at the return of the previous Uninitialize call.
Hope that helps clear up some of the hooks for acquiring and releasing resources.
|
-
Is there anything about Windows Workflow Foundation (WF) that you would like to know and feel is an appropriate topic for this blog? If so, please add a comment to this post. I will use the comments as seeds for future postings. Even if someone has already posted your specific question, please add your own comment as well so that I can do a better job prioritizing.
Note that posting here does not in any way guarantee that I will respond, but I will try to do the best that I can. Another good resource for general WF questions is the http://www.windowsworkflow.net. This site has articles and samples of its own as well as links to other blogs and related MSDN forums.
|
-
The purpose of this blog is to provide public answers to the Windows Workflow Foundation questions that I come across every now and again at work. These questions usually target a specific scenario but can be easily abstracted to a more general inquiry of "how to accomplish X" or "why does Y happen" or "what is the purpose of Z". The end goal is to provide a quick reference for the solutions to tricky problems that custom activity writers and complex scenario developers will face during design and implementation. As such, welcome to Advanced Workflow: Enabling Tricky Scenarios.
About the author: My name is Nathan Talbert and I work for the Windows Workflow Foundation team at Microsoft. I've been working closely with the custom activity and core runtime aspects of our product for quite a while now and hope to share some of the knowledge I've gained with our customers. I enjoy any feedback which helps me to that end, so please feel free to comment on content, writing style, and accuracy.
|
|
|
|