1、前言
.net Framework 3.0的Workflow用过了吧,什么?还没有,好吧,就连我这种当初认为Workflow是个不值得花时间去学习的人也用了一下,毕竟在某些情况下,使用WF的编码效率以及灵活性远要比不使用WF的要高。
2、场景
比如说,现在需要做个异步的服务,其中有调用了很多其他服务,并且这些服务是远程的,也就是可能在很多阶段都回出现调用失败的情况,当然,由于服务本身是异步的,那么不太可能遇到一个失败就把这个过程都设为失败,要是这样的话,如果每个服务的失败概率是5%,如果过程中有10个这样的服务,那么总体的失败率就达到了40%,显然这个失败率是无法接受的。
如果按照传统的手段实现重试操作,那么,需要把每一步都记录到数据库或消息队列中,这样在每一步都需要自己写持久化和再次加载的方法,如果对象类型较少,那也不算麻烦,如果对象多了,那就有点吃不消了。
这里就可以让Workflow大显身手了,在Workflow里面,只需要放一个While、一个IfElse和一个Delay,以及需要Retry的活动本身,再准备一个持久化服务,一个带有Retry功能的服务就自动完成了。
太抽象了?看图:
其中的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中的样子:
以及它的属性:
其中的codeActivity2代表可能需要重试的活动,RetryCondition则表达一个需要重试的条件,TimeoutDuration(因为Delay里面是这个名字,Copy的时候偷懒了,连名字也没改)则表示需要重试的情况下的延迟时间。
这个RetryActivity有这么几个优点:
- 拖拽起来简单
- 由于RetryActivity的实现更接近于DoWhile语义,所以不用担心RetryCondition对第一次进入时的判断
- 看起来舒服,至少比一个While+一个IfElse+一个Delay要少很多东西
不过,同样也有一些部分没有做严格的实现,例如:
- 中间的活动出现Cancel、Fault等状态时没有严格测试过
- 没有自定义验证
所以,如果遇到问题,最好能告知本人。
浙公网安备 33010602011771号