Saturday, December 8, 2007

Cider, Workflows and Just Enough Knowledge...

As I recall, it was out in the country somewhere...  The kind of place you can't find without having been there before.  The sweet, musty scent of a single room log cabin that hasn't seen a carbon based life form larger than a raccoon for well over 20 years.  The only light coming from the soft blue glow of the power L.E.D from the wireless access point... Hmm...  Wait a second. I'm not remembering quite right...  Might have been a room at the DoubleTree in San Jose.

On the road and armed with Peanut M&M's, a case of Hornsby's Hard Apple Cider, and some book we'd just purchased from the local techno-geek bookshop, Mr. Turner and I decided to implement our first Windows Workflow Based solution at a client.  Start simple, was our motto.  So we did.  We took one of simplest concepts we could come up with and decided to implement it in the most complex and convoluted way possible.  All in the name of progress.  We had chosen to build a workflow based delayed events Management Agent for MIIS.

Brad's knowledge of the inner workings of MIIS is intense, and I have the ability to write code so concise that a popular compression algorithm, in a fit of jealousy and frustration, locked itself in the inner sanctum of my display driver and refuses to come out.  (The proof is in the dead pixel in the middle of the screen on my laptop.)  And while the intricacies of marrying these two skill sets have evoked solicitations from the Hollywood screenwriting elite, that's not what this blog is about.  That's just a bit of background.  The history.  A peek behind the wizard's curtain, if you will...

What I gathered you all here to talk about is this:  Never drop a goldfish into a glass of vodka.  Okay?  No, not even if it's the good vodka like the kind that comes in the fancy frosted bottle with that guy's face that you can see through the small patch of clear glass on the inside of the other side of the bottle.  (How do they get that guy in there?)

And speaking of goldfish, here's a little story on how I was blind-sided by a .Net assembly versioning conflict...

I did build that workflow engine.  'Twas my first foray into the world of Windows Workflow Foundation and I had just enough knowledge to make it all work, but not enough to understand what I was doing.  The solution consisted of four projects: The workflow engine service, the workflows assembly, a utility project and a setup project.  The workflows, themselves, were pretty simple: Delay, Notify, Delay.  Short and sweet.  The workflow engine was implemented as a service and contains the standard SQL based persistence service, the standard tracking service and an External Data Exchange service to allow the workflows to notify the host of certain events.  All of the versioning was set to auto increment the build and revision.

After creating the workflows and getting the service built and running and everything tested to a point of satisfaction, the service was rolled out and a production WorkflowMA was born.  And it was good.  But not good enough.  Had to tweak the service a bit and ended up having to deploy a couple of replacement versions.

These were long running workflows, delaying for up to 90 days at a time.  So it was quite a while before I was enlightened unto the error of my ways.  After a while we realized that things were not processing as they did in testing.  (You see, in testing, I used 90 seconds, not 90 days.  Deadlines, you know...)  So I'll skip past the details here and get to the stuff that matters...

I used strong named assemblies.  That's important.  If I hadn't, I might not have had any of these issues.  But then I couldn't have deployed signed code, either.

What was happening was that workflows were getting stuck in the persistence service.  While I didn't make changes to the workflows, I had made updates to the service.  But I always redeployed the full setup.  And when I did a full solution recompile, I inadvertently changed the version numbers of the workflows.  So when the persistence service tried to re-hydrate them, it failed as the appropriately versioned workflow classes were not available.  .Net will not automatically use a newer version of a strong named assembly.  They just sat there in the persistence store.  Orphans.

How to fix it?  Well, my first attempt at a workaround was to use a binding redirect in the app.config:

Code
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="MIISWorkflowLib" publicKeyToken="123abc456def7890" culture="neutral" />
        <bindingRedirect oldVersion="1.0.0.0-1.0.9999.0" newVersion="1.1.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Just specify a version range big enough to cover all the previous versions and redirect them to the one new and latest version.

Awesome!  The old workflows rehydrated using the new assembly.  But then the tracking service complained:

Value cannot be null. Parameter name: profile

There was a mismatch in the profile information in the serialized workflow and the tracking database records.  Another exception.  It was like the parents had come back to claim their orphaned little workflow, but they didn't have proper ID.  Couldn't prove they were they rightful owners, so the WWF authorities intercepted the happy reunion, again leaving the workflow cold and naked in the persistence store.  (Why naked, you ask?  More dramatic.) 

How to fix it?  Well, you can look at the tracking database (examine the WorkflowDefinition column of the Workflow table) and see the version numbers of the workflows that it's cataloged.  (If the WorkflowTypeID doesn't match any records in the WorkflowInstance table, you can probably skip that version.  No workflows were actually created from that assembly.)  Recompile a workflow assembly for each version, updating the AssemblyVersion before compile and copying the compiled assembly to a subfolder structure under the host's startup folder.  Then use codebase hints in the app.config file to tell the host where each version of the assembly lives.  (For some reason, I didn't use the GAC.  If you do, you can just dump each version there and be done with it.  But my solution required more typing, so it must be better.)

Folder Hierarchy:

Folder Hierarchy 

app.config:

Code
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        <dependentAssembly>
            <assemblyIdentity name="MIISWorkflowLib" publicKeyToken="123abc456def7890" culture="neutral" version="1.0.2701.23729" />
            <codeBase version="1.0.2701.23729" href="Workflows\1_0_2701_23729\MIISWorkflowLib.dll" />
        </dependentAssembly>
        <dependentAssembly>
            <assemblyIdentity name="MIISWorkflowLib" publicKeyToken="123abc456def7890" culture="neutral" version="1.0.0.0" />
            <codeBase version="1.0.0.0" href="Workflows\1_0_0_0\MIISWorkflowLib.dll" />
        </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Awesomer!  The workflows can re-hydrate and that finicky tracking service is no longer complaining.  But then one more little demon wielded its ugly head.  The ExternalDataExchange service.  See, I was using that to allow the workflows to chat with the host application.  When I put this project together I only had one workflow assembly.  I added the required interface definition to that project and just referenced it from the workflow host application.  In Visual Studio, I set a reference to the workflow project in the host application project.  This allowed me access to that interface.  But post-fix there were multiple versions of the workflow assembly.  You can't easily reference more than one assembly with the same name.  (With Reflection, all things are possible...  Well, many things.)  And I needed a reference to an interface my class would implement, not a class already defined in the other assembly.

How to fix it?  Well, this took me while to figure out.  Not because it's an especially difficult problem, really, but because I expected it to be.  And if you're not looking for a simple solution, I can promise that you won't find one.  All I had to do here was separate the interface definition from the workflow definition.  I couldn't add the interface to the host app because the host app already had a reference to the workflow app.  If I put the interface there, the workflow app would need a reference to the host app.  That's what they call a circular dependency and, in some states, that's a felony.  (Well, Visual Studio won't let you do it, anyway.)  So enter project number five, consisting of only the interface definition.  Reference the new project from both the host application and the workflow assembly and... Viola! A complete and working solution.  (Awesomest!)

Now...  The real lesson here is not how to fix this situation - it's that you should avoid it all to begin with.  I was so focused on messing with the new workflow gizmos that I just didn't think through the peripheral .Net stuff.  Lesson learned and In the solution, now, the workflow assembly project no longer auto-increments it's version.  Yeah...  It would have been that simple.

2 comments:

Stanislav Perekrestov said...

Good article. Thanks.
I fought with that :) Your solution is identical to mine one.

Anonymous said...

Nice article. I was pulling my hair out trying to figure that error out. After reading your article, I took a closer look at my project and it turns out one of my coworkers set my assembly to auto-increment.

Thanks! If not for this article I probably would never have figured it out.