Achei essa pergunta muito interessante e pensei em tentar.
Conforme eu avaliei mais, sua tentativa em si é boa, exceto o seguinte:
particionado pelos 5-6 primeiros dígitos da latitude concatenados aos 5-6 primeiros dígitos da longitude
Se você já possui um método para obter o ID / nome da seção de estrada com base na latitude e longitude, por que não chamar esse método primeiro e usar o ID / nome da seção de estrada para particionar os dados em primeiro lugar?
E depois disso, tudo fica bem fácil, então a topologia será
Merge all four streams ->
Select key as the road section id/name ->
Group the stream by Key ->
Use time windowed aggregation for the given time ->
Materialize it to a store.
(Uma explicação mais detalhada pode ser encontrada nos comentários no código abaixo. Por favor, pergunte se algo não está claro)
Adicionei o código no final desta resposta. Observe que, em vez da média, usei soma, pois é mais fácil de demonstrar. É possível fazer a média armazenando alguns dados extras.
Eu detalhei a resposta nos comentários. A seguir, é apresentado um diagrama de topologia gerado a partir do código (graças a https://zz85.github.io/kafka-streams-viz/ )
Topologia:
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.Topology;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.TimeWindows;
import org.apache.kafka.streams.state.Stores;
import org.apache.kafka.streams.state.WindowBytesStoreSupplier;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
public class VehicleStream {
// 5 minutes aggregation window
private static final long AGGREGATION_WINDOW = 5 * 50 * 1000L;
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
// Setting configs, change accordingly
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "vehicle.stream.app");
properties.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092,kafka2:19092");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
// initializing a streambuilder for building topology.
final StreamsBuilder builder = new StreamsBuilder();
// Our initial 4 streams.
List<String> streamInputTopics = Arrays.asList(
"vehicle.stream1", "vehicle.stream2",
"vehicle.stream3", "vehicle.stream4"
);
/*
* Since there is no connection between a specific stream
* to a specific road or vehicle or anything else,
* we can take all four streams as a single stream
*/
KStream<String, String> source = builder.stream(streamInputTopics);
/*
* The initial key is unimportant (which can be ignored),
* Instead, we will be using the section name/id as key.
* Data will contain comma separated values in following format.
* VehicleId,Speed,Latitude,Longitude
*/
WindowBytesStoreSupplier windowSpeedStore = Stores.persistentWindowStore(
"windowSpeedStore",
AGGREGATION_WINDOW,
2, 10, true
);
source
.peek((k, v) -> printValues("Initial", k, v))
// First, we rekey the stream based on the road section.
.selectKey(VehicleStream::selectKeyAsRoadSection)
.peek((k, v) -> printValues("After rekey", k, v))
.groupByKey()
.windowedBy(TimeWindows.of(AGGREGATION_WINDOW))
.aggregate(
() -> "0.0", // Initialize
/*
* I'm using summing here for the aggregation as that's easier.
* It can be converted to average by storing extra details on number of records, etc..
*/
(k, v, previousSpeed) -> // Aggregator (summing speed)
String.valueOf(
Double.parseDouble(previousSpeed) +
VehicleSpeed.getVehicleSpeed(v).speed
),
Materialized.as(windowSpeedStore)
);
// generating the topology
final Topology topology = builder.build();
System.out.print(topology.describe());
// constructing a streams client with the properties and topology
final KafkaStreams streams = new KafkaStreams(topology, properties);
final CountDownLatch latch = new CountDownLatch(1);
// attaching shutdown handler
Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
@Override
public void run() {
streams.close();
latch.countDown();
}
});
try {
streams.start();
latch.await();
} catch (Throwable e) {
System.exit(1);
}
System.exit(0);
}
private static void printValues(String message, String key, Object value) {
System.out.printf("===%s=== key: %s value: %s%n", message, key, value.toString());
}
private static String selectKeyAsRoadSection(String key, String speedValue) {
// Would make more sense when it's the section id, rather than a name.
return coordinateToRoadSection(
VehicleSpeed.getVehicleSpeed(speedValue).latitude,
VehicleSpeed.getVehicleSpeed(speedValue).longitude
);
}
private static String coordinateToRoadSection(String latitude, String longitude) {
// Dummy function
return "Area 51";
}
public static class VehicleSpeed {
public String vehicleId;
public double speed;
public String latitude;
public String longitude;
public static VehicleSpeed getVehicleSpeed(String data) {
return new VehicleSpeed(data);
}
public VehicleSpeed(String data) {
String[] dataArray = data.split(",");
this.vehicleId = dataArray[0];
this.speed = Double.parseDouble(dataArray[1]);
this.latitude = dataArray[2];
this.longitude = dataArray[3];
}
@Override
public String toString() {
return String.format("veh: %s, speed: %f, latlong : %s,%s", vehicleId, speed, latitude, longitude);
}
}
}