-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add throttled punctuator (#82)
* feat: add callback registry punctuator * change package * nit * expose punctuate timestamp * expose punctuate timestamp * make abstract * nit * visiblity * empty fix * fix * nit * some more changes * rename * pass on boolean * logging * nit * renames * address comments * log tune * opt * nit * remove list * update * wrap up * minor refactor * nit * remove --------- Co-authored-by: Laxman Ch <laxman@traceable.ai>
- Loading branch information
1 parent
f57eb9b
commit 58173bd
Showing
7 changed files
with
415 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
...a/org/hypertrace/core/kafkastreams/framework/punctuators/AbstractThrottledPunctuator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package org.hypertrace.core.kafkastreams.framework.punctuators; | ||
|
||
import java.time.Clock; | ||
import java.util.ArrayList; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.kafka.streams.KeyValue; | ||
import org.apache.kafka.streams.processor.Punctuator; | ||
import org.apache.kafka.streams.state.KeyValueIterator; | ||
import org.apache.kafka.streams.state.KeyValueStore; | ||
import org.hypertrace.core.kafkastreams.framework.punctuators.action.TaskResult; | ||
|
||
@Slf4j | ||
public abstract class AbstractThrottledPunctuator<T> implements Punctuator { | ||
private final Clock clock; | ||
private final KeyValueStore<Long, ArrayList<T>> eventStore; | ||
private final ThrottledPunctuatorConfig config; | ||
|
||
public AbstractThrottledPunctuator( | ||
Clock clock, ThrottledPunctuatorConfig config, KeyValueStore<Long, ArrayList<T>> eventStore) { | ||
this.clock = clock; | ||
this.config = config; | ||
this.eventStore = eventStore; | ||
} | ||
|
||
public void scheduleTask(long scheduleMs, T event) { | ||
long windowMs = normalize(scheduleMs); | ||
ArrayList<T> events = Optional.ofNullable(eventStore.get(windowMs)).orElse(new ArrayList<>()); | ||
events.add(event); | ||
eventStore.put(windowMs, events); | ||
} | ||
|
||
public boolean rescheduleTask(long oldScheduleMs, long newScheduleMs, T event) { | ||
scheduleTask(newScheduleMs, event); | ||
return cancelTask(oldScheduleMs, event); | ||
} | ||
|
||
public boolean cancelTask(long scheduleMs, T event) { | ||
long windowMs = normalize(scheduleMs); | ||
ArrayList<T> events = Optional.ofNullable(eventStore.get(windowMs)).orElse(new ArrayList<>()); | ||
boolean removed = events.remove(event); | ||
if (removed) { | ||
if (events.isEmpty()) { | ||
eventStore.delete(windowMs); | ||
} else { | ||
eventStore.put(windowMs, events); | ||
} | ||
} else { | ||
log.warn( | ||
"task cancel failed. event not found for ts: {}, window: {}", | ||
new Date(scheduleMs), | ||
new Date(windowMs)); | ||
} | ||
return removed; | ||
} | ||
|
||
@Override | ||
public final void punctuate(long timestamp) { | ||
long startTime = clock.millis(); | ||
int totalProcessedWindows = 0; | ||
int totalProcessedTasks = 0; | ||
|
||
log.debug( | ||
"Processing tasks with throttling yield of {} until timestamp {}", | ||
config.getYieldMs(), | ||
timestamp); | ||
try (KeyValueIterator<Long, ArrayList<T>> it = | ||
eventStore.range(getRangeStart(timestamp), getRangeEnd(timestamp))) { | ||
// iterate through all keys in range until yield timeout is reached | ||
while (it.hasNext() && !shouldYieldNow(startTime)) { | ||
KeyValue<Long, ArrayList<T>> kv = it.next(); | ||
totalProcessedWindows++; | ||
ArrayList<T> events = kv.value; | ||
long windowMs = kv.key; | ||
// collect all tasks to be rescheduled by key to perform bulk reschedules | ||
Map<Long, ArrayList<T>> rescheduledTasks = new HashMap<>(); | ||
// loop through all events for this key until yield timeout is reached | ||
int i = 0; | ||
for (; i < events.size() && !shouldYieldNow(startTime); i++) { | ||
T event = events.get(i); | ||
totalProcessedTasks++; | ||
TaskResult action = executeTask(timestamp, event); | ||
action | ||
.getRescheduleTimestamp() | ||
.ifPresent( | ||
(rescheduleTimestamp) -> | ||
rescheduledTasks | ||
.computeIfAbsent(normalize(rescheduleTimestamp), (t) -> new ArrayList<>()) | ||
.add(event)); | ||
} | ||
// process all reschedules | ||
rescheduledTasks.forEach( | ||
(newWindowMs, rescheduledEvents) -> { | ||
ArrayList<T> windowTasks = | ||
Optional.ofNullable(eventStore.get(newWindowMs)).orElse(new ArrayList<>()); | ||
windowTasks.addAll(rescheduledEvents); | ||
eventStore.put(newWindowMs, windowTasks); | ||
}); | ||
|
||
// all tasks till i-1 have been cancelled or rescheduled hence to be removed from store | ||
if (i == events.size()) { | ||
// can directly delete key from store | ||
eventStore.delete(windowMs); | ||
} else { | ||
eventStore.put(windowMs, new ArrayList<>(events.subList(i, events.size()))); | ||
} | ||
} | ||
} | ||
log.info( | ||
"processed windows: {}, processed tasks: {}, time taken: {}", | ||
totalProcessedWindows, | ||
totalProcessedTasks, | ||
clock.millis() - startTime); | ||
} | ||
|
||
protected abstract TaskResult executeTask(long punctuateTimestamp, T object); | ||
|
||
protected long getRangeStart(long punctuateTimestamp) { | ||
return 0; | ||
} | ||
|
||
protected long getRangeEnd(long punctuateTimestamp) { | ||
return punctuateTimestamp; | ||
} | ||
|
||
private boolean shouldYieldNow(long startTimestamp) { | ||
return (clock.millis() - startTimestamp) > config.getYieldMs(); | ||
} | ||
|
||
private long normalize(long timestamp) { | ||
return timestamp - (timestamp % config.getWindowMs()); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...ava/org/hypertrace/core/kafkastreams/framework/punctuators/ThrottledPunctuatorConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package org.hypertrace.core.kafkastreams.framework.punctuators; | ||
|
||
import static org.apache.kafka.clients.consumer.ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG; | ||
import static org.apache.kafka.streams.StreamsConfig.consumerPrefix; | ||
|
||
import com.typesafe.config.Config; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public class ThrottledPunctuatorConfig { | ||
private static final String YIELD_CONFIG_SUFFIX = ".yield.ms"; | ||
private static final String WINDOW_CONFIG_SUFFIX = ".window.ms"; | ||
private static final long DEFAULT_WINDOW_MS = 1; | ||
private static final double DEFAULT_YIELD_SESSION_TIMEOUT_RATIO = 1 / 5.0; | ||
private final long yieldMs; | ||
private final long windowMs; | ||
|
||
public ThrottledPunctuatorConfig(Config kafkaStreamsConfig, String punctuatorName) { | ||
if (kafkaStreamsConfig.hasPath(punctuatorName + YIELD_CONFIG_SUFFIX)) { | ||
this.yieldMs = kafkaStreamsConfig.getLong(punctuatorName + YIELD_CONFIG_SUFFIX); | ||
} else { | ||
// when not configured, set to 20% of session timeout. | ||
this.yieldMs = | ||
(long) | ||
(kafkaStreamsConfig.getLong(consumerPrefix(SESSION_TIMEOUT_MS_CONFIG)) | ||
* DEFAULT_YIELD_SESSION_TIMEOUT_RATIO); | ||
} | ||
if (kafkaStreamsConfig.hasPath(punctuatorName + WINDOW_CONFIG_SUFFIX)) { | ||
this.windowMs = kafkaStreamsConfig.getLong(punctuatorName + WINDOW_CONFIG_SUFFIX); | ||
} else { | ||
this.windowMs = DEFAULT_WINDOW_MS; | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
...va/org/hypertrace/core/kafkastreams/framework/punctuators/action/CompletedTaskResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package org.hypertrace.core.kafkastreams.framework.punctuators.action; | ||
|
||
import java.util.Optional; | ||
|
||
public class CompletedTaskResult implements TaskResult { | ||
|
||
@Override | ||
public Optional<Long> getRescheduleTimestamp() { | ||
return Optional.empty(); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...a/org/hypertrace/core/kafkastreams/framework/punctuators/action/RescheduleTaskResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package org.hypertrace.core.kafkastreams.framework.punctuators.action; | ||
|
||
import java.util.Optional; | ||
|
||
public class RescheduleTaskResult implements TaskResult { | ||
private final long rescheduleTimestamp; | ||
|
||
public RescheduleTaskResult(long rescheduleTimestamp) { | ||
this.rescheduleTimestamp = rescheduleTimestamp; | ||
} | ||
|
||
@Override | ||
public Optional<Long> getRescheduleTimestamp() { | ||
return Optional.of(rescheduleTimestamp); | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
...c/main/java/org/hypertrace/core/kafkastreams/framework/punctuators/action/TaskResult.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package org.hypertrace.core.kafkastreams.framework.punctuators.action; | ||
|
||
import java.util.Optional; | ||
|
||
public interface TaskResult { | ||
Optional<Long> getRescheduleTimestamp(); | ||
} |
Oops, something went wrong.