wave13: 514/239 — flink/nifi/artemis + K8s/cilium/linkerd2 + ES/OpenSearch/Solr + hadoop/hbase/spark
This commit is contained in:
parent
424a2a7787
commit
8f0bc73afa
32 changed files with 3977 additions and 5 deletions
|
|
@ -0,0 +1,91 @@
|
|||
# artemis-0001: BindingsImpl routeFromCluster O(R×A) → O(R+A)
|
||||
|
||||
## Location
|
||||
`artemis-server/src/main/java/org/apache/activemq/artemis/core/postoffice/impl/BindingsImpl.java`
|
||||
Lines 630–664 (`routeFromCluster`)
|
||||
|
||||
## Severity
|
||||
HIGH — called on every clustered message that requires selective ACK routing
|
||||
|
||||
## Description
|
||||
`routeFromCluster` decodes two byte arrays: `ids` (all binding IDs to route to) and
|
||||
`idsToAck` (subset that require ACK). It builds `idsToAckList` as an `ArrayList<Long>`,
|
||||
then iterates over `ids` calling `idsToAckList.contains(bindingID)` for each entry.
|
||||
|
||||
For R route targets and A ACK targets, this is O(R×A). Since this method is called
|
||||
on the hot message routing path in clustered deployments, it compounds per message.
|
||||
|
||||
## Root Cause
|
||||
```java
|
||||
private void routeFromCluster(final Message message, final RoutingContext context,
|
||||
final byte[] ids) throws Exception {
|
||||
byte[] idsToAck = (byte[]) message.removeProperty(Message.HDR_ROUTE_TO_ACK_IDS);
|
||||
List<Long> idsToAckList = new ArrayList<>(); // ← ArrayList
|
||||
|
||||
if (idsToAck != null) {
|
||||
ByteBuffer buff = ByteBuffer.wrap(idsToAck);
|
||||
while (buff.hasRemaining()) {
|
||||
idsToAckList.add(buff.getLong()); // populate O(A)
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer buff = ByteBuffer.wrap(ids);
|
||||
while (buff.hasRemaining()) { // O(R) loop
|
||||
long bindingID = buff.getLong();
|
||||
Binding binding = bindingsIdMap.get(bindingID);
|
||||
if (binding != null) {
|
||||
if (idsToAckList.contains(bindingID)) { // O(A) scan → O(R×A) total
|
||||
binding.routeWithAck(message, context);
|
||||
} else {
|
||||
binding.route(message, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fix
|
||||
Use a `HashSet<Long>` instead of `ArrayList<Long>` for `idsToAckSet`:
|
||||
|
||||
```java
|
||||
private void routeFromCluster(final Message message, final RoutingContext context,
|
||||
final byte[] ids) throws Exception {
|
||||
byte[] idsToAck = (byte[]) message.removeProperty(Message.HDR_ROUTE_TO_ACK_IDS);
|
||||
Set<Long> idsToAckSet = new HashSet<>(); // ← HashSet
|
||||
|
||||
if (idsToAck != null) {
|
||||
ByteBuffer buff = ByteBuffer.wrap(idsToAck);
|
||||
while (buff.hasRemaining()) {
|
||||
idsToAckSet.add(buff.getLong()); // populate O(A)
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer buff = ByteBuffer.wrap(ids);
|
||||
while (buff.hasRemaining()) { // O(R) loop
|
||||
long bindingID = buff.getLong();
|
||||
Binding binding = bindingsIdMap.get(bindingID);
|
||||
if (binding != null) {
|
||||
if (idsToAckSet.contains(bindingID)) { // O(1) → O(R+A) total
|
||||
binding.routeWithAck(message, context);
|
||||
} else {
|
||||
binding.route(message, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complexity
|
||||
| | Before | After |
|
||||
|---|---|---|
|
||||
| routeFromCluster | O(R×A) | O(R+A) |
|
||||
|
||||
Where R=routing targets, A=ACK targets.
|
||||
At R=100 bindings, A=50 ACKs: 5,000 ops per message → 150 ops (33x improvement).
|
||||
Under sustained load of 10,000 msg/s this saves ~49.8M operations/second.
|
||||
|
||||
## Context
|
||||
This is in the clustered message routing path — called when the broker receives a
|
||||
message with `HDR_ROUTE_TO_ACK_IDS` set, routing to multiple queues where some
|
||||
require acknowledgment. In large clustered deployments with many queues per address,
|
||||
R and A can be large.
|
||||
203
defects/artemis/unit/BindingsRouteFromClusterAlgorithm.java
Normal file
203
defects/artemis/unit/BindingsRouteFromClusterAlgorithm.java
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package unit;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* artemis-0001: BindingsImpl routeFromCluster O(R×A) → O(R+A)
|
||||
*
|
||||
* Simulates the routing logic in:
|
||||
* artemis-server/.../core/postoffice/impl/BindingsImpl.java
|
||||
* Method: routeFromCluster (lines 630–664)
|
||||
*
|
||||
* Standalone — no JUnit, no Artemis deps.
|
||||
*/
|
||||
public class BindingsRouteFromClusterAlgorithm {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Result types
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static class RouteResult {
|
||||
final long ops;
|
||||
final List<Long> routedWithAck;
|
||||
final List<Long> routedWithoutAck;
|
||||
|
||||
RouteResult(long ops, List<Long> routedWithAck, List<Long> routedWithoutAck) {
|
||||
this.ops = ops;
|
||||
this.routedWithAck = routedWithAck;
|
||||
this.routedWithoutAck = routedWithoutAck;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Slow: ArrayList.contains inside loop (production code)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static RouteResult routeFromClusterSlow(long[] routeIds, long[] ackIds) {
|
||||
long ops = 0;
|
||||
|
||||
// Build idsToAckList as ArrayList (defect)
|
||||
List<Long> idsToAckList = new ArrayList<>();
|
||||
for (long id : ackIds) {
|
||||
idsToAckList.add(id);
|
||||
}
|
||||
|
||||
List<Long> routedWithAck = new ArrayList<>();
|
||||
List<Long> routedWithoutAck = new ArrayList<>();
|
||||
|
||||
for (long bindingID : routeIds) { // O(R) loop
|
||||
// List.contains: O(A) scan
|
||||
boolean needsAck = false;
|
||||
for (Long ackId : idsToAckList) {
|
||||
ops++;
|
||||
if (ackId.equals(bindingID)) {
|
||||
needsAck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (needsAck) {
|
||||
routedWithAck.add(bindingID);
|
||||
} else {
|
||||
routedWithoutAck.add(bindingID);
|
||||
}
|
||||
}
|
||||
|
||||
return new RouteResult(ops, routedWithAck, routedWithoutAck);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Fast: HashSet for O(1) lookup
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static RouteResult routeFromClusterFast(long[] routeIds, long[] ackIds) {
|
||||
long ops = 0;
|
||||
|
||||
// Build idsToAckSet as HashSet (fix)
|
||||
Set<Long> idsToAckSet = new HashSet<>();
|
||||
for (long id : ackIds) {
|
||||
ops++;
|
||||
idsToAckSet.add(id);
|
||||
}
|
||||
|
||||
List<Long> routedWithAck = new ArrayList<>();
|
||||
List<Long> routedWithoutAck = new ArrayList<>();
|
||||
|
||||
for (long bindingID : routeIds) { // O(R) loop
|
||||
ops++;
|
||||
if (idsToAckSet.contains(bindingID)) { // O(1)
|
||||
routedWithAck.add(bindingID);
|
||||
} else {
|
||||
routedWithoutAck.add(bindingID);
|
||||
}
|
||||
}
|
||||
|
||||
return new RouteResult(ops, routedWithAck, routedWithoutAck);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test harness
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public static void main(String[] args) {
|
||||
int passed = 0;
|
||||
int total = 0;
|
||||
|
||||
// ---- Test 1: basic routing correctness ----
|
||||
total++;
|
||||
long[] routes = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L};
|
||||
long[] acks = {2L, 4L, 6L, 8L, 10L};
|
||||
|
||||
RouteResult slow1 = routeFromClusterSlow(routes, acks);
|
||||
RouteResult fast1 = routeFromClusterFast(routes, acks);
|
||||
|
||||
boolean match1 = new HashSet<>(slow1.routedWithAck).equals(new HashSet<>(fast1.routedWithAck)) &&
|
||||
new HashSet<>(slow1.routedWithoutAck).equals(new HashSet<>(fast1.routedWithoutAck));
|
||||
if (match1) {
|
||||
System.out.println("PASS test1: basic routing — ack=" + slow1.routedWithAck.size() +
|
||||
" noack=" + slow1.routedWithoutAck.size());
|
||||
passed++;
|
||||
} else {
|
||||
System.out.println("FAIL test1: routing mismatch");
|
||||
}
|
||||
|
||||
// ---- Test 2: no ACKs required ----
|
||||
total++;
|
||||
long[] noAcks = {};
|
||||
RouteResult slow2 = routeFromClusterSlow(routes, noAcks);
|
||||
RouteResult fast2 = routeFromClusterFast(routes, noAcks);
|
||||
|
||||
boolean match2 = slow2.routedWithAck.isEmpty() && fast2.routedWithAck.isEmpty() &&
|
||||
slow2.routedWithoutAck.size() == routes.length &&
|
||||
fast2.routedWithoutAck.size() == routes.length;
|
||||
if (match2) {
|
||||
System.out.println("PASS test2: no-ACK case — all routes without ack");
|
||||
passed++;
|
||||
} else {
|
||||
System.out.println("FAIL test2: no-ACK case mismatch");
|
||||
}
|
||||
|
||||
// ---- Test 3: all ACKs required ----
|
||||
total++;
|
||||
RouteResult slow3 = routeFromClusterSlow(routes, routes);
|
||||
RouteResult fast3 = routeFromClusterFast(routes, routes);
|
||||
|
||||
boolean match3 = slow3.routedWithAck.size() == routes.length &&
|
||||
fast3.routedWithAck.size() == routes.length &&
|
||||
slow3.routedWithoutAck.isEmpty() && fast3.routedWithoutAck.isEmpty();
|
||||
if (match3) {
|
||||
System.out.println("PASS test3: all-ACK case — all routes with ack");
|
||||
passed++;
|
||||
} else {
|
||||
System.out.println("FAIL test3: all-ACK case mismatch");
|
||||
}
|
||||
|
||||
// ---- Test 4: large scale ops ratio ----
|
||||
total++;
|
||||
int R = 100; // route targets
|
||||
int A = 50; // ACK targets
|
||||
|
||||
long[] bigRoutes = new long[R];
|
||||
for (int i = 0; i < R; i++) bigRoutes[i] = i;
|
||||
|
||||
long[] bigAcks = new long[A];
|
||||
for (int i = 0; i < A; i++) bigAcks[i] = i * 2; // every other route needs ACK
|
||||
|
||||
RouteResult slowBig = routeFromClusterSlow(bigRoutes, bigAcks);
|
||||
RouteResult fastBig = routeFromClusterFast(bigRoutes, bigAcks);
|
||||
|
||||
boolean matchBig = new HashSet<>(slowBig.routedWithAck).equals(new HashSet<>(fastBig.routedWithAck));
|
||||
if (!matchBig) {
|
||||
System.out.println("FAIL test4: large scale result mismatch");
|
||||
} else {
|
||||
double ratio = (double) slowBig.ops / fastBig.ops;
|
||||
if (ratio >= 10.0) {
|
||||
System.out.printf("PASS test4: large scale slow=%d ops, fast=%d ops, ratio=%.1fx%n",
|
||||
slowBig.ops, fastBig.ops, ratio);
|
||||
passed++;
|
||||
} else {
|
||||
System.out.printf("FAIL test4: ratio=%.1fx (need >=10x) slow=%d fast=%d%n",
|
||||
ratio, slowBig.ops, fastBig.ops);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Test 5: route IDs not in ACK list ----
|
||||
total++;
|
||||
long[] disjointAcks = {200L, 201L, 202L}; // none match routes 0..9
|
||||
RouteResult slow5 = routeFromClusterSlow(routes, disjointAcks);
|
||||
RouteResult fast5 = routeFromClusterFast(routes, disjointAcks);
|
||||
|
||||
boolean match5 = slow5.routedWithAck.isEmpty() && fast5.routedWithAck.isEmpty();
|
||||
if (match5) {
|
||||
System.out.println("PASS test5: disjoint ACK IDs — no routes with ack");
|
||||
passed++;
|
||||
} else {
|
||||
System.out.println("FAIL test5: disjoint ACK IDs mismatch");
|
||||
}
|
||||
|
||||
System.out.println("\n" + passed + "/" + total + " PASS");
|
||||
|
||||
if (passed != total) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue