Workflow之打造RetryActivity

1、前言

    .net Framework 3.0的Workflow用过了吧,什么?还没有,好吧,就连我这种当初认为Workflow是个不值得花时间去学习的人也用了一下,毕竟在某些情况下,使用WF的编码效率以及灵活性远要比不使用WF的要高。

2、场景

    比如说,现在需要做个异步的服务,其中有调用了很多其他服务,并且这些服务是远程的,也就是可能在很多阶段都回出现调用失败的情况,当然,由于服务本身是异步的,那么不太可能遇到一个失败就把这个过程都设为失败,要是这样的话,如果每个服务的失败概率是5%,如果过程中有10个这样的服务,那么总体的失败率就达到了40%,显然这个失败率是无法接受的。

    如果按照传统的手段实现重试操作,那么,需要把每一步都记录到数据库或消息队列中,这样在每一步都需要自己写持久化和再次加载的方法,如果对象类型较少,那也不算麻烦,如果对象多了,那就有点吃不消了。

    这里就可以让Workflow大显身手了,在Workflow里面,只需要放一个While、一个IfElse和一个Delay,以及需要Retry的活动本身,再准备一个持久化服务,一个带有Retry功能的服务就自动完成了。

    太抽象了?看图:

1

    其中的codeActivitiy1部分就是那些可能失败的操作,然后只要设置好While和IfElse的条件,以及延迟的时间,那么这个自动重试就可以工作了。

    只要WF中的持久化服务能工作,那么这个过程中无论Delay多少时间,都不用担心内存问题,也不用担心怎么持久化中间的对象,需要确保的仅仅是这些状态是可以序列化的。

3、更好的方案

    上面的方案看起来不错吧,不过要真正用起来,就会发现太麻烦,只要有一个需要Retry的活动,就需要把这个结构再画一把,画上10个保证想劈了显示器。

    为了避免重复拖拽出这个相似的结构,就自己写一个RetryActivity吧,在从零开始写这个活动的时候,建议参考WhileActivity和DelayActivity的实现,当然,也可以比较偷懒的直接copy这里的实现:

   1:      [Designer(typeof(RetryDesigner), typeof(IDesigner))]
   2:      public partial class RetryActivity
   3:          : CompositeActivity, IEventActivity,
   4:          IActivityEventListener<ActivityExecutionStatusChangedEventArgs>,
   5:          IActivityEventListener<QueueEventArgs>
   6:      {
   7:   
   8:          #region Sub-Classes
   9:   
  10:          private sealed class TimeoutDurationConverter : TypeConverter
  11:          {
  12:   
  13:              public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  14:              {
  15:                  return ((sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType));
  16:              }
  17:   
  18:              public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
  19:              {
  20:                  return ((destinationType == typeof(string)) || base.CanConvertTo(context, destinationType));
  21:              }
  22:   
  23:              public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  24:              {
  25:                  object zero = TimeSpan.Zero;
  26:                  string str = value as string;
  27:                  if (!string.IsNullOrEmpty(str))
  28:                  {
  29:                      try
  30:                      {
  31:                          zero = TimeSpan.Parse(str);
  32:                          if (zero != null)
  33:                          {
  34:                              TimeSpan span = (TimeSpan)zero;
  35:                              if (span.Ticks < 0L)
  36:                              {
  37:                                  throw new Exception(string.Format("Error_NegativeValue:{0}", value.ToString()));
  38:                              }
  39:                          }
  40:                      }
  41:                      catch
  42:                      {
  43:                          throw new Exception("InvalidTimespanFormat" + str);
  44:                      }
  45:                  }
  46:                  return zero;
  47:              }
  48:   
  49:              public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
  50:              {
  51:                  if ((destinationType == typeof(string)) && (value is TimeSpan))
  52:                  {
  53:                      TimeSpan span = (TimeSpan)value;
  54:                      return span.ToString();
  55:                  }
  56:                  return base.ConvertTo(context, culture, value, destinationType);
  57:              }
  58:   
  59:              public override TypeConverter.StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
  60:              {
  61:                  ArrayList values = new ArrayList();
  62:                  values.Add(new TimeSpan(0, 0, 0));
  63:                  values.Add(new TimeSpan(0, 1, 0));
  64:                  values.Add(new TimeSpan(0, 30, 0));
  65:                  values.Add(new TimeSpan(1, 0, 0));
  66:                  values.Add(new TimeSpan(12, 0, 0));
  67:                  values.Add(new TimeSpan(1, 0, 0, 0));
  68:                  return new TypeConverter.StandardValuesCollection(values);
  69:              }
  70:   
  71:              public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
  72:              {
  73:                  return true;
  74:              }
  75:   
  76:          }
  77:   
  78:          #endregion
  79:   
  80:          #region Ctors
  81:   
  82:          public RetryActivity() { }
  83:   
  84:          public RetryActivity(string name)
  85:              : base(name) { }
  86:   
  87:          #endregion
  88:   
  89:          #region Properties
  90:   
  91:          public static readonly DependencyProperty RetryConditionProperty =
  92:              DependencyProperty.Register("RetryCondition",
  93:              typeof(ActivityCondition), typeof(RetryActivity),
  94:              new PropertyMetadata(DependencyPropertyOptions.Metadata,
  95:                  new Attribute[]
  96:                  {
  97:                      new ValidationOptionAttribute(ValidationOption.Required)
  98:                  }));
  99:   
 100:          public ActivityCondition RetryCondition
 101:          {
 102:              get
 103:              {
 104:                  return (base.GetValue(RetryConditionProperty) as ActivityCondition);
 105:              }
 106:              set
 107:              {
 108:                  base.SetValue(RetryConditionProperty, value);
 109:              }
 110:          }
 111:   
 112:          public static readonly DependencyProperty IsInEventActivityModeProperty =
 113:              DependencyProperty.Register("IsInEventActivityMode",
 114:              typeof(bool), typeof(RetryActivity), new PropertyMetadata(false));
 115:   
 116:          private bool IsInEventActivityMode
 117:          {
 118:              get { return (bool)base.GetValue(IsInEventActivityModeProperty); }
 119:              set { base.SetValue(IsInEventActivityModeProperty, value); }
 120:          }
 121:   
 122:          public static readonly DependencyProperty InitializeTimeoutDurationEvent =
 123:              DependencyProperty.Register("InitializeTimeoutDuration",
 124:              typeof(EventHandler), typeof(RetryActivity));
 125:   
 126:          [MergableProperty(false), Category("Handlers"), Description("TimeoutInitializerDescription")]
 127:          public event EventHandler InitializeTimeoutDuration
 128:          {
 129:              add { base.AddHandler(InitializeTimeoutDurationEvent, value); }
 130:              remove { base.RemoveHandler(InitializeTimeoutDurationEvent, value); }
 131:          }
 132:   
 133:          public static readonly DependencyProperty TimeoutDurationProperty =
 134:              DependencyProperty.Register("TimeoutDuration",
 135:              typeof(TimeSpan), typeof(RetryActivity),
 136:              new PropertyMetadata(new TimeSpan(0, 0, 0)));
 137:   
 138:          [TypeConverter(typeof(TimeoutDurationConverter)), Description("TimeoutDurationDescription"), MergableProperty(false)]
 139:          public TimeSpan TimeoutDuration
 140:          {
 141:              get { return (TimeSpan)base.GetValue(TimeoutDurationProperty); }
 142:              set { base.SetValue(TimeoutDurationProperty, value); }
 143:          }
 144:   
 145:          public static readonly DependencyProperty QueueNameProperty =
 146:              DependencyProperty.Register("QueueName",
 147:              typeof(IComparable), typeof(RetryActivity));
 148:   
 149:          public static readonly DependencyProperty SubscriptionIDProperty =
 150:              DependencyProperty.Register("SubscriptionID",
 151:              typeof(Guid), typeof(RetryActivity),
 152:              new PropertyMetadata(Guid.NewGuid()));
 153:   
 154:          private Guid SubscriptionID
 155:          {
 156:              get { return (Guid)base.GetValue(SubscriptionIDProperty); }
 157:              set { base.SetValue(SubscriptionIDProperty, value); }
 158:          }
 159:   
 160:          #endregion
 161:   
 162:          #region Overrides
 163:   
 164:          protected override ActivityExecutionStatus Cancel(ActivityExecutionContext executionContext)
 165:          {
 166:              if (executionContext == null)
 167:                  throw new ArgumentNullException("executionContext");
 168:              if (base.EnabledActivities.Count == 0)
 169:                  return ActivityExecutionStatus.Closed;
 170:              Activity activity = base.EnabledActivities[0];
 171:              if (!this.IsInEventActivityMode && (this.SubscriptionID != Guid.Empty))
 172:                  ((IEventActivity)this).Unsubscribe(executionContext, this);
 173:              ActivityExecutionContext context = executionContext.ExecutionContextManager.GetExecutionContext(activity);
 174:              if (context == null)
 175:                  return ActivityExecutionStatus.Closed;
 176:              if (context.Activity.ExecutionStatus == ActivityExecutionStatus.Executing)
 177:                  context.CancelActivity(context.Activity);
 178:              return ActivityExecutionStatus.Canceling;
 179:          }
 180:   
 181:          protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
 182:          {
 183:              if (executionContext == null)
 184:                  throw new ArgumentNullException("executionContext");
 185:              if (this.ExecuteCore(executionContext))
 186:                  return ActivityExecutionStatus.Executing;
 187:              else
 188:                  return ActivityExecutionStatus.Closed;
 189:          }
 190:   
 191:          protected override void Initialize(IServiceProvider provider)
 192:          {
 193:              base.Initialize(provider);
 194:              base.SetValue(IsInEventActivityModeProperty, true);
 195:          }
 196:   
 197:          protected override void OnClosed(IServiceProvider provider)
 198:          {
 199:              base.RemoveProperty(SubscriptionIDProperty);
 200:              base.RemoveProperty(IsInEventActivityModeProperty);
 201:          }
 202:   
 203:          protected sealed override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
 204:          {
 205:              if (executionContext == null)
 206:                  throw new ArgumentNullException("executionContext");
 207:              if (exception == null)
 208:                  throw new ArgumentNullException("exception");
 209:              ActivityExecutionStatus status = this.Cancel(executionContext);
 210:              if (status == ActivityExecutionStatus.Canceling)
 211:                  return ActivityExecutionStatus.Faulting;
 212:              else
 213:                  return status;
 214:          }
 215:   
 216:          #endregion
 217:   
 218:          #region Private Implements
 219:   
 220:          private bool ExecuteCore(ActivityExecutionContext context)
 221:          {
 222:              this.IsInEventActivityMode = false;
 223:              if (base.ExecutionStatus == ActivityExecutionStatus.Canceling ||
 224:                  base.ExecutionStatus == ActivityExecutionStatus.Faulting)
 225:                  return false;
 226:              if (base.EnabledActivities.Count > 0)
 227:              {
 228:                  ActivityExecutionContext context2 = context.ExecutionContextManager.CreateExecutionContext(base.EnabledActivities[0]);
 229:                  context2.Activity.RegisterForStatusChange(Activity.ClosedEvent, this);
 230:                  context2.ExecuteActivity(context2.Activity);
 231:              }
 232:              return true;
 233:          }
 234:   
 235:          private TimerEventSubscriptionCollection SubscriptionCollection
 236:          {
 237:              get
 238:              {
 239:                  Activity parent = this;
 240:                  while (parent.Parent != null)
 241:                      parent = parent.Parent;
 242:                  return (TimerEventSubscriptionCollection)parent.GetValue(TimerEventSubscriptionCollection.TimerCollectionProperty);
 243:              }
 244:          }
 245:   
 246:          #endregion
 247:   
 248:          #region IEventActivity Members
 249:   
 250:          IComparable IEventActivity.QueueName
 251:          {
 252:              get { return (IComparable)base.GetValue(QueueNameProperty); }
 253:          }
 254:   
 255:          void IEventActivity.Subscribe(ActivityExecutionContext parentContext,
 256:              IActivityEventListener<QueueEventArgs> parentEventHandler)
 257:          {
 258:              if (parentContext == null)
 259:                  throw new ArgumentNullException("parentContext");
 260:              if (parentEventHandler == null)
 261:                  throw new ArgumentNullException("parentEventHandler");
 262:              this.IsInEventActivityMode = true;
 263:              base.RaiseEvent(InitializeTimeoutDurationEvent, this, EventArgs.Empty);
 264:              TimeSpan timeoutDuration = this.TimeoutDuration;
 265:              DateTime expiresAt = DateTime.UtcNow + timeoutDuration;
 266:              Guid guid = Guid.NewGuid();
 267:              base.SetValue(QueueNameProperty, guid);
 268:              WorkflowQueuingService service = parentContext.GetService<WorkflowQueuingService>();
 269:              IComparable queueName = guid;
 270:              TimerEventSubscription item = new TimerEventSubscription(guid, base.WorkflowInstanceId, expiresAt);
 271:              service.CreateWorkflowQueue(queueName, false).RegisterForQueueItemAvailable(parentEventHandler, base.QualifiedName);
 272:              this.SubscriptionID = item.SubscriptionId;
 273:              SubscriptionCollection.Add(item);
 274:          }
 275:   
 276:          void IEventActivity.Unsubscribe(ActivityExecutionContext parentContext,
 277:              IActivityEventListener<QueueEventArgs> parentEventHandler)
 278:          {
 279:              if (parentContext == null)
 280:                  throw new ArgumentNullException("parentContext");
 281:              if (parentEventHandler == null)
 282:                  throw new ArgumentNullException("parentEventHandler");
 283:              WorkflowQueuingService service = parentContext.GetService<WorkflowQueuingService>();
 284:              WorkflowQueue workflowQueue = null;
 285:              try
 286:              {
 287:                  workflowQueue = service.GetWorkflowQueue(this.SubscriptionID);
 288:              }
 289:              catch { }
 290:              if ((workflowQueue != null) && (workflowQueue.Count != 0))
 291:                  workflowQueue.Dequeue();
 292:              SubscriptionCollection.Remove(this.SubscriptionID);
 293:              if (workflowQueue != null)
 294:              {
 295:                  workflowQueue.UnregisterForQueueItemAvailable(parentEventHandler);
 296:                  service.DeleteWorkflowQueue(this.SubscriptionID);
 297:              }
 298:              this.SubscriptionID = Guid.Empty;
 299:          }
 300:   
 301:          #endregion
 302:   
 303:          #region IActivityEventListener<ActivityExecutionStatusChangedEventArgs> Members
 304:   
 305:          void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent(
 306:              object sender, ActivityExecutionStatusChangedEventArgs e)
 307:          {
 308:              if (e == null)
 309:                  throw new ArgumentNullException("e");
 310:              if (sender == null)
 311:                  throw new ArgumentNullException("sender");
 312:              ActivityExecutionContext context = sender as ActivityExecutionContext;
 313:              if (context == null)
 314:                  throw new ArgumentException("Error_SenderMustBeActivityExecutionContext", "sender");
 315:              e.Activity.UnregisterForStatusChange(Activity.ClosedEvent, this);
 316:              ActivityExecutionContextManager executionContextManager = context.ExecutionContextManager;
 317:              executionContextManager.CompleteExecutionContext(executionContextManager.GetExecutionContext(e.Activity));
 318:              bool retry = this.RetryCondition.Evaluate(this, context);
 319:              if (retry)
 320:                  ((IEventActivity)this).Subscribe(context, this);
 321:              else
 322:                  context.CloseActivity();
 323:          }
 324:   
 325:          #endregion
 326:   
 327:          #region IActivityEventListener<QueueEventArgs> Members
 328:   
 329:          void IActivityEventListener<QueueEventArgs>.OnEvent(object sender, QueueEventArgs e)
 330:          {
 331:              if (sender == null)
 332:                  throw new ArgumentNullException("sender");
 333:              if (e == null)
 334:                  throw new ArgumentNullException("e");
 335:              ActivityExecutionContext context = sender as ActivityExecutionContext;
 336:              if (context == null)
 337:                  throw new ArgumentException("Error_SenderMustBeActivityExecutionContext", "sender");
 338:              if (base.ExecutionStatus != ActivityExecutionStatus.Closed)
 339:              {
 340:                  WorkflowQueuingService service = context.GetService<WorkflowQueuingService>();
 341:                  service.GetWorkflowQueue(e.QueueName).Dequeue();
 342:                  service.DeleteWorkflowQueue(e.QueueName);
 343:                  ExecuteCore(context);
 344:              }
 345:          }
 346:   
 347:          #endregion
 348:   
 349:      }

    是不是感觉很长,其实就是把While和Delay两者的代码合并到了一起。

    顺便借用一下While的Designer,给它改个名字,换成RetryDesigner:

   1:      [ActivityDesignerTheme(typeof(RetryDesignerTheme))]
   2:      internal sealed class RetryDesigner : SequentialActivityDesigner
   3:      {
   4:   
   5:          public override bool CanInsertActivities(HitTestInfo insertLocation, ReadOnlyCollection<Activity> activitiesToInsert)
   6:          {
   7:              if ((this == base.ActiveView.AssociatedDesigner) && (this.ContainedDesigners.Count > 0))
   8:              {
   9:                  return false;
  10:              }
  11:              return base.CanInsertActivities(insertLocation, activitiesToInsert);
  12:          }
  13:   
  14:          protected override Rectangle[] GetConnectors()
  15:          {
  16:              Rectangle[] connectors = base.GetConnectors();
  17:              CompositeDesignerTheme designerTheme = base.DesignerTheme as CompositeDesignerTheme;
  18:              if (this.Expanded && (connectors.GetLength(0) > 0))
  19:              {
  20:                  connectors[connectors.GetLength(0) - 1].Height -= ((designerTheme != null) ? designerTheme.ConnectorSize.Height : 0) / 3;
  21:              }
  22:              return connectors;
  23:          }
  24:   
  25:          protected override void Initialize(Activity activity)
  26:          {
  27:              base.Initialize(activity);
  28:              this.HelpText = "DropActivityHere";
  29:          }
  30:   
  31:          protected override Size OnLayoutSize(ActivityDesignerLayoutEventArgs e)
  32:          {
  33:              Size size = base.OnLayoutSize(e);
  34:              CompositeDesignerTheme designerTheme = e.DesignerTheme as CompositeDesignerTheme;
  35:              if ((designerTheme != null) && this.Expanded)
  36:              {
  37:                  size.Width += 2 * designerTheme.ConnectorSize.Width;
  38:                  size.Height += designerTheme.ConnectorSize.Height;
  39:              }
  40:              return size;
  41:          }
  42:   
  43:          protected override void OnPaint(ActivityDesignerPaintEventArgs e)
  44:          {
  45:              base.OnPaint(e);
  46:              if (this.Expanded)
  47:              {
  48:                  CompositeDesignerTheme designerTheme = e.DesignerTheme as CompositeDesignerTheme;
  49:                  if (designerTheme != null)
  50:                  {
  51:                      Rectangle bounds = base.Bounds;
  52:                      Rectangle textRectangle = this.TextRectangle;
  53:                      Rectangle imageRectangle = this.ImageRectangle;
  54:                      Point empty = Point.Empty;
  55:                      if (!imageRectangle.IsEmpty)
  56:                      {
  57:                          empty = new Point(imageRectangle.Right + (e.AmbientTheme.Margin.Width / 2), imageRectangle.Top + (imageRectangle.Height / 2));
  58:                      }
  59:                      else if (!textRectangle.IsEmpty)
  60:                      {
  61:                          empty = new Point(textRectangle.Right + (e.AmbientTheme.Margin.Width / 2), textRectangle.Top + (textRectangle.Height / 2));
  62:                      }
  63:                      else
  64:                      {
  65:                          empty = new Point((bounds.Left + (bounds.Width / 2)) + (e.AmbientTheme.Margin.Width / 2), bounds.Top + (e.AmbientTheme.Margin.Height / 2));
  66:                      }
  67:                      Point[] points = new Point[4];
  68:                      points[0].X = bounds.Left + (bounds.Width / 2);
  69:                      points[0].Y = bounds.Bottom - (designerTheme.ConnectorSize.Height / 3);
  70:                      points[1].X = bounds.Right - (designerTheme.ConnectorSize.Width / 3);
  71:                      points[1].Y = bounds.Bottom - (designerTheme.ConnectorSize.Height / 3);
  72:                      points[2].X = bounds.Right - (designerTheme.ConnectorSize.Width / 3);
  73:                      points[2].Y = empty.Y;
  74:                      points[3].X = empty.X;
  75:                      points[3].Y = empty.Y;
  76:                      base.DrawConnectors(e.Graphics, designerTheme.ForegroundPen, points, LineAnchor.None, LineAnchor.ArrowAnchor);
  77:                      Point[] pointArray2 = new Point[] { points[0], new Point(bounds.Left + (bounds.Width / 2), bounds.Bottom) };
  78:                      base.DrawConnectors(e.Graphics, designerTheme.ForegroundPen, pointArray2, LineAnchor.None, LineAnchor.None);
  79:                  }
  80:              }
  81:          }
  82:   
  83:      }

    以及While的Theme,也改个名字:

   1:      internal sealed class RetryDesignerTheme : CompositeDesignerTheme
   2:      {
   3:          public RetryDesignerTheme(WorkflowTheme theme)
   4:              : base(theme)
   5:          {
   6:              this.ShowDropShadow = false;
   7:              this.ConnectorStartCap = LineAnchor.None;
   8:              this.ConnectorEndCap = LineAnchor.ArrowAnchor;
   9:              this.ForeColor = Color.FromArgb(0xff, 0x52, 0x8a, 0xf7);
  10:              this.BorderColor = Color.FromArgb(0xff, 0xe0, 0xe0, 0xe0);
  11:              this.BorderStyle = DashStyle.Dash;
  12:              this.BackColorStart = Color.FromArgb(0, 0, 0, 0);
  13:              this.BackColorEnd = Color.FromArgb(0, 0, 0, 0);
  14:          }
  15:      }

    好了,这个RetryActivity以及可以用了。

4、试用RetryActivity

    是不是觉得不可思议,东拼西凑的一个RetryActivity就完成了,感觉就像是在忽悠别人一样。

    好吧,耳听为虚,眼见为实。看看在Designer中的样子:

1

    以及它的属性:

1

    其中的codeActivity2代表可能需要重试的活动,RetryCondition则表达一个需要重试的条件,TimeoutDuration(因为Delay里面是这个名字,Copy的时候偷懒了,连名字也没改)则表示需要重试的情况下的延迟时间。

    这个RetryActivity有这么几个优点:

  • 拖拽起来简单
  • 由于RetryActivity的实现更接近于DoWhile语义,所以不用担心RetryCondition对第一次进入时的判断
  • 看起来舒服,至少比一个While+一个IfElse+一个Delay要少很多东西

    不过,同样也有一些部分没有做严格的实现,例如:

  • 中间的活动出现Cancel、Fault等状态时没有严格测试过
  • 没有自定义验证

    所以,如果遇到问题,最好能告知本人。

原文地址:https://www.cnblogs.com/vwxyzh/p/1707514.html