• 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等状态时没有严格测试过
    • 没有自定义验证

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

  • 相关阅读:
    Eclipse集成Tomcat:6个常见的”how to”问题
    linux环境变量配置
    (原创)JS点击事件——Uncaught TypeError: Cannot set property 'onclick' of null
    [ JS 进阶 ] 闭包,作用域链,垃圾回收,内存泄露
    webstorm安装后的一些设置技巧:
    前端工程师的知识体系
    Git常用命令及软件推荐
    Vue.js双向绑定的实现原理
    GET和POST面试知识点
    CSS 巧用 :before和:after
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1707514.html
Copyright © 2020-2023  润新知