locked
BookMark usage question. RRS feed

  • Question

  • Hi all,

    What I am attempting do accomplish here is to do some long running execution on another thread. However The BookmarkCompleted event never gets hit. (It does get hit if I make the thread sleep while its computing, but this kind of defeats the point.)

    Unload instances when idle is set to false. And Persist  instances when idle is set to true.  

     

    Has anyone any ideas on what I'm doing wrong? Or perhaps how I should be doing this?

     

     

    private Bookmark _bookmark;
            protected override void Execute(NativeActivityContext context)
            {              // configure the helper object to access the database dbHelper = new DatabaseHelper(); dbHelper.GetConnectionStringFromExecutionProperties(context, this); dbHelper.ConfigName = this.ConfigName.Get(context); dbHelper.Sql = this.Sql.Get(context); dbHelper.CommandType = this.CommandType; dbHelper.Parameters = this.parameters; dbHelper.Init(context);  GetSqlDataExtension ext = context.GetExtension<GetSqlDataExtension>();             if (ext == null)
                    throw new InvalidOperationException("This activity requires a GetDataSetExtension");

                
                this._bookmark = context.CreateBookmark(GetSqlDataExtension.GetSQLExecuteUpdateBookmark, new BookmarkCallback(BookmarkCompleted));
                ext.ExecuteUpdate(this._bookmark, dbHelper);
                //System.Threading.Thread.Sleep(10000);
            }

     

    protected void BookmarkCompleted(NativeActivityContext context, Bookmark bookmark, object value)
            {
                this.AffectedRecords.Set(context, (int)value);            
                dbHelper.Dispose();
            }

     

    public void ExecuteUpdate(Bookmark bookmark, DatabaseHelper dbHelper)
            {
                System.Threading.ThreadPool.QueueUserWorkItem(
                    (Instance) =>
                    {
                        int rowsAffected = dbHelper.Execute();

                        (Instance as System.Activities.Hosting.WorkflowInstanceProxy).BeginResumeBookmark(bookmark, rowsAffected, (ticket) =>
                        {
                            (Instance as System.Activities.Hosting.WorkflowInstanceProxy).EndResumeBookmark(ticket);
                        }, null);

                    }, this.Instance
                );
            }

     


    Greg
    Sunday, November 14, 2010 10:25 PM

Answers

  • Hi Tim, thanks for that.

    I repro'd your repro and it worked fine, then I ported my activity and associated classes and it worked fine as well. So I started digging in the web.config and noticed this in my service behaviour.

    <workflowIdle timeToUnload="00:00:00.001" timeToPersist="10675199.02:48:05.4775807"/>

    Upping the timeToUnload time makes it work. 

    http://msdn.microsoft.com/en-us/library/ee342461.aspx show how you can resume from workflow application and http://msdn.microsoft.com/en-us/library/ee834522.aspx shows how I can resume it if hosted in a WorkflowServicehost. I'm not sure how to do this from code when my workflow is hosted in AppFabric and I have a WorkflowInstanceProxy?


    Greg
    Tuesday, November 30, 2010 1:06 AM
  • I'm thinking that the simplest solution is just to prevent persist/unload altogether while your async task is running, which is what AsyncCodeActivity does. I'm not quite sure which is the best way to do that.

    However, before getting into that further, Noticing you say you have persist on idle set to true - do you have specific activity persistence behavior in mind?

    Tim

    Wednesday, December 1, 2010 1:00 AM

All replies

  • If you purpose is to have a long running operation but you don't want to block the thread, I think AsynCodeActivity would be a better fit for your need (and simpler logically too).  Using Bookmark to implement this async logic is not the best use.

    You can look at this sample http://msdn.microsoft.com/en-us/library/ee358731.aspx and I think it would fit your purpose.

    Isaac

    Monday, November 15, 2010 9:44 AM
  • Hi Greg,

    I'm not sure i understand your problem.

    You are trying to call asynch ExecuteUpdate and adding a bookmark. And the ExecuteUpdate could resume the same bookmark?


    Jérémy Jeanson MVP, MCP, MCTS http://blogs.codes-sources.com/JeremyJeanson/ (French or English spoken)
    Monday, November 15, 2010 9:58 AM
  • Hi Isaac,

     

    Originally we did use the AsyncCodeActivity, but we needed access to execution properties and thus switched to a native activity.

     

    Greg


    Greg
    Tuesday, November 16, 2010 12:45 AM
  • Hi Jeremy

     

    ExecuteUpdate is just a method in our own class GetSqlDataExtension. What I'm trying to do is add a bookmark, then call my execute update method, which then creates another thread that does the long running process. That thread then resumes the workflow.

     

    Cheers,

    Greg


    Greg
    Tuesday, November 16, 2010 12:47 AM
  • bump
    Greg
    Monday, November 22, 2010 2:31 AM
  • Greg, is your delegate doing BeginResume getting hit? How about EndResume?
    Tim
    Monday, November 22, 2010 6:22 PM
  • Hi Tim,

    BeginResume and EndResume are getting hit. However the BookmarkCompleted method never gets hit after that.


    Greg
    Monday, November 22, 2010 9:02 PM
  • Does EndResume return ResumeBookmarkResult.Succeeded?

    I tried a repro project where I don't do anything on idle. The BookmarkCompleted method does get hit, fine.

    I haven't yet figured out why in my repro I am getting the Idle event and I never get a PersistableIdle event, but maybe it is just that I have not set up a persistance provider...

    Tim

    Monday, November 22, 2010 10:11 PM
  • Hi Tim,

     

    Yeah I now see the EndResume returns BookMarkResumptionResult.NotReady.  from msdn: (The bookmark resumption was not scheduled because the runtime has not yet created the bookmark. The host should wait until another idle point to try resumption again.)

     

    When I look at my Workflow in AppFabric, there are no errors being reported. My workflow is reported as Idle, when I drill into that the overview for the Idle Workflow has an Active Bookmark with my bookmark name.


    Greg
    Tuesday, November 23, 2010 1:47 AM
  • A few comments:

    1) Is there a reason you preferred execution properties over an explicit argument to the activity?

    2) Is there a reason you decided to use Bookmarks here instead of Send/Receive? Are you not using WorkflowServiceHost?

    3) When playing with bookmarks directly (as opposed to through the various WCF helpers), you need to handle NotReady, and try again. I suspect in your case you are racing between the instance Unloading from memory and your bookmark resumption

    Tuesday, November 23, 2010 8:26 PM
  • Hi Kenny, 

    thanks for your response.

     

    1. We do use arguments, but also execution properties as the other activity provides the data we need as such. We can't change this sorry.

    2. We are using WorkflowServiceHost, but not sure why we would need a Send/Recieve as we are just trying to access a database (CRUD operations) and do not want that to hold things up.

    3. To be honest I'm not sure how to handle the NotReady. EndResume is an async method so I can only call it once. Calling it checking the result, and calling it again results in End cannot be called twice on an AsyncResult.

     

    Tim, can I get hold of that repro you did? Perhaps it will help me help myself?


    Greg
    Thursday, November 25, 2010 8:30 PM
  • Hi Greg,
    Here's my simplified version of your scenario, not very faithful as repros go, as it is not running as workflow service or in appfabric:

    public class GetSqlDataExtension : IWorkflowInstanceExtension
      {
        WorkflowInstanceProxy instance;
    
        public IEnumerable<object> GetAdditionalExtensions()
        {
     	    yield break;
        }
    
        public void SetInstance(WorkflowInstanceProxy instance)
        {
     	    this.instance = instance;
        }
    
        public void ExecuteUpdate(Bookmark bookmark, object helper)
        {
          System.Threading.ThreadPool.QueueUserWorkItem(
            (Instance) =>
            {
              System.Threading.Thread.Sleep(5000);
              int rowsAffected = 1;
              (Instance as System.Activities.Hosting.WorkflowInstanceProxy).BeginResumeBookmark(bookmark, rowsAffected, (ticket) =>
              {
                (Instance as System.Activities.Hosting.WorkflowInstanceProxy).EndResumeBookmark(ticket);
              }, 
              null);
            }, 
            this.instance
          );
        }
        
        public static string GetSQLExecuteUpdateBookmark = "GetSQLExecuteUpdateBookmark";
      }
    
      public class CustomAsyncActivity : NativeActivity
      {
        private Bookmark _bookmark;
        private Variable<int> AffectedRecords;
    
        public CustomAsyncActivity()
        {
          AffectedRecords = new Variable<int>("AffectedRecords");
        }
    
        protected override void Execute(NativeActivityContext context)
        {
          GetSqlDataExtension ext = context.GetExtension<GetSqlDataExtension>();
          if (ext == null)
          {
            throw new InvalidOperationException("This activity requires a GetDataSetExtension");
          }
    
          this._bookmark = context.CreateBookmark(GetSqlDataExtension.GetSQLExecuteUpdateBookmark, new BookmarkCallback(BookmarkCompleted));
          ext.ExecuteUpdate(this._bookmark, null);
        }
    
        protected void BookmarkCompleted(NativeActivityContext context, Bookmark bookmark, object value)
        {
          this.AffectedRecords.Set(context, (int)value);
        }
    
        protected override bool CanInduceIdle
        {
          get
          {
            return true;
          }
        }
    
        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
          base.CacheMetadata(metadata);
          metadata.AddImplementationVariable(this.AffectedRecords);
          metadata.AddDefaultExtensionProvider<GetSqlDataExtension>(() => new GetSqlDataExtension());
        }
    
      }
    
    public static void Main()
        {
          Activity wf = new Sequence
          {
            Activities =
           {
             new WriteLine
             {
               Text = "Starting the workflow."
             },
             new CustomAsyncActivity
             {
             },
             new WriteLine
             {
               Text = "Ending the workflow."
             }
           }
          };
    
          // Create a WorkflowApplication instance.
          WorkflowApplication wfApp = new WorkflowApplication(wf);
    
          // Subscribe to any desired workflow lifecycle events.
          wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
          {
            if (e.CompletionState == ActivityInstanceState.Faulted)
            {
              Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
              Console.WriteLine("Exception: {0}\n{1}",
                e.TerminationException.GetType().FullName,
                e.TerminationException.Message);
            }
            else if (e.CompletionState == ActivityInstanceState.Canceled)
            {
              Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
            }
            else
            {
              Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    
              // Outputs can be retrieved from the Outputs dictionary,
              // keyed by argument name.
              // Console.WriteLine("The winner is {0}.", e.Outputs["Winner"]);
            }
          };
    
          wfApp.Aborted = delegate(WorkflowApplicationAbortedEventArgs e)
          {
            // Display the exception that caused the workflow
            // to abort.
            Console.WriteLine("Workflow {0} Aborted.", e.InstanceId);
            Console.WriteLine("Exception: {0}\n{1}",
              e.Reason.GetType().FullName,
              e.Reason.Message);
          };
    
          wfApp.Idle = delegate(WorkflowApplicationIdleEventArgs e)
          {
            // Perform any processing that should occur
            // when a workflow goes idle. If the workflow can persist,
            // both Idle and PersistableIdle are called in that order.
            Console.WriteLine("Workflow {0} Idle.", e.InstanceId);
          };
    
          wfApp.PersistableIdle = delegate(WorkflowApplicationIdleEventArgs e)
          {
            // Instruct the runtime to persist and unload the workflow
            return PersistableIdleAction.Unload;
          };
    
          wfApp.Unloaded = delegate(WorkflowApplicationEventArgs e)
          {
            Console.WriteLine("Workflow {0} Unloaded.", e.InstanceId);
          };
    
          wfApp.OnUnhandledException = delegate(WorkflowApplicationUnhandledExceptionEventArgs e)
          {
            // Display the unhandled exception.
            Console.WriteLine("OnUnhandledException in Workflow {0}\n{1}",
              e.InstanceId, e.UnhandledException.Message);
    
            Console.WriteLine("ExceptionSource: {0} - {1}",
              e.ExceptionSource.DisplayName, e.ExceptionSourceInstanceId);
    
            // Instruct the runtime to terminate the workflow.
            // Other choices are Abort and Cancel
            return UnhandledExceptionAction.Terminate;
          };
    
          // Run the workflow.
          wfApp.Run();
    
          Console.ReadLine();
        }
    

    Not sure if it can help, but if it does that's great.

    Tim

    Friday, November 26, 2010 2:28 AM
  • Hi Tim, thanks for that.

    I repro'd your repro and it worked fine, then I ported my activity and associated classes and it worked fine as well. So I started digging in the web.config and noticed this in my service behaviour.

    <workflowIdle timeToUnload="00:00:00.001" timeToPersist="10675199.02:48:05.4775807"/>

    Upping the timeToUnload time makes it work. 

    http://msdn.microsoft.com/en-us/library/ee342461.aspx show how you can resume from workflow application and http://msdn.microsoft.com/en-us/library/ee834522.aspx shows how I can resume it if hosted in a WorkflowServicehost. I'm not sure how to do this from code when my workflow is hosted in AppFabric and I have a WorkflowInstanceProxy?


    Greg
    Tuesday, November 30, 2010 1:06 AM
  • I'm thinking that the simplest solution is just to prevent persist/unload altogether while your async task is running, which is what AsyncCodeActivity does. I'm not quite sure which is the best way to do that.

    However, before getting into that further, Noticing you say you have persist on idle set to true - do you have specific activity persistence behavior in mind?

    Tim

    Wednesday, December 1, 2010 1:00 AM