Skip to content

Commit 54543a7

Browse files
author
Kishan Sairam Adapa
authored
feat: expose method to update caches with change events (#266)
* feat: use kafka live event listener for eds cache client * nit * wrap up ut * update * unused * spotless * build file * update * update
1 parent b3bb3be commit 54543a7

File tree

3 files changed

+219
-21
lines changed

3 files changed

+219
-21
lines changed

entity-service-client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ tasks.test {
1111

1212
dependencies {
1313
api(project(":entity-service-api"))
14+
api(project(":entity-service-change-event-api"))
1415
api("com.typesafe:config:1.4.1")
1516

1617
implementation("org.hypertrace.core.grpcutils:grpc-client-utils:0.12.6")

entity-service-client/src/main/java/org/hypertrace/entity/data/service/client/EdsCacheClient.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.concurrent.TimeUnit;
1414
import javax.annotation.Nonnull;
1515
import org.hypertrace.core.serviceframework.metrics.PlatformMetricsRegistry;
16+
import org.hypertrace.entity.change.event.v1.EntityChangeEventKey;
17+
import org.hypertrace.entity.change.event.v1.EntityChangeEventValue;
1618
import org.hypertrace.entity.data.service.client.exception.NotFoundException;
1719
import org.hypertrace.entity.data.service.v1.ByIdRequest;
1820
import org.hypertrace.entity.data.service.v1.ByTypeAndIdentifyingAttributes;
@@ -224,4 +226,76 @@ public void upsertEnrichedEntities(String tenantId, EnrichedEntities enrichedEnt
224226
public void upsertRelationships(String tenantId, EntityRelationships relationships) {
225227
client.upsertRelationships(tenantId, relationships);
226228
}
229+
230+
public void updateBasedOnChangeEvent(
231+
EntityChangeEventKey entityChangeEventKey, EntityChangeEventValue entityChangeEventValue) {
232+
LOG.debug("Entity change event is {}, {} ", entityChangeEventKey, entityChangeEventValue);
233+
234+
switch (entityChangeEventValue.getEventCase()) {
235+
case CREATE_EVENT:
236+
// ignore create events, don't populate caches if not necessary
237+
break;
238+
case UPDATE_EVENT:
239+
updateCacheValues(
240+
entityChangeEventKey, entityChangeEventValue.getUpdateEvent().getLatestEntity());
241+
break;
242+
case DELETE_EVENT:
243+
invalidateCacheEntries(
244+
entityChangeEventKey, entityChangeEventValue.getDeleteEvent().getDeletedEntity());
245+
break;
246+
default:
247+
LOG.warn(
248+
"Entity change event value has invalid event type -> {}",
249+
entityChangeEventValue.getEventCase());
250+
}
251+
}
252+
253+
private void updateCacheValues(EntityChangeEventKey entityChangeEventKey, Entity entity) {
254+
getEntityCacheKeys(entityChangeEventKey)
255+
.forEach(
256+
cacheKey -> {
257+
if (entityCache.asMap().containsKey(cacheKey)) {
258+
entityCache.put(cacheKey, entity);
259+
}
260+
});
261+
EdsTypeAndIdAttributesCacheKey idsCacheKey = getIdsCacheKey(entityChangeEventKey, entity);
262+
if (entityIdsCache.asMap().containsKey(idsCacheKey)) {
263+
entityIdsCache.put(idsCacheKey, entity.getEntityId());
264+
}
265+
}
266+
267+
private void invalidateCacheEntries(EntityChangeEventKey entityChangeEventKey, Entity entity) {
268+
getEntityCacheKeys(entityChangeEventKey).forEach(cacheKey -> entityCache.invalidate(cacheKey));
269+
entityIdsCache.invalidate(getIdsCacheKey(entityChangeEventKey, entity));
270+
}
271+
272+
private Iterable<EdsCacheKey> getEntityCacheKeys(EntityChangeEventKey entityChangeEventKey) {
273+
return List.of(
274+
getEntityCacheKeyWithoutEntityType(entityChangeEventKey),
275+
getEntityCacheKeyWithEntityType(entityChangeEventKey));
276+
}
277+
278+
private EdsCacheKey getEntityCacheKeyWithoutEntityType(
279+
// used by - `getById(String tenantId, String entityId)`
280+
EntityChangeEventKey entityChangeEventKey) {
281+
return new EdsCacheKey(entityChangeEventKey.getTenantId(), entityChangeEventKey.getEntityId());
282+
}
283+
284+
private EdsCacheKey getEntityCacheKeyWithEntityType(EntityChangeEventKey entityChangeEventKey) {
285+
// used by `getById(String tenantId, ByIdRequest byIdRequest)`
286+
return new EdsCacheKey(
287+
entityChangeEventKey.getTenantId(),
288+
entityChangeEventKey.getEntityId(),
289+
entityChangeEventKey.getEntityType());
290+
}
291+
292+
private EdsTypeAndIdAttributesCacheKey getIdsCacheKey(
293+
EntityChangeEventKey entityChangeEventKey, Entity entity) {
294+
return new EdsTypeAndIdAttributesCacheKey(
295+
entityChangeEventKey.getTenantId(),
296+
ByTypeAndIdentifyingAttributes.newBuilder()
297+
.setEntityType(entity.getEntityType())
298+
.putAllIdentifyingAttributes(entity.getIdentifyingAttributesMap())
299+
.build());
300+
}
227301
}

entity-service-client/src/test/java/org/hypertrace/entity/data/service/client/EdsCacheClientTest.java

Lines changed: 144 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.hypertrace.entity.data.service.client;
22

3+
import static org.junit.jupiter.api.Assertions.assertEquals;
34
import static org.mockito.ArgumentMatchers.any;
45
import static org.mockito.ArgumentMatchers.anyString;
56
import static org.mockito.Mockito.mock;
@@ -10,6 +11,11 @@
1011

1112
import java.util.HashMap;
1213
import java.util.Map;
14+
import org.hypertrace.entity.change.event.v1.EntityChangeEventKey;
15+
import org.hypertrace.entity.change.event.v1.EntityChangeEventValue;
16+
import org.hypertrace.entity.change.event.v1.EntityCreateEvent;
17+
import org.hypertrace.entity.change.event.v1.EntityDeleteEvent;
18+
import org.hypertrace.entity.change.event.v1.EntityUpdateEvent;
1319
import org.hypertrace.entity.data.service.v1.AttributeValue;
1420
import org.hypertrace.entity.data.service.v1.ByIdRequest;
1521
import org.hypertrace.entity.data.service.v1.ByTypeAndIdentifyingAttributes;
@@ -163,32 +169,14 @@ public void testGetById() {
163169
String tenantId = "tenant";
164170
String entityId = "entity-12346";
165171

166-
Map<String, AttributeValue> identifyingAttributesMap = new HashMap<>();
167-
identifyingAttributesMap.put(
168-
"entity_name",
169-
AttributeValue.newBuilder()
170-
.setValue(Value.newBuilder().setString("GET /products").build())
171-
.build());
172-
identifyingAttributesMap.put(
173-
"is_active",
174-
AttributeValue.newBuilder().setValue(Value.newBuilder().setBoolean(true).build()).build());
175-
176-
Entity entity =
177-
Entity.newBuilder()
178-
.setTenantId(tenantId)
179-
.setEntityId(entityId)
180-
.setEntityType("API")
181-
.setEntityName("GET /products")
182-
.putAllIdentifyingAttributes(identifyingAttributesMap)
183-
.build();
184-
185-
when(entityDataServiceClient.getById(anyString(), any(ByIdRequest.class))).thenReturn(entity);
172+
when(entityDataServiceClient.getById(anyString(), any(ByIdRequest.class)))
173+
.thenReturn(getEntity(tenantId, entityId));
186174

187175
edsCacheClient.getById(tenantId, entityId);
188176
edsCacheClient.getById(tenantId, entityId);
189177

190178
verify(entityDataServiceClient, times(1))
191-
.getById("tenant", ByIdRequest.newBuilder().setEntityId("entity-12346").build());
179+
.getById(tenantId, ByIdRequest.newBuilder().setEntityId(entityId).build());
192180
}
193181

194182
@Test
@@ -207,4 +195,139 @@ public void testGetByIdForNull() {
207195
verify(entityDataServiceClient, times(2))
208196
.getById("tenant", ByIdRequest.newBuilder().setEntityId("entity-12346").build());
209197
}
198+
199+
@Test
200+
void testUpdateBasedOnCreateChangeEvent() {
201+
String tenantId = "tenant";
202+
String entityId = "entityId";
203+
EntityChangeEventKey key =
204+
EntityChangeEventKey.newBuilder()
205+
.setTenantId(tenantId)
206+
.setEntityType("API")
207+
.setEntityId(entityId)
208+
.build();
209+
EntityChangeEventValue value =
210+
EntityChangeEventValue.newBuilder()
211+
.setCreateEvent(
212+
EntityCreateEvent.newBuilder()
213+
.setCreatedEntity(getEntity(tenantId, entityId))
214+
.build())
215+
.build();
216+
217+
// expectation: no-op for create event
218+
edsCacheClient.updateBasedOnChangeEvent(key, value);
219+
// mock the eds call for get
220+
when(entityDataServiceClient.getById(anyString(), any(ByIdRequest.class)))
221+
.thenReturn(getEntity(tenantId, entityId));
222+
// try to fetch from cache
223+
edsCacheClient.getById(tenantId, entityId);
224+
// since create event is ignored the fetch triggers a invocation
225+
verify(entityDataServiceClient, times(1))
226+
.getById(tenantId, ByIdRequest.newBuilder().setEntityId(entityId).build());
227+
}
228+
229+
@Test
230+
void testUpdateBasedOnUpdateChangeEvent() {
231+
String tenantId = "tenant";
232+
String entityId = "entityId";
233+
234+
String originalEntityName = "GET /products-v1";
235+
when(entityDataServiceClient.getById(anyString(), any(ByIdRequest.class)))
236+
.thenReturn(getEntity(tenantId, entityId, originalEntityName));
237+
238+
// seed cache
239+
edsCacheClient.getById(tenantId, entityId);
240+
Entity returnedEntity = edsCacheClient.getById(tenantId, entityId);
241+
assertEquals(originalEntityName, returnedEntity.getEntityName());
242+
243+
verify(entityDataServiceClient, times(1))
244+
.getById(tenantId, ByIdRequest.newBuilder().setEntityId(entityId).build());
245+
246+
EntityChangeEventKey key =
247+
EntityChangeEventKey.newBuilder()
248+
.setTenantId(tenantId)
249+
.setEntityType("API")
250+
.setEntityId(entityId)
251+
.build();
252+
String updatedEntityName = "GET /products-v2";
253+
EntityChangeEventValue value =
254+
EntityChangeEventValue.newBuilder()
255+
.setUpdateEvent(
256+
EntityUpdateEvent.newBuilder()
257+
.setLatestEntity(getEntity(tenantId, entityId, updatedEntityName))
258+
.build())
259+
.build();
260+
261+
// expectation: update entity name to newer one
262+
edsCacheClient.updateBasedOnChangeEvent(key, value);
263+
// try to fetch from cache, should return one with new entityName
264+
returnedEntity = edsCacheClient.getById(tenantId, entityId);
265+
assertEquals(updatedEntityName, returnedEntity.getEntityName());
266+
// no more invocations same as before (1) done while seeding cache
267+
verify(entityDataServiceClient, times(1))
268+
.getById(tenantId, ByIdRequest.newBuilder().setEntityId(entityId).build());
269+
}
270+
271+
@Test
272+
void testUpdateBasedOnDeleteChangeEvent() {
273+
String tenantId = "tenant";
274+
String entityId = "entityId";
275+
276+
when(entityDataServiceClient.getById(anyString(), any(ByIdRequest.class)))
277+
.thenReturn(getEntity(tenantId, entityId));
278+
279+
// seed cache
280+
edsCacheClient.getById(tenantId, entityId);
281+
edsCacheClient.getById(tenantId, entityId);
282+
283+
verify(entityDataServiceClient, times(1))
284+
.getById(tenantId, ByIdRequest.newBuilder().setEntityId(entityId).build());
285+
286+
EntityChangeEventKey key =
287+
EntityChangeEventKey.newBuilder()
288+
.setTenantId(tenantId)
289+
.setEntityType("API")
290+
.setEntityId(entityId)
291+
.build();
292+
String updatedEntityName = "GET /products-v2";
293+
EntityChangeEventValue value =
294+
EntityChangeEventValue.newBuilder()
295+
.setDeleteEvent(
296+
EntityDeleteEvent.newBuilder()
297+
.setDeletedEntity(getEntity(tenantId, entityId, updatedEntityName))
298+
.build())
299+
.build();
300+
301+
// expectation: invalidate cache
302+
edsCacheClient.updateBasedOnChangeEvent(key, value);
303+
// try to fetch from cache, fetch should result in remote call
304+
edsCacheClient.getById(tenantId, entityId);
305+
// one more invocation same after seeding cache
306+
verify(entityDataServiceClient, times(2))
307+
.getById(tenantId, ByIdRequest.newBuilder().setEntityId(entityId).build());
308+
}
309+
310+
private Entity getEntity(String tenantId, String entityId) {
311+
return getEntity(tenantId, entityId, "GET /products");
312+
}
313+
314+
private Entity getEntity(String tenantId, String entityId, String entityName) {
315+
Map<String, AttributeValue> identifyingAttributesMap = new HashMap<>();
316+
identifyingAttributesMap.put(
317+
"entity_name",
318+
AttributeValue.newBuilder()
319+
.setValue(Value.newBuilder().setString(entityName).build())
320+
.build());
321+
identifyingAttributesMap.put(
322+
"is_active",
323+
AttributeValue.newBuilder().setValue(Value.newBuilder().setBoolean(true).build()).build());
324+
325+
return Entity.newBuilder()
326+
.setTenantId(tenantId)
327+
.setEntityId(entityId)
328+
.setEntityType("API")
329+
.setEntityName(entityName)
330+
.putAllIdentifyingAttributes(identifyingAttributesMap)
331+
.build();
332+
}
210333
}

0 commit comments

Comments
 (0)