25
25
import org .apache .nifi .util .TestRunners ;
26
26
import org .jetbrains .annotations .Nullable ;
27
27
import org .junit .jupiter .api .BeforeEach ;
28
+ import org .junit .jupiter .api .Test ;
28
29
import org .junit .jupiter .params .ParameterizedTest ;
29
30
import org .junit .jupiter .params .provider .Arguments ;
30
31
import org .junit .jupiter .params .provider .MethodSource ;
31
32
32
33
import java .util .ArrayList ;
33
34
import java .util .List ;
35
+ import java .util .concurrent .CountDownLatch ;
36
+ import java .util .concurrent .ExecutorService ;
37
+ import java .util .concurrent .Executors ;
38
+ import java .util .concurrent .Future ;
39
+ import java .util .concurrent .TimeUnit ;
40
+ import java .util .concurrent .atomic .AtomicInteger ;
41
+ import java .util .function .Function ;
34
42
import java .util .stream .Stream ;
35
43
36
44
import static java .util .Collections .emptyList ;
45
+ import static org .junit .jupiter .api .Assertions .assertDoesNotThrow ;
37
46
import static org .junit .jupiter .api .Assertions .assertEquals ;
47
+ import static org .junit .jupiter .api .Assertions .assertTrue ;
38
48
import static org .junit .jupiter .params .provider .Arguments .arguments ;
39
49
import static org .mockito .Mockito .lenient ;
40
50
import static org .mockito .Mockito .mock ;
41
51
import static org .mockito .Mockito .when ;
42
52
43
53
class ConsumeBoxEnterpriseEventsTest extends AbstractBoxFileTest {
44
54
45
- private TestEventStream eventStream ;
55
+ private TestConsumeBoxEnterpriseEvents processor ;
46
56
47
57
@ BeforeEach
48
58
void setUp () throws Exception {
49
- eventStream = new TestEventStream ();
50
-
51
- final ConsumeBoxEnterpriseEvents processor = new ConsumeBoxEnterpriseEvents () {
52
- @ Override
53
- EventLog getEventLog (String position ) {
54
- return eventStream .consume (position );
55
- }
56
- };
59
+ processor = new TestConsumeBoxEnterpriseEvents ();
57
60
58
61
testRunner = TestRunners .newTestRunner (processor );
59
62
super .setUp ();
@@ -71,6 +74,9 @@ void testConsumeEvents(
71
74
testRunner .setProperty (ConsumeBoxEnterpriseEvents .START_OFFSET , startOffset );
72
75
}
73
76
77
+ final TestEventStream eventStream = new TestEventStream ();
78
+ processor .overrideGetEventLog (eventStream ::consume );
79
+
74
80
eventStream .addEvent (0 );
75
81
eventStream .addEvent (1 );
76
82
eventStream .addEvent (2 );
@@ -97,6 +103,39 @@ static List<Arguments> dataFor_testConsumeEvents() {
97
103
);
98
104
}
99
105
106
+ @ Test
107
+ void testGracefulTermination () throws InterruptedException {
108
+ final CountDownLatch scheduledLatch = new CountDownLatch (1 );
109
+ final AtomicInteger consumedEvents = new AtomicInteger (0 );
110
+
111
+ // Infinite stream.
112
+ processor .overrideGetEventLog (__ -> {
113
+ scheduledLatch .countDown ();
114
+ consumedEvents .incrementAndGet ();
115
+ return createEventLog (List .of (createBoxEvent (1 )), "" );
116
+ });
117
+
118
+ final ExecutorService runExecutor = Executors .newSingleThreadExecutor ();
119
+
120
+ try {
121
+ // Starting the processor that consumes an infinite stream.
122
+ final Future <?> runFuture = runExecutor .submit (() -> testRunner .run (/*iterations=*/ 1 , /*stopOnFinish=*/ false ));
123
+
124
+ assertTrue (scheduledLatch .await (5 , TimeUnit .SECONDS ), "Processor did not start" );
125
+
126
+ // Triggering the processor to stop.
127
+ testRunner .unSchedule ();
128
+
129
+ assertDoesNotThrow (() -> runFuture .get (5 , TimeUnit .SECONDS ), "Processor did not stop gracefully" );
130
+
131
+ testRunner .assertAllFlowFilesTransferred (ConsumeBoxEnterpriseEvents .REL_SUCCESS , consumedEvents .get ());
132
+ } finally {
133
+ // We can't use try with resources, as Executors use a shutdown method
134
+ // which indefinitely waits for submitted tasks.
135
+ runExecutor .shutdownNow ();
136
+ }
137
+ }
138
+
100
139
private Stream <Integer > extractEventIds (final MockFlowFile flowFile ) {
101
140
final JsonValue json = Json .parse (flowFile .getContent ());
102
141
return json .asArray ().values ().stream ()
@@ -105,46 +144,64 @@ private Stream<Integer> extractEventIds(final MockFlowFile flowFile) {
105
144
.map (Integer ::parseInt );
106
145
}
107
146
147
+ /**
148
+ * This class is used to override external call in {@link ConsumeBoxEnterpriseEvents#getEventLog(String)}.
149
+ */
150
+ private static class TestConsumeBoxEnterpriseEvents extends ConsumeBoxEnterpriseEvents {
151
+
152
+ private volatile Function <String , EventLog > fakeEventLog ;
153
+
154
+ void overrideGetEventLog (final Function <String , EventLog > fakeEventLog ) {
155
+ this .fakeEventLog = fakeEventLog ;
156
+ }
157
+
158
+ @ Override
159
+ EventLog getEventLog (String position ) {
160
+ return fakeEventLog .apply (position );
161
+ }
162
+ }
163
+
108
164
private static class TestEventStream {
109
165
110
166
private static final String NOW_POSITION = "now" ;
111
167
112
168
private final List <BoxEvent > events = new ArrayList <>();
113
169
114
170
void addEvent (final int eventId ) {
115
- final BoxEvent boxEvent = new BoxEvent (null , "{\" event_id\" : \" %d\" }" .formatted (eventId ));
116
- events .add (boxEvent );
171
+ events .add (createBoxEvent (eventId ));
117
172
}
118
173
119
174
EventLog consume (final String position ) {
175
+ final String nextPosition = String .valueOf (events .size ());
176
+
120
177
if (NOW_POSITION .equals (position )) {
121
- return createEmptyEventLog ( );
178
+ return createEventLog ( emptyList (), nextPosition );
122
179
}
123
180
124
181
final int streamPosition = Integer .parseInt (position );
125
182
if (streamPosition > events .size ()) {
126
183
// Real Box API returns the latest offset position, even if streamPosition was greater.
127
- return createEmptyEventLog ( );
184
+ return createEventLog ( emptyList (), nextPosition );
128
185
}
129
186
130
187
final List <BoxEvent > consumedEvents = events .subList (streamPosition , events .size ());
131
188
132
- return createEventLog (consumedEvents );
189
+ return createEventLog (consumedEvents , nextPosition );
133
190
}
191
+ }
134
192
135
- private EventLog createEmptyEventLog ( ) {
136
- return createEventLog ( emptyList ( ));
137
- }
193
+ private static BoxEvent createBoxEvent ( final int eventId ) {
194
+ return new BoxEvent ( null , "{ \" event_id \" : \" %d \" }" . formatted ( eventId ));
195
+ }
138
196
139
- private EventLog createEventLog (final List <BoxEvent > consumedEvents ) {
140
- // EventLog is not designed for being extended. Thus, mocking it.
141
- final EventLog eventLog = mock ();
197
+ private static EventLog createEventLog (final List <BoxEvent > consumedEvents , final String nextPosition ) {
198
+ // EventLog is not designed for being extended. Thus, mocking it.
199
+ final EventLog eventLog = mock ();
142
200
143
- when (eventLog .getNextStreamPosition ()).thenReturn (String . valueOf ( events . size ()) );
144
- lenient ().when (eventLog .getSize ()).thenReturn (consumedEvents .size ());
145
- lenient ().when (eventLog .iterator ()).thenReturn (consumedEvents .iterator ());
201
+ when (eventLog .getNextStreamPosition ()).thenReturn (nextPosition );
202
+ lenient ().when (eventLog .getSize ()).thenReturn (consumedEvents .size ());
203
+ lenient ().when (eventLog .iterator ()).thenReturn (consumedEvents .iterator ());
146
204
147
- return eventLog ;
148
- }
205
+ return eventLog ;
149
206
}
150
207
}
0 commit comments