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等状态时没有严格测试过
- 没有自定义验证
所以,如果遇到问题,最好能告知本人。