29
29
import com .uber .cadence .client .ActivityNotExistsException ;
30
30
import com .uber .cadence .converter .DataConverter ;
31
31
import com .uber .cadence .serviceclient .IWorkflowService ;
32
- import java .util .concurrent .CancellationException ;
32
+ import java .util .Optional ;
33
+ import java .util .concurrent .ScheduledExecutorService ;
34
+ import java .util .concurrent .ScheduledFuture ;
35
+ import java .util .concurrent .TimeUnit ;
36
+ import java .util .concurrent .locks .Lock ;
37
+ import java .util .concurrent .locks .ReentrantLock ;
33
38
import org .apache .thrift .TException ;
34
39
import org .slf4j .Logger ;
35
40
import org .slf4j .LoggerFactory ;
43
48
class ActivityExecutionContextImpl implements ActivityExecutionContext {
44
49
45
50
private static final Logger log = LoggerFactory .getLogger (ActivityExecutionContextImpl .class );
51
+ private static final long HEARTBEAT_RETRY_WAIT_MILLIS = 1000 ;
52
+ private static final long MAX_HEARTBEAT_INTERVAL_MILLIS = 30000 ;
46
53
47
54
private final IWorkflowService service ;
48
-
49
55
private final String domain ;
50
-
51
56
private final ActivityTask task ;
52
57
private final DataConverter dataConverter ;
53
58
private boolean doNotCompleteOnReturn ;
59
+ private final long heartbeatIntervalMillis ;
60
+ private Optional <Object > lastDetails ;
61
+ private final ScheduledExecutorService heartbeatExecutor ;
62
+ private Lock lock = new ReentrantLock ();
63
+ private ScheduledFuture future ;
64
+ private ActivityCompletionException lastException ;
54
65
55
66
/** Create an ActivityExecutionContextImpl with the given attributes. */
56
67
ActivityExecutionContextImpl (
57
- IWorkflowService service , String domain , ActivityTask task , DataConverter dataConverter ) {
68
+ IWorkflowService service ,
69
+ String domain ,
70
+ ActivityTask task ,
71
+ DataConverter dataConverter ,
72
+ ScheduledExecutorService heartbeatExecutor ) {
58
73
this .domain = domain ;
59
74
this .service = service ;
60
75
this .task = task ;
61
76
this .dataConverter = dataConverter ;
77
+ this .heartbeatIntervalMillis =
78
+ Math .min (
79
+ (long ) (0.8 * task .getHeartbeatTimeout ().toMillis ()), MAX_HEARTBEAT_INTERVAL_MILLIS );
80
+ this .heartbeatExecutor = heartbeatExecutor ;
62
81
}
63
82
64
- /**
65
- * @throws CancellationException
66
- * @see ActivityExecutionContext#recordActivityHeartbeat(Object)
67
- */
83
+ /** @see ActivityExecutionContext#recordActivityHeartbeat(Object) */
68
84
@ Override
69
85
public void recordActivityHeartbeat (Object details ) throws ActivityCompletionException {
70
- // TODO: call service with the specified minimal interval (through @ActivityExecutionOptions)
71
- // allowing more frequent calls of this method.
86
+ lock .lock ();
87
+ try {
88
+ // always set lastDetail. Successful heartbeat will clear it.
89
+ lastDetails = details == null ? Optional .empty () : Optional .of (details );
90
+
91
+ // Only do sync heartbeat if there is no such call scheduled.
92
+ if (future == null ) {
93
+ doHeartBeat (details );
94
+ }
95
+
96
+ if (lastException != null ) {
97
+ throw lastException ;
98
+ }
99
+ } finally {
100
+ lock .unlock ();
101
+ }
102
+ }
103
+
104
+ private void doHeartBeat (Object details ) {
105
+ long nextHeartbeatDelay ;
106
+ try {
107
+ sendHeartbeatRequest (details );
108
+ // Clear lastDetails only if heartbeat succeeds.
109
+ lastDetails = null ;
110
+ nextHeartbeatDelay = heartbeatIntervalMillis ;
111
+ } catch (TException e ) {
112
+ // Not rethrowing to not fail activity implementation on intermittent connection or Cadence
113
+ // errors.
114
+ log .warn ("Heartbeat failed." , e );
115
+ nextHeartbeatDelay = HEARTBEAT_RETRY_WAIT_MILLIS ;
116
+ }
117
+
118
+ scheduleNextHeartbeat (nextHeartbeatDelay );
119
+ }
120
+
121
+ private void scheduleNextHeartbeat (long delay ) {
122
+ future =
123
+ heartbeatExecutor .schedule (
124
+ () -> {
125
+ lock .lock ();
126
+ try {
127
+ if (lastDetails != null ) {
128
+ Object details = lastDetails .orElse (null );
129
+ doHeartBeat (details );
130
+ } else {
131
+ future = null ;
132
+ }
133
+ } finally {
134
+ lock .unlock ();
135
+ }
136
+ },
137
+ delay ,
138
+ TimeUnit .MILLISECONDS );
139
+ }
140
+
141
+ private void sendHeartbeatRequest (Object details ) throws TException {
72
142
RecordActivityTaskHeartbeatRequest r = new RecordActivityTaskHeartbeatRequest ();
73
143
r .setTaskToken (task .getTaskToken ());
74
144
byte [] serialized = dataConverter .toData (details );
@@ -77,21 +147,14 @@ public void recordActivityHeartbeat(Object details) throws ActivityCompletionExc
77
147
try {
78
148
status = service .RecordActivityTaskHeartbeat (r );
79
149
if (status .isCancelRequested ()) {
80
- throw new ActivityCancelledException (task );
150
+ lastException = new ActivityCancelledException (task );
151
+ } else {
152
+ lastException = null ;
81
153
}
82
154
} catch (EntityNotExistsError e ) {
83
- throw new ActivityNotExistsException (task , e );
155
+ lastException = new ActivityNotExistsException (task , e );
84
156
} catch (BadRequestError e ) {
85
- throw new ActivityCompletionFailureException (task , e );
86
- } catch (TException e ) {
87
- log .warn (
88
- "Failure heartbeating on activityID="
89
- + task .getActivityId ()
90
- + " of Workflow="
91
- + task .getWorkflowExecution (),
92
- e );
93
- // Not rethrowing to not fail activity implementation on intermittent connection or Cadence
94
- // errors.
157
+ lastException = new ActivityCompletionFailureException (task , e );
95
158
}
96
159
}
97
160
0 commit comments