Skip to content

Commit a2f4d6f

Browse files
feat: allow creating drop message for accumulator
Signed-off-by: Vaibhav Tiwari <vaibhav.tiwari33@gmail.com>
1 parent 0c3b01c commit a2f4d6f

4 files changed

Lines changed: 212 additions & 0 deletions

File tree

examples/pom.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,28 @@
381381
</to>
382382
</configuration>
383383
</execution>
384+
<execution>
385+
<id>accumulator-blackhole</id>
386+
<phase>package</phase>
387+
<goals>
388+
<goal>dockerBuild</goal>
389+
</goals>
390+
<configuration>
391+
<from>
392+
<image>amazoncorretto:11</image>
393+
</from>
394+
<container>
395+
<mainClass>
396+
io.numaproj.numaflow.examples.accumulator.blackhole.BlackholeFactory
397+
</mainClass>
398+
</container>
399+
<to>
400+
<image>
401+
numaflow-java-examples/accumulator-blackhole:${docker.tag}
402+
</image>
403+
</to>
404+
</configuration>
405+
</execution>
384406
<execution>
385407
<id>on-success-sink</id>
386408
<phase>package</phase>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.numaproj.numaflow.examples.accumulator.blackhole;
2+
3+
import io.numaproj.numaflow.accumulator.Server;
4+
import io.numaproj.numaflow.accumulator.model.Accumulator;
5+
import io.numaproj.numaflow.accumulator.model.AccumulatorFactory;
6+
import io.numaproj.numaflow.accumulator.model.Datum;
7+
import io.numaproj.numaflow.accumulator.model.Message;
8+
import io.numaproj.numaflow.accumulator.model.OutputStreamObserver;
9+
import lombok.extern.slf4j.Slf4j;
10+
11+
/**
12+
* Blackhole is an accumulator that intentionally discards every datum it receives without
13+
* forwarding any data downstream.
14+
*
15+
* <p>A naive implementation would simply read the input stream and emit nothing. However, an
16+
* accumulator that never emits anything for the datums it consumes leaves the framework unable to
17+
* release the per-datum tracked state, leading to unbounded memory growth.
18+
*
19+
* <p>Instead, this example emits a drop message for every datum using {@link Message#toDrop(Datum)}.
20+
* A drop message is not forwarded to the next vertex, but it still allows the framework to advance
21+
* the watermark and release the tracked state for that datum - giving us "blackhole" semantics
22+
* without leaking memory. This pattern is useful for multiplexer-, cross-join-, or filter-style
23+
* accumulators that legitimately need to omit some (or all) of their inputs.
24+
*/
25+
@Slf4j
26+
public class BlackholeFactory extends AccumulatorFactory<BlackholeFactory.Blackhole> {
27+
28+
public static void main(String[] args) throws Exception {
29+
log.info("Starting blackhole accumulator server..");
30+
Server server = new Server(new BlackholeFactory());
31+
32+
// Start the server
33+
server.start();
34+
35+
// wait for the server to shut down
36+
server.awaitTermination();
37+
log.info("Blackhole accumulator server exited..");
38+
}
39+
40+
@Override
41+
public Blackhole createAccumulator() {
42+
return new Blackhole();
43+
}
44+
45+
public static class Blackhole extends Accumulator {
46+
@Override
47+
public void processMessage(Datum datum, OutputStreamObserver outputStream) {
48+
log.info(
49+
"Dropping datum with event time: {}, watermark: {}",
50+
datum.getEventTime().toEpochMilli(),
51+
datum.getWatermark().toEpochMilli());
52+
// Emit a drop message: nothing is forwarded downstream, but the framework still
53+
// advances the watermark and releases the tracked state for this datum.
54+
outputStream.send(Message.toDrop(datum));
55+
}
56+
57+
@Override
58+
public void handleEndOfStream(OutputStreamObserver outputStreamObserver) {
59+
log.info("End of stream received, nothing to flush for blackhole accumulator");
60+
}
61+
}
62+
}

src/main/java/io/numaproj/numaflow/accumulator/model/Message.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
/** Message is used to wrap the data returned by Accumulator functions. */
88
@Getter
99
public class Message {
10+
private static final String[] DROP_TAGS = {"U+005C__DROP__"};
11+
1012
private final Instant eventTime;
1113
private final Instant watermark;
1214
private final Map<String, String> headers;
@@ -32,6 +34,23 @@ public Message(Datum datum) {
3234
this.tags = null;
3335
}
3436

37+
/**
38+
* Creates a Message from the given Datum with drop tags set, so the message is not forwarded to
39+
* the next vertex but still allows the accumulator to advance the watermark and release the
40+
* tracked state. It is advised to use the incoming Datum to construct the message, because event
41+
* time, watermark, id and headers of the message are derived from the Datum. Only use a custom
42+
* implementation of the Datum if you know what you are doing.
43+
*
44+
* @param datum {@link Datum} object to drop the results for
45+
* @return a {@link Message} tagged to be dropped
46+
*/
47+
public static Message toDrop(Datum datum) {
48+
Message message = new Message(datum);
49+
message.setValue(new byte[0]);
50+
message.setTags(DROP_TAGS);
51+
return message;
52+
}
53+
3554
/*
3655
* sets the value of the message
3756
*
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package io.numaproj.numaflow.accumulator.model;
2+
3+
import org.junit.Test;
4+
5+
import java.time.Instant;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
import static org.junit.Assert.assertArrayEquals;
10+
import static org.junit.Assert.assertEquals;
11+
12+
public class MessageTest {
13+
14+
@Test
15+
public void testMessageFromDatum() {
16+
Datum datum = buildDatum();
17+
Message message = new Message(datum);
18+
19+
assertArrayEquals("hello".getBytes(), message.getValue());
20+
assertArrayEquals(new String[]{"key1", "key2"}, message.getKeys());
21+
assertArrayEquals(null, message.getTags());
22+
assertEquals("test-id", message.getId());
23+
assertEquals(Instant.ofEpochMilli(60000), message.getEventTime());
24+
assertEquals(Instant.ofEpochMilli(59000), message.getWatermark());
25+
}
26+
27+
@Test
28+
public void testToDrop() {
29+
Datum datum = buildDatum();
30+
Message message = Message.toDrop(datum);
31+
32+
// The DROP tag must be set so the message is not forwarded downstream.
33+
String[] dropTags = {"U+005C__DROP__"};
34+
assertArrayEquals(dropTags, message.getTags());
35+
// No value is forwarded, but the identifying/watermark fields are carried over so the
36+
// accumulator can advance the watermark and release the tracked state.
37+
assertArrayEquals(new byte[0], message.getValue());
38+
assertArrayEquals(new String[]{"key1", "key2"}, message.getKeys());
39+
assertEquals("test-id", message.getId());
40+
assertEquals(Instant.ofEpochMilli(60000), message.getEventTime());
41+
assertEquals(Instant.ofEpochMilli(59000), message.getWatermark());
42+
}
43+
44+
private Datum buildDatum() {
45+
Map<String, String> headers = new HashMap<>();
46+
headers.put("x", "y");
47+
return new TestDatum(
48+
new String[]{"key1", "key2"},
49+
"hello".getBytes(),
50+
Instant.ofEpochMilli(59000),
51+
Instant.ofEpochMilli(60000),
52+
headers,
53+
"test-id");
54+
}
55+
56+
private static class TestDatum implements Datum {
57+
private final String[] keys;
58+
private final byte[] value;
59+
private final Instant watermark;
60+
private final Instant eventTime;
61+
private final Map<String, String> headers;
62+
private final String id;
63+
64+
TestDatum(
65+
String[] keys,
66+
byte[] value,
67+
Instant watermark,
68+
Instant eventTime,
69+
Map<String, String> headers,
70+
String id) {
71+
this.keys = keys;
72+
this.value = value;
73+
this.watermark = watermark;
74+
this.eventTime = eventTime;
75+
this.headers = headers;
76+
this.id = id;
77+
}
78+
79+
@Override
80+
public byte[] getValue() {
81+
return value;
82+
}
83+
84+
@Override
85+
public String[] getKeys() {
86+
return keys;
87+
}
88+
89+
@Override
90+
public Instant getEventTime() {
91+
return eventTime;
92+
}
93+
94+
@Override
95+
public Instant getWatermark() {
96+
return watermark;
97+
}
98+
99+
@Override
100+
public Map<String, String> getHeaders() {
101+
return headers;
102+
}
103+
104+
@Override
105+
public String getID() {
106+
return id;
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)