Repository navigation
Expand file tree
/
Copy pathEnrichUsers.java
More file actions
122 lines (105 loc) · 5.31 KB
/
Copy pathEnrichUsers.java
File metadata and controls
122 lines (105 loc) · 5.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import dev.whitewater.Feed;
import dev.whitewater.FeedWriter;
import dev.whitewater.StateStore;
import dev.whitewater.WhitewaterClient;
import dev.whitewater.model.FeedEvent;
import dev.whitewater.model.FeedMessage;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.temporal.IsoFields;
import java.util.Map;
import java.util.UUID;
/**
* A permanent enrichment consumer — the Kafka-streams shape, with the
* plumbing inside the SDK. Reads users.update.request, enriches each
* record from the users.profiles StateStore (primary key, then the
* user_id named key), writes users.update.enriched.request or the DLQ,
* and bumps day/week/month counters.
*
* The named Reader "enricher" owns durable server-side progress: a
* pod restart or fenced session resumes exactly where it acked.
*/
public class EnrichUsers {
public record UpdateRequest(String requestId, String userEmail,
String userId, Map<String, String> changes) {}
public record EnrichedUpdate(String requestId, String userId,
String email, String displayName,
String region, Map<String, String> changes) {}
public record UserProfile(String userId, String email,
String displayName, String region) {}
public record DeadLetter(String reason, String sourceFeed,
String sourceMessageId, String sourceCursor,
UpdateRequest original) {}
public static void main(String[] args) {
var client = WhitewaterClient.builder()
.endpoints(
URI.create("http://localhost:7371"),
URI.create("http://localhost:7372"),
URI.create("http://localhost:7373"))
.apiKey(System.getenv().getOrDefault(
"WHITEWATER_API_KEY",
"whitewater-local-development-admin-key"))
.build();
Feed<String, UpdateRequest> in =
client.feed("users.update.request", UpdateRequest.class);
Feed<String, EnrichedUpdate> out =
client.feed("users.update.enriched.request", EnrichedUpdate.class);
Feed<String, DeadLetter> dlq =
client.feed("users.update.dlq", DeadLetter.class);
StateStore<String, UserProfile> users =
client.stateStore("users.profiles", UserProfile.class);
StateStore<String, Long> counters =
client.stateStore("users.stats", Long.class);
FeedWriter<String, EnrichedUpdate> outWriter = out.writer("enricher");
FeedWriter<String, DeadLetter> dlqWriter = dlq.writer("enricher");
// The whole "permanent consumer loop" — supervision, restart
// backoff, durable resume, ack-after-handler are all inside run().
var consumer = in.consumer("enricher")
.onRecord(msg -> process(msg, users, counters, outWriter, dlqWriter))
.onError(err -> System.err.println("consumer retrying: " + err));
Runtime.getRuntime().addShutdownHook(new Thread(consumer::close));
consumer.run();
}
static void process(FeedMessage<String, UpdateRequest> msg,
StateStore<String, UserProfile> users,
StateStore<String, Long> counters,
FeedWriter<String, EnrichedUpdate> out,
FeedWriter<String, DeadLetter> dlq) {
UpdateRequest req = msg.value();
var user = users.find(req.userEmail())
.or(() -> users.findBy("user_id", req.userId()));
// Idempotent effect identity: a replay after a crash re-sends
// the same requestId, which dedups to the original append.
UUID effectId = UUID.nameUUIDFromBytes(
msg.messageId().toString().getBytes(StandardCharsets.UTF_8));
if (user.isEmpty()) {
dlq.append(new FeedEvent<>(
req.userEmail(),
new DeadLetter("user not found",
"users.update.request",
msg.messageId().toString(),
msg.cursor().toString(), req))
.withRequestId(effectId)
.withTextMetadata(Map.of("reason", "enrichment-miss")));
return;
}
var profile = user.get();
out.append(new FeedEvent<>(
profile.userId(),
new EnrichedUpdate(req.requestId(), profile.userId(),
profile.email(), profile.displayName(),
profile.region(), req.changes()))
.withRequestId(effectId));
var day = LocalDate.now(ZoneOffset.UTC);
increment(counters, "updates.day." + day);
increment(counters, "updates.week." + day.getYear()
+ "-W" + day.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR));
increment(counters, "updates.month." + day.getYear()
+ "-" + String.format("%02d", day.getMonthValue()));
}
static void increment(StateStore<String, Long> counters, String bucket) {
counters.put(bucket, counters.getOrDefault(bucket, 0L) + 1);
}
}