Skip to content

Commit 7428c4c

Browse files
authored
feat: Added ODPManager implementation (#489)
## Summary Added ODPManager Implementation which does the following. 1. Initializes and provides access to ODPEventManager and ODPSegmentManager 2. Provides updated ODPConfig settings to event manager and segment manager. 3. Stops Event Manager thread when closed. ## Test plan 1. Manually tested thoroughly 2. Added unit tests. ## Issues [FSSDK-8388](https://jira.sso.episerver.net/browse/FSSDK-8388)
1 parent 913b8e4 commit 7428c4c

File tree

7 files changed

+277
-7
lines changed

7 files changed

+277
-7
lines changed

Diff for: core-api/src/main/java/com/optimizely/ab/odp/ODPConfig.java

+8
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,12 @@ public synchronized List<String> getAllSegments() {
7171
public synchronized void setAllSegments(List<String> allSegments) {
7272
this.allSegments = allSegments;
7373
}
74+
75+
public Boolean equals(ODPConfig toCompare) {
76+
return getApiHost().equals(toCompare.getApiHost()) && getApiKey().equals(toCompare.getApiKey()) && getAllSegments().equals(toCompare.allSegments);
77+
}
78+
79+
public synchronized ODPConfig getClone() {
80+
return new ODPConfig(apiKey, apiHost, allSegments);
81+
}
7482
}

Diff for: core-api/src/main/java/com/optimizely/ab/odp/ODPEvent.java

+18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import javax.annotation.Nonnull;
1919
import javax.annotation.Nullable;
20+
import java.beans.Transient;
2021
import java.util.Collections;
2122
import java.util.Map;
2223

@@ -64,4 +65,21 @@ public Map<String, Object> getData() {
6465
public void setData(Map<String, Object> data) {
6566
this.data = data;
6667
}
68+
69+
@Transient
70+
public Boolean isDataValid() {
71+
for (Object entry: this.data.values()) {
72+
if (
73+
!( entry instanceof String
74+
|| entry instanceof Integer
75+
|| entry instanceof Long
76+
|| entry instanceof Boolean
77+
|| entry instanceof Float
78+
|| entry instanceof Double
79+
|| entry == null)) {
80+
return false;
81+
}
82+
}
83+
return true;
84+
}
6785
}

Diff for: core-api/src/main/java/com/optimizely/ab/odp/ODPEventManager.java

+31-5
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class ODPEventManager {
5050

5151
// The eventQueue needs to be thread safe. We are not doing anything extra for thread safety here
5252
// because `LinkedBlockingQueue` itself is thread safe.
53-
private final BlockingQueue<ODPEvent> eventQueue = new LinkedBlockingQueue<>();
53+
private final BlockingQueue<Object> eventQueue = new LinkedBlockingQueue<>();
5454

5555
public ODPEventManager(@Nonnull ODPConfig odpConfig, @Nonnull ODPApiManager apiManager) {
5656
this(odpConfig, apiManager, null, null, null);
@@ -71,7 +71,9 @@ public void start() {
7171
}
7272

7373
public void updateSettings(ODPConfig odpConfig) {
74-
this.odpConfig = odpConfig;
74+
if (!this.odpConfig.equals(odpConfig) && eventQueue.offer(new FlushEvent(this.odpConfig))) {
75+
this.odpConfig = odpConfig;
76+
}
7577
}
7678

7779
public void identifyUser(@Nullable String vuid, String userId) {
@@ -85,6 +87,10 @@ public void identifyUser(@Nullable String vuid, String userId) {
8587
}
8688

8789
public void sendEvent(ODPEvent event) {
90+
if (!event.isDataValid()) {
91+
logger.error("ODP event send failed (ODP data is not valid)");
92+
return;
93+
}
8894
event.setData(augmentCommonData(event.getData()));
8995
processEvent(event);
9096
}
@@ -137,7 +143,7 @@ private class EventDispatcherThread extends Thread {
137143
public void run() {
138144
while (true) {
139145
try {
140-
ODPEvent nextEvent;
146+
Object nextEvent;
141147

142148
// If batch has events, set the timeout to remaining time for flush interval,
143149
// otherwise wait for the new event indefinitely
@@ -158,12 +164,17 @@ public void run() {
158164
continue;
159165
}
160166

167+
if (nextEvent instanceof FlushEvent) {
168+
flush(((FlushEvent) nextEvent).getOdpConfig());
169+
continue;
170+
}
171+
161172
if (currentBatch.size() == 0) {
162173
// Batch starting, create a new flush time
163174
nextFlushTime = new Date().getTime() + flushInterval;
164175
}
165176

166-
currentBatch.add(nextEvent);
177+
currentBatch.add((ODPEvent) nextEvent);
167178

168179
if (currentBatch.size() >= batchSize) {
169180
flush();
@@ -176,7 +187,7 @@ public void run() {
176187
logger.debug("Exiting ODP Event Dispatcher Thread.");
177188
}
178189

179-
private void flush() {
190+
private void flush(ODPConfig odpConfig) {
180191
if (odpConfig.isReady()) {
181192
String payload = ODPJsonSerializerFactory.getSerializer().serializeEvents(currentBatch);
182193
String endpoint = odpConfig.getApiHost() + EVENT_URL_PATH;
@@ -192,8 +203,23 @@ private void flush() {
192203
currentBatch.clear();
193204
}
194205

206+
private void flush() {
207+
flush(odpConfig);
208+
}
209+
195210
public void signalStop() {
196211
shouldStop = true;
197212
}
198213
}
214+
215+
private static class FlushEvent {
216+
private final ODPConfig odpConfig;
217+
public FlushEvent(ODPConfig odpConfig) {
218+
this.odpConfig = odpConfig.getClone();
219+
}
220+
221+
public ODPConfig getOdpConfig() {
222+
return odpConfig;
223+
}
224+
}
199225
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.odp;
18+
19+
import javax.annotation.Nonnull;
20+
import java.util.List;
21+
22+
public class ODPManager {
23+
private volatile ODPConfig odpConfig;
24+
private final ODPSegmentManager segmentManager;
25+
private final ODPEventManager eventManager;
26+
27+
public ODPManager(@Nonnull ODPConfig odpConfig, @Nonnull ODPApiManager apiManager) {
28+
this(odpConfig, new ODPSegmentManager(odpConfig, apiManager), new ODPEventManager(odpConfig, apiManager));
29+
}
30+
31+
public ODPManager(@Nonnull ODPConfig odpConfig, @Nonnull ODPSegmentManager segmentManager, @Nonnull ODPEventManager eventManager) {
32+
this.odpConfig = odpConfig;
33+
this.segmentManager = segmentManager;
34+
this.eventManager = eventManager;
35+
this.eventManager.start();
36+
}
37+
38+
public ODPSegmentManager getSegmentManager() {
39+
return segmentManager;
40+
}
41+
42+
public ODPEventManager getEventManager() {
43+
return eventManager;
44+
}
45+
46+
public Boolean updateSettings(String apiHost, String apiKey, List<String> allSegments) {
47+
ODPConfig newConfig = new ODPConfig(apiKey, apiHost, allSegments);
48+
if (!odpConfig.equals(newConfig)) {
49+
odpConfig = newConfig;
50+
eventManager.updateSettings(odpConfig);
51+
segmentManager.resetCache();
52+
segmentManager.updateSettings(odpConfig);
53+
return true;
54+
}
55+
return false;
56+
}
57+
58+
public void close() {
59+
eventManager.stop();
60+
}
61+
}

Diff for: core-api/src/main/java/com/optimizely/ab/odp/ODPSegmentManager.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class ODPSegmentManager {
3434

3535
private final ODPApiManager apiManager;
3636

37-
private final ODPConfig odpConfig;
37+
private volatile ODPConfig odpConfig;
3838

3939
private final Cache<List<String>> segmentsCache;
4040

@@ -105,4 +105,12 @@ public List<String> getQualifiedSegments(ODPUserKey userKey, String userValue, L
105105
private String getCacheKey(String userKey, String userValue) {
106106
return userKey + "-$-" + userValue;
107107
}
108+
109+
public void updateSettings(ODPConfig odpConfig) {
110+
this.odpConfig = odpConfig;
111+
}
112+
113+
public void resetCache() {
114+
segmentsCache.reset();
115+
}
108116
}

Diff for: core-api/src/test/java/com/optimizely/ab/odp/ODPEventManagerTest.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,36 @@ public void applyUpdatedODPConfigWhenAvailable() throws InterruptedException {
216216
Thread.sleep(500);
217217
Mockito.verify(mockApiManager, times(2)).sendEvents(eq("key"), eq("http://www.odp-host.com/v3/events"), any());
218218
eventManager.updateSettings(new ODPConfig("new-key", "http://www.new-odp-host.com"));
219-
Thread.sleep(1500);
219+
220+
// Should immediately Flush current batch with old ODP config when settings are changed
221+
Thread.sleep(100);
222+
Mockito.verify(mockApiManager, times(3)).sendEvents(eq("key"), eq("http://www.odp-host.com/v3/events"), any());
223+
224+
// New events should use new config
225+
for (int i = 0; i < 10; i++) {
226+
eventManager.sendEvent(getEvent(i));
227+
}
228+
Thread.sleep(100);
220229
Mockito.verify(mockApiManager, times(1)).sendEvents(eq("new-key"), eq("http://www.new-odp-host.com/v3/events"), any());
221230
}
222231

232+
@Test
233+
public void validateEventData() {
234+
ODPEvent event = new ODPEvent("type", "action", null, null);
235+
Map<String, Object> data = new HashMap<>();
236+
237+
data.put("String", "string Value");
238+
data.put("Integer", 100);
239+
data.put("Float", 33.89);
240+
data.put("Boolean", true);
241+
data.put("null", null);
242+
event.setData(data);
243+
assertTrue(event.isDataValid());
244+
245+
data.put("RandomObject", new Object());
246+
assertFalse(event.isDataValid());
247+
}
248+
223249
private ODPEvent getEvent(int id) {
224250
Map<String, String> identifiers = new HashMap<>();
225251
identifiers.put("identifier1", "value1-" + id);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
*
3+
* Copyright 2022, Optimizely
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.odp;
18+
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
import org.mockito.Mock;
22+
import org.mockito.Mockito;
23+
24+
import java.util.Arrays;
25+
26+
import static org.mockito.Matchers.*;
27+
import static org.mockito.Mockito.*;
28+
import static org.junit.Assert.*;
29+
30+
public class ODPManagerTest {
31+
private static final String API_RESPONSE = "{\"data\":{\"customer\":{\"audiences\":{\"edges\":[{\"node\":{\"name\":\"segment1\",\"state\":\"qualified\"}},{\"node\":{\"name\":\"segment2\",\"state\":\"qualified\"}}]}}}}";
32+
33+
@Mock
34+
ODPApiManager mockApiManager;
35+
36+
@Mock
37+
ODPEventManager mockEventManager;
38+
39+
@Mock
40+
ODPSegmentManager mockSegmentManager;
41+
42+
@Before
43+
public void setup() {
44+
mockApiManager = mock(ODPApiManager.class);
45+
mockEventManager = mock(ODPEventManager.class);
46+
mockSegmentManager = mock(ODPSegmentManager.class);
47+
}
48+
49+
@Test
50+
public void shouldStartEventManagerWhenODPManagerIsInitialized() {
51+
ODPConfig config = new ODPConfig("test-key", "test-host");
52+
new ODPManager(config, mockSegmentManager, mockEventManager);
53+
verify(mockEventManager, times(1)).start();
54+
}
55+
56+
@Test
57+
public void shouldStopEventManagerWhenCloseIsCalled() {
58+
ODPConfig config = new ODPConfig("test-key", "test-host");
59+
ODPManager odpManager = new ODPManager(config, mockSegmentManager, mockEventManager);
60+
61+
// Stop is not called in the default flow.
62+
verify(mockEventManager, times(0)).stop();
63+
64+
odpManager.close();
65+
// stop should be called when odpManager is closed.
66+
verify(mockEventManager, times(1)).stop();
67+
}
68+
69+
@Test
70+
public void shouldUseNewSettingsInEventManagerWhenODPConfigIsUpdated() throws InterruptedException {
71+
Mockito.when(mockApiManager.sendEvents(any(), any(), any())).thenReturn(200);
72+
ODPConfig config = new ODPConfig("test-key", "test-host", Arrays.asList("segment1", "segment2"));
73+
ODPManager odpManager = new ODPManager(config, mockApiManager);
74+
75+
odpManager.getEventManager().identifyUser("vuid", "fsuid");
76+
Thread.sleep(2000);
77+
verify(mockApiManager, times(1))
78+
.sendEvents(eq("test-key"), eq("test-host/v3/events"), any());
79+
80+
odpManager.updateSettings("test-host-updated", "test-key-updated", Arrays.asList("segment1"));
81+
odpManager.getEventManager().identifyUser("vuid", "fsuid");
82+
Thread.sleep(1200);
83+
verify(mockApiManager, times(1))
84+
.sendEvents(eq("test-key-updated"), eq("test-host-updated/v3/events"), any());
85+
}
86+
87+
@Test
88+
public void shouldUseNewSettingsInSegmentManagerWhenODPConfigIsUpdated() {
89+
Mockito.when(mockApiManager.fetchQualifiedSegments(anyString(), anyString(), anyString(), anyString(), anyList()))
90+
.thenReturn(API_RESPONSE);
91+
ODPConfig config = new ODPConfig("test-key", "test-host", Arrays.asList("segment1", "segment2"));
92+
ODPManager odpManager = new ODPManager(config, mockApiManager);
93+
94+
odpManager.getSegmentManager().getQualifiedSegments("test-id");
95+
verify(mockApiManager, times(1))
96+
.fetchQualifiedSegments(eq("test-key"), eq("test-host/v3/graphql"), any(), any(), any());
97+
98+
odpManager.updateSettings("test-host-updated", "test-key-updated", Arrays.asList("segment1"));
99+
odpManager.getSegmentManager().getQualifiedSegments("test-id");
100+
verify(mockApiManager, times(1))
101+
.fetchQualifiedSegments(eq("test-key-updated"), eq("test-host-updated/v3/graphql"), any(), any(), any());
102+
}
103+
104+
@Test
105+
public void shouldGetEventManager() {
106+
ODPConfig config = new ODPConfig("test-key", "test-host");
107+
ODPManager odpManager = new ODPManager(config, mockSegmentManager, mockEventManager);
108+
assertNotNull(odpManager.getEventManager());
109+
110+
odpManager = new ODPManager(config, mockApiManager);
111+
assertNotNull(odpManager.getEventManager());
112+
}
113+
114+
@Test
115+
public void shouldGetSegmentManager() {
116+
ODPConfig config = new ODPConfig("test-key", "test-host");
117+
ODPManager odpManager = new ODPManager(config, mockSegmentManager, mockEventManager);
118+
assertNotNull(odpManager.getSegmentManager());
119+
120+
odpManager = new ODPManager(config, mockApiManager);
121+
assertNotNull(odpManager.getSegmentManager());
122+
}
123+
}

0 commit comments

Comments
 (0)