From e1c3a53dd75927e31e6c0d3dbcaadf8d07544f35 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Fri, 27 Jun 2025 03:18:37 -0400 Subject: [PATCH 01/31] Version upgrade initialized. --- .../java/cl/throttr/enums/RequestType.java | 67 ++++++++++++++++++- .../java/cl/throttr/requests/ListRequest.java | 44 ++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/main/java/cl/throttr/requests/ListRequest.java diff --git a/src/main/java/cl/throttr/enums/RequestType.java b/src/main/java/cl/throttr/enums/RequestType.java index e406ef1..5c03a12 100644 --- a/src/main/java/cl/throttr/enums/RequestType.java +++ b/src/main/java/cl/throttr/enums/RequestType.java @@ -47,7 +47,72 @@ public enum RequestType { /** * Get */ - GET(0x06); + GET(0x06), + + /** + * List + */ + LIST(0x07), + + /** + * List + */ + INFO(0x08), + + /** + * Stat + */ + STAT(0x09), + + /** + * Stats + */ + STATS(0x10), + + /** + * Subscribe + */ + SUBSCRIBE(0x11), + + /** + * Unsubscribe + */ + UNSUBSCRIBE(0x12), + + /** + * Publish + */ + PUBLISH(0x13), + + /** + * Connections + */ + CONNECTIONS(0x14), + + /** + * Connection + */ + CONNECTION(0x15), + + /** + * Channels + */ + CHANNELS(0x16), + + /** + * Channel + */ + CHANNEL(0x17), + + /** + * WhoAmI + */ + WHOAMI(0x18), + + /** + * Event + */ + EVENT(0x19); /** * Value diff --git a/src/main/java/cl/throttr/requests/ListRequest.java b/src/main/java/cl/throttr/requests/ListRequest.java new file mode 100644 index 0000000..487df82 --- /dev/null +++ b/src/main/java/cl/throttr/requests/ListRequest.java @@ -0,0 +1,44 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * List request + */ +public record ListRequest( + String key +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + var buffer = ByteBuffer.allocate( + 2 + keyBytes.length + ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) RequestType.LIST.getValue()); + return buffer.array(); + } +} From 2af74ef5c672f6748ad2155efc79c24b72cbe874 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Fri, 27 Jun 2025 05:25:04 -0400 Subject: [PATCH 02/31] All new requests added. --- .../cl/throttr/requests/ChannelRequest.java | 49 ++++++++++++++++ .../cl/throttr/requests/ChannelsRequest.java | 38 +++++++++++++ .../throttr/requests/ConnectionRequest.java | 57 +++++++++++++++++++ .../throttr/requests/ConnectionsRequest.java | 38 +++++++++++++ .../java/cl/throttr/requests/InfoRequest.java | 38 +++++++++++++ .../java/cl/throttr/requests/ListRequest.java | 10 +--- .../cl/throttr/requests/PublishRequest.java | 56 ++++++++++++++++++ .../java/cl/throttr/requests/StatRequest.java | 49 ++++++++++++++++ .../cl/throttr/requests/StatsRequest.java | 38 +++++++++++++ .../cl/throttr/requests/SubscribeRequest.java | 49 ++++++++++++++++ .../throttr/requests/UnsubscribeRequest.java | 49 ++++++++++++++++ .../cl/throttr/requests/WhoAmiRequest.java | 38 +++++++++++++ 12 files changed, 501 insertions(+), 8 deletions(-) create mode 100644 src/main/java/cl/throttr/requests/ChannelRequest.java create mode 100644 src/main/java/cl/throttr/requests/ChannelsRequest.java create mode 100644 src/main/java/cl/throttr/requests/ConnectionRequest.java create mode 100644 src/main/java/cl/throttr/requests/ConnectionsRequest.java create mode 100644 src/main/java/cl/throttr/requests/InfoRequest.java create mode 100644 src/main/java/cl/throttr/requests/PublishRequest.java create mode 100644 src/main/java/cl/throttr/requests/StatRequest.java create mode 100644 src/main/java/cl/throttr/requests/StatsRequest.java create mode 100644 src/main/java/cl/throttr/requests/SubscribeRequest.java create mode 100644 src/main/java/cl/throttr/requests/UnsubscribeRequest.java create mode 100644 src/main/java/cl/throttr/requests/WhoAmiRequest.java diff --git a/src/main/java/cl/throttr/requests/ChannelRequest.java b/src/main/java/cl/throttr/requests/ChannelRequest.java new file mode 100644 index 0000000..a334cf8 --- /dev/null +++ b/src/main/java/cl/throttr/requests/ChannelRequest.java @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Stat request + */ +public record ChannelRequest( + String channel +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + byte[] channelBytes = channel.getBytes(StandardCharsets.UTF_8); + + var buffer = ByteBuffer.allocate( + 2 + channelBytes.length + ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) RequestType.CHANNEL.getValue()); + buffer.put((byte) channelBytes.length); + buffer.put(channelBytes); + + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/ChannelsRequest.java b/src/main/java/cl/throttr/requests/ChannelsRequest.java new file mode 100644 index 0000000..b70e6f8 --- /dev/null +++ b/src/main/java/cl/throttr/requests/ChannelsRequest.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Channels request + */ +public record ChannelsRequest() { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + var buffer = ByteBuffer.allocate(1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) RequestType.CHANNELS.getValue()); + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/ConnectionRequest.java b/src/main/java/cl/throttr/requests/ConnectionRequest.java new file mode 100644 index 0000000..cc128d7 --- /dev/null +++ b/src/main/java/cl/throttr/requests/ConnectionRequest.java @@ -0,0 +1,57 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Stat request + */ +public record ConnectionRequest( + String id +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + byte[] idBytes = hexStringToByteArray(id); + + var buffer = ByteBuffer.allocate(1 + 16); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) RequestType.CONNECTION.getValue()); + buffer.put(idBytes); + + return buffer.array(); + } + + private static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + int hi = Character.digit(s.charAt(i), 16); + int lo = Character.digit(s.charAt(i + 1), 16); + data[i / 2] = (byte) ((hi << 4) + lo); + } + return data; + } +} diff --git a/src/main/java/cl/throttr/requests/ConnectionsRequest.java b/src/main/java/cl/throttr/requests/ConnectionsRequest.java new file mode 100644 index 0000000..0797d84 --- /dev/null +++ b/src/main/java/cl/throttr/requests/ConnectionsRequest.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Connections request + */ +public record ConnectionsRequest() { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + var buffer = ByteBuffer.allocate(1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) RequestType.CONNECTIONS.getValue()); + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/InfoRequest.java b/src/main/java/cl/throttr/requests/InfoRequest.java new file mode 100644 index 0000000..efefadc --- /dev/null +++ b/src/main/java/cl/throttr/requests/InfoRequest.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Info request + */ +public record InfoRequest() { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + var buffer = ByteBuffer.allocate(1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) RequestType.INFO.getValue()); + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/ListRequest.java b/src/main/java/cl/throttr/requests/ListRequest.java index 487df82..8e38a5f 100644 --- a/src/main/java/cl/throttr/requests/ListRequest.java +++ b/src/main/java/cl/throttr/requests/ListRequest.java @@ -19,24 +19,18 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; /** * List request */ -public record ListRequest( - String key -) { +public record ListRequest() { /** * To bytes * * @return byte[] */ public byte[] toBytes() { - byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); - var buffer = ByteBuffer.allocate( - 2 + keyBytes.length - ); + var buffer = ByteBuffer.allocate(1); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.put((byte) RequestType.LIST.getValue()); return buffer.array(); diff --git a/src/main/java/cl/throttr/requests/PublishRequest.java b/src/main/java/cl/throttr/requests/PublishRequest.java new file mode 100644 index 0000000..c6f85fc --- /dev/null +++ b/src/main/java/cl/throttr/requests/PublishRequest.java @@ -0,0 +1,56 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; +import cl.throttr.enums.ValueSize; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +import static cl.throttr.utils.Binary.put; + +/** + * Publish request + */ +public record PublishRequest( + String channel, + String payload +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes(ValueSize size) { + byte[] channelBytes = channel.getBytes(StandardCharsets.UTF_8); + byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); + + var buffer = ByteBuffer.allocate( + 2 + channelBytes.length + size.getValue() + payloadBytes.length + ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) RequestType.PUBLISH.getValue()); + buffer.put((byte) channelBytes.length); + put(buffer, payloadBytes.length, size); + buffer.put(channelBytes); + buffer.put(payloadBytes); + + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/StatRequest.java b/src/main/java/cl/throttr/requests/StatRequest.java new file mode 100644 index 0000000..7b52072 --- /dev/null +++ b/src/main/java/cl/throttr/requests/StatRequest.java @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Stat request + */ +public record StatRequest( + String key +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + + var buffer = ByteBuffer.allocate( + 2 + keyBytes.length + ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) RequestType.STAT.getValue()); + buffer.put((byte) keyBytes.length); + buffer.put(keyBytes); + + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/StatsRequest.java b/src/main/java/cl/throttr/requests/StatsRequest.java new file mode 100644 index 0000000..058fee7 --- /dev/null +++ b/src/main/java/cl/throttr/requests/StatsRequest.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Stats request + */ +public record StatsRequest() { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + var buffer = ByteBuffer.allocate(1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) RequestType.STATS.getValue()); + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/SubscribeRequest.java b/src/main/java/cl/throttr/requests/SubscribeRequest.java new file mode 100644 index 0000000..f37c967 --- /dev/null +++ b/src/main/java/cl/throttr/requests/SubscribeRequest.java @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Subscribe request + */ +public record SubscribeRequest( + String channel +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + byte[] channelBytes = channel.getBytes(StandardCharsets.UTF_8); + + var buffer = ByteBuffer.allocate( + 2 + channelBytes.length + ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) RequestType.STAT.getValue()); + buffer.put((byte) channelBytes.length); + buffer.put(channelBytes); + + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/UnsubscribeRequest.java b/src/main/java/cl/throttr/requests/UnsubscribeRequest.java new file mode 100644 index 0000000..42aa357 --- /dev/null +++ b/src/main/java/cl/throttr/requests/UnsubscribeRequest.java @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * Unsubscribe request + */ +public record UnsubscribeRequest( + String channel +) { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + byte[] channelBytes = channel.getBytes(StandardCharsets.UTF_8); + + var buffer = ByteBuffer.allocate( + 2 + channelBytes.length + ); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.put((byte) RequestType.STAT.getValue()); + buffer.put((byte) channelBytes.length); + buffer.put(channelBytes); + + return buffer.array(); + } +} diff --git a/src/main/java/cl/throttr/requests/WhoAmiRequest.java b/src/main/java/cl/throttr/requests/WhoAmiRequest.java new file mode 100644 index 0000000..a035b50 --- /dev/null +++ b/src/main/java/cl/throttr/requests/WhoAmiRequest.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.RequestType; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * WhoAmI request + */ +public record WhoAmiRequest() { + /** + * To bytes + * + * @return byte[] + */ + public byte[] toBytes() { + var buffer = ByteBuffer.allocate(1); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) RequestType.WHOAMI.getValue()); + return buffer.array(); + } +} From eeab8e467dafa3033e663b8ca286462637123a71 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Fri, 27 Jun 2025 13:08:46 -0400 Subject: [PATCH 03/31] INFO, LIST, STATS and STAT implemented. --- .github/workflows/build.yml | 2 +- src/main/java/cl/throttr/Connection.java | 229 +++++++++++++++++- .../cl/throttr/responses/InfoResponse.java | 156 ++++++++++++ .../java/cl/throttr/responses/ListItem.java | 62 +++++ .../cl/throttr/responses/ListResponse.java | 36 +++ .../cl/throttr/responses/StatResponse.java | 77 ++++++ .../java/cl/throttr/responses/StatsItem.java | 62 +++++ .../cl/throttr/responses/StatsResponse.java | 36 +++ src/test/java/cl/throttr/ServiceTest.java | 104 ++++++++ 9 files changed, 760 insertions(+), 4 deletions(-) create mode 100644 src/main/java/cl/throttr/responses/InfoResponse.java create mode 100644 src/main/java/cl/throttr/responses/ListItem.java create mode 100644 src/main/java/cl/throttr/responses/ListResponse.java create mode 100644 src/main/java/cl/throttr/responses/StatResponse.java create mode 100644 src/main/java/cl/throttr/responses/StatsItem.java create mode 100644 src/main/java/cl/throttr/responses/StatsResponse.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1539293..edf85ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: services: throttr: - image: ghcr.io/throttr/throttr:4.0.17-debug-${{ matrix.size }} + image: ghcr.io/throttr/throttr:5.0.8-debug-${{ matrix.size }}-AMD64-metrics-enabled ports: - 9000:9000 diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 69055a2..3443127 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -15,12 +15,11 @@ package cl.throttr; +import cl.throttr.enums.RequestType; import cl.throttr.enums.TTLType; import cl.throttr.enums.ValueSize; import cl.throttr.requests.*; -import cl.throttr.responses.GetResponse; -import cl.throttr.responses.QueryResponse; -import cl.throttr.responses.StatusResponse; +import cl.throttr.responses.*; import cl.throttr.utils.Binary; import java.io.ByteArrayOutputStream; @@ -108,6 +107,10 @@ public Object send(Object request) throws IOException { Object response = switch (type) { case 0x02 -> readQueryResponse(head); case 0x06 -> readGetResponse(head); + case 0x07 -> readListResponse(head); + case 0x08 -> readInfoResponse(head); + case 0x09 -> readStatResponse(head); + case 0x10 -> readStatsResponse(head); default -> readStatusResponse(head); }; responses.add(response); @@ -131,10 +134,226 @@ public Object send(Object request) throws IOException { return switch (type) { case 0x02 -> readQueryResponse(head); case 0x06 -> readGetResponse(head); + case 0x07 -> readListResponse(head); + case 0x08 -> readInfoResponse(head); + case 0x09 -> readStatResponse(head); + case 0x10 -> readStatsResponse(head); default -> readStatusResponse(head); }; } + private ListResponse readListResponse(int head) throws IOException { + if (head != 0x01) { + return new ListResponse(false, new ArrayList<>()); + } + + List items = new ArrayList<>(); + + byte[] header = new byte[8]; + int offset = 0; + while (offset < 8) { + int read = in.read(header, offset, 8 - offset); + if (read == -1) throw new IOException("Unexpected EOF while reading fragments count"); + offset += read; + } + + ByteBuffer hb = ByteBuffer.wrap(header).order(java.nio.ByteOrder.LITTLE_ENDIAN); + long fragments = hb.getLong(); + + for (long f = 0; f < fragments; f++) { + byte[] skip = new byte[8]; + in.read(skip); + + // Leer cantidad de claves + byte[] keysHeader = new byte[8]; + in.read(keysHeader); + ByteBuffer khb = ByteBuffer.wrap(keysHeader).order(java.nio.ByteOrder.LITTLE_ENDIAN); + long keysInFragment = khb.getLong(); + + // Leer headers de claves + List scopedItems = new ArrayList<>(); + List keyLengths = new ArrayList<>(); + int perKeyHeader = 3 + 8 + size.getValue(); // key_length, key_type, ttl_type, expires_at, bytes_used + + byte[] keyHeaderBuffer = new byte[(int) keysInFragment * perKeyHeader]; + offset = 0; + while (offset < keyHeaderBuffer.length) { + int read = in.read(keyHeaderBuffer, offset, keyHeaderBuffer.length - offset); + if (read == -1) throw new IOException("Unexpected EOF while reading key headers"); + offset += read; + } + + ByteBuffer kb = ByteBuffer.wrap(keyHeaderBuffer).order(java.nio.ByteOrder.LITTLE_ENDIAN); + + for (int i = 0; i < keysInFragment; i++) { + int keyLength = Byte.toUnsignedInt(kb.get()); + int keyType = Byte.toUnsignedInt(kb.get()); + int ttlType = Byte.toUnsignedInt(kb.get()); + long expiresAt = kb.getLong(); + long bytesUsed = Binary.read(kb, size); + + scopedItems.add(new ListItem( + "", keyLength, keyType, ttlType, expiresAt, bytesUsed + )); + keyLengths.add(keyLength); + } + + // Leer claves reales + for (int i = 0; i < scopedItems.size(); i++) { + int len = keyLengths.get(i); + byte[] key = new byte[len]; + offset = 0; + while (offset < len) { + int read = in.read(key, offset, len - offset); + if (read == -1) throw new IOException("Unexpected EOF while reading key string"); + offset += read; + } + scopedItems.get(i).setKey(new String(key)); + } + + items.addAll(scopedItems); + } + + return new ListResponse(true, items); + } + + private StatsResponse readStatsResponse(int head) throws IOException { + if (head != 0x01) { + return new StatsResponse(false, new ArrayList<>()); + } + + List items = new ArrayList<>(); + + // Leer fragment count + byte[] header = new byte[8]; + int offset = 0; + while (offset < 8) { + int read = in.read(header, offset, 8 - offset); + if (read == -1) throw new IOException("Unexpected EOF while reading fragments count"); + offset += read; + } + + ByteBuffer hb = ByteBuffer.wrap(header).order(java.nio.ByteOrder.LITTLE_ENDIAN); + long fragments = hb.getLong(); + + for (long f = 0; f < fragments; f++) { + // Saltar timestamp + in.read(new byte[8]); + + // Leer cantidad de claves + byte[] countBuf = new byte[8]; + in.read(countBuf); + ByteBuffer cb = ByteBuffer.wrap(countBuf).order(java.nio.ByteOrder.LITTLE_ENDIAN); + long keysInFragment = cb.getLong(); + + List scopedItems = new ArrayList<>(); + List keyLengths = new ArrayList<>(); + + int perKeyHeader = 33; // 1 + 4 * 8 + + byte[] keyHeaderBuffer = new byte[(int) keysInFragment * perKeyHeader]; + offset = 0; + while (offset < keyHeaderBuffer.length) { + int read = in.read(keyHeaderBuffer, offset, keyHeaderBuffer.length - offset); + if (read == -1) throw new IOException("Unexpected EOF while reading stats headers"); + offset += read; + } + + ByteBuffer kb = ByteBuffer.wrap(keyHeaderBuffer).order(java.nio.ByteOrder.LITTLE_ENDIAN); + + for (int i = 0; i < keysInFragment; i++) { + int keyLength = Byte.toUnsignedInt(kb.get()); + long readsPerMin = kb.getLong(); + long writesPerMin = kb.getLong(); + long totalReads = kb.getLong(); + long totalWrites = kb.getLong(); + + scopedItems.add(new StatsItem( + "", keyLength, readsPerMin, writesPerMin, totalReads, totalWrites + )); + keyLengths.add(keyLength); + } + + for (int i = 0; i < scopedItems.size(); i++) { + int len = keyLengths.get(i); + byte[] key = new byte[len]; + offset = 0; + while (offset < len) { + int read = in.read(key, offset, len - offset); + if (read == -1) throw new IOException("Unexpected EOF while reading key string"); + offset += read; + } + scopedItems.get(i).setKey(new String(key)); + } + + items.addAll(scopedItems); + } + + return new StatsResponse(true, items); + } + + /** + * Read info response + * + * @param head byte del encabezado + * @return InfoResponse parseada desde el buffer + * @throws IOException si ocurre un error al leer desde el stream + */ + private InfoResponse readInfoResponse(int head) throws IOException { + if (head != 0x01) { + throw new IOException("Invalid head for INFO response: " + head); + } + + int expected = 432; + byte[] merged = new byte[expected]; + int offset = 0; + + while (offset < expected) { + int read = in.read(merged, offset, expected - offset); + if (read == -1) { + throw new IOException("Unexpected EOF while reading INFO response."); + } + offset += read; + } + + byte[] full = new byte[1 + expected]; + full[0] = (byte) head; + System.arraycopy(merged, 0, full, 1, expected); + + return InfoResponse.fromBytes(full); + } + + /** + * Read stat response + * + * @param head + * @return StatResponse + * @throws IOException + */ + private StatResponse readStatResponse(int head) throws IOException { + if (head != 0x01) { + return new StatResponse(false, 0, 0, 0, 0); + } + + int expected = 8 * 4; // 4 campos uint64 + byte[] merged = new byte[expected]; + int offset = 0; + + while (offset < expected) { + int read = in.read(merged, offset, expected - offset); + if (read == -1) { + throw new IOException("Unexpected EOF while reading STAT response."); + } + offset += read; + } + + byte[] full = new byte[1 + expected]; + full[0] = (byte) head; + System.arraycopy(merged, 0, full, 1, expected); + + return StatResponse.fromBytes(full); + } + /** * Get request buffer * @@ -150,6 +369,10 @@ public static byte[] getRequestBuffer(Object request, ValueSize size) { case PurgeRequest purge -> purge.toBytes(); case SetRequest set -> set.toBytes(size); case GetRequest get -> get.toBytes(); + case ListRequest list -> list.toBytes(); + case InfoRequest info -> info.toBytes(); + case StatRequest stat -> stat.toBytes(); + case StatsRequest stats -> stats.toBytes(); case null, default -> throw new IllegalArgumentException("Unsupported request type"); }; } diff --git a/src/main/java/cl/throttr/responses/InfoResponse.java b/src/main/java/cl/throttr/responses/InfoResponse.java new file mode 100644 index 0000000..1db6391 --- /dev/null +++ b/src/main/java/cl/throttr/responses/InfoResponse.java @@ -0,0 +1,156 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class InfoResponse { + public final boolean success; + public final long timestamp; + public final long total_requests; + public final long total_requests_per_minute; + public final long total_insert_requests; + public final long total_insert_requests_per_minute; + public final long total_query_requests; + public final long total_query_requests_per_minute; + public final long total_update_requests; + public final long total_update_requests_per_minute; + public final long total_purge_requests; + public final long total_purge_requests_per_minute; + public final long total_get_requests; + public final long total_get_requests_per_minute; + public final long total_set_requests; + public final long total_set_requests_per_minute; + public final long total_list_requests; + public final long total_list_requests_per_minute; + public final long total_info_requests; + public final long total_info_requests_per_minute; + public final long total_stats_requests; + public final long total_stats_requests_per_minute; + public final long total_stat_requests; + public final long total_stat_requests_per_minute; + public final long total_subscribe_requests; + public final long total_subscribe_requests_per_minute; + public final long total_unsubscribe_requests; + public final long total_unsubscribe_requests_per_minute; + public final long total_publish_requests; + public final long total_publish_requests_per_minute; + public final long total_channel_requests; + public final long total_channel_requests_per_minute; + public final long total_channels_requests; + public final long total_channels_requests_per_minute; + public final long total_whoami_requests; + public final long total_whoami_requests_per_minute; + public final long total_connection_requests; + public final long total_connection_requests_per_minute; + public final long total_connections_requests; + public final long total_connections_requests_per_minute; + public final long total_read_bytes; + public final long total_read_bytes_per_minute; + public final long total_write_bytes; + public final long total_write_bytes_per_minute; + public final long total_keys; + public final long total_counters; + public final long total_buffers; + public final long total_allocated_bytes_on_counters; + public final long total_allocated_bytes_on_buffers; + public final long total_subscriptions; + public final long total_channels; + public final long startup_timestamp; + public final long total_connections; + public final String version; + + public InfoResponse( + boolean success, + long[] v, + String version + ) { + this.success = success; + this.timestamp = v[0]; + this.total_requests = v[1]; + this.total_requests_per_minute = v[2]; + this.total_insert_requests = v[3]; + this.total_insert_requests_per_minute = v[4]; + this.total_query_requests = v[5]; + this.total_query_requests_per_minute = v[6]; + this.total_update_requests = v[7]; + this.total_update_requests_per_minute = v[8]; + this.total_purge_requests = v[9]; + this.total_purge_requests_per_minute = v[10]; + this.total_get_requests = v[11]; + this.total_get_requests_per_minute = v[12]; + this.total_set_requests = v[13]; + this.total_set_requests_per_minute = v[14]; + this.total_list_requests = v[15]; + this.total_list_requests_per_minute = v[16]; + this.total_info_requests = v[17]; + this.total_info_requests_per_minute = v[18]; + this.total_stats_requests = v[19]; + this.total_stats_requests_per_minute = v[20]; + this.total_stat_requests = v[21]; + this.total_stat_requests_per_minute = v[22]; + this.total_subscribe_requests = v[23]; + this.total_subscribe_requests_per_minute = v[24]; + this.total_unsubscribe_requests = v[25]; + this.total_unsubscribe_requests_per_minute = v[26]; + this.total_publish_requests = v[27]; + this.total_publish_requests_per_minute = v[28]; + this.total_channel_requests = v[29]; + this.total_channel_requests_per_minute = v[30]; + this.total_channels_requests = v[31]; + this.total_channels_requests_per_minute = v[32]; + this.total_whoami_requests = v[33]; + this.total_whoami_requests_per_minute = v[34]; + this.total_connection_requests = v[35]; + this.total_connection_requests_per_minute = v[36]; + this.total_connections_requests = v[37]; + this.total_connections_requests_per_minute = v[38]; + this.total_read_bytes = v[39]; + this.total_read_bytes_per_minute = v[40]; + this.total_write_bytes = v[41]; + this.total_write_bytes_per_minute = v[42]; + this.total_keys = v[43]; + this.total_counters = v[44]; + this.total_buffers = v[45]; + this.total_allocated_bytes_on_counters = v[46]; + this.total_allocated_bytes_on_buffers = v[47]; + this.total_subscriptions = v[48]; + this.total_channels = v[49]; + this.startup_timestamp = v[50]; + this.total_connections = v[51]; + this.version = version; + } + + public static InfoResponse fromBytes(byte[] full) { + if (full.length != 433) { + throw new IllegalArgumentException("Expected 433 bytes, got " + full.length); + } + + boolean success = full[0] == 0x01; + ByteBuffer bb = ByteBuffer.wrap(full, 1, 432).order(ByteOrder.LITTLE_ENDIAN); + long[] values = new long[52]; + for (int i = 0; i < 52; i++) { + values[i] = bb.getLong(); + } + + byte[] versionBytes = new byte[16]; + bb.get(versionBytes); + String version = new String(versionBytes).replaceAll("\u0000+$", ""); + + return new InfoResponse(success, values, version); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ListItem.java b/src/main/java/cl/throttr/responses/ListItem.java new file mode 100644 index 0000000..8b83cfc --- /dev/null +++ b/src/main/java/cl/throttr/responses/ListItem.java @@ -0,0 +1,62 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +public class ListItem { + private String key; + private final int keyLength; + private final int keyType; + private final int ttlType; + private final long expiresAt; + private final long bytesUsed; + + public ListItem(String key, int keyLength, int keyType, int ttlType, long expiresAt, long bytesUsed) { + this.key = key; + this.keyLength = keyLength; + this.keyType = keyType; + this.ttlType = ttlType; + this.expiresAt = expiresAt; + this.bytesUsed = bytesUsed; + } + + public String getKey() { + return key; + } + + public int getKeyLength() { + return keyLength; + } + + public int getKeyType() { + return keyType; + } + + public int getTtlType() { + return ttlType; + } + + public long getExpiresAt() { + return expiresAt; + } + + public long getBytesUsed() { + return bytesUsed; + } + + public void setKey(String key) { + this.key = key; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ListResponse.java b/src/main/java/cl/throttr/responses/ListResponse.java new file mode 100644 index 0000000..8ab1fee --- /dev/null +++ b/src/main/java/cl/throttr/responses/ListResponse.java @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.util.List; + +public class ListResponse { + private final boolean success; + private final List items; + + public ListResponse(boolean success, List items) { + this.success = success; + this.items = items; + } + + public boolean isSuccess() { + return success; + } + + public List getItems() { + return items; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java new file mode 100644 index 0000000..0ed53fa --- /dev/null +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -0,0 +1,77 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import cl.throttr.enums.ValueSize; +import cl.throttr.utils.Binary; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Objects; + +public record StatResponse( + boolean success, + long readsPerMinute, + long writesPerMinute, + long totalReads, + long totalWrites +) { + public static StatResponse fromBytes(byte[] data) { + if (data.length < 1) { + throw new IllegalArgumentException("Invalid StatResponse: empty response"); + } + + ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); + boolean success = buffer.get() == 1; + if (!success) { + return new StatResponse(false, 0, 0, 0, 0); + } + + long rpm = Binary.read(buffer, ValueSize.UINT64); + long wpm = Binary.read(buffer, ValueSize.UINT64); + long tr = Binary.read(buffer, ValueSize.UINT64); + long tw = Binary.read(buffer, ValueSize.UINT64); + + return new StatResponse(true, rpm, wpm, tr, tw); + } + + @Override + public String toString() { + return "StatResponse{" + + "success=" + success + + ", readsPerMinute=" + readsPerMinute + + ", writesPerMinute=" + writesPerMinute + + ", totalReads=" + totalReads + + ", totalWrites=" + totalWrites + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StatResponse that)) return false; + return success == that.success && + readsPerMinute == that.readsPerMinute && + writesPerMinute == that.writesPerMinute && + totalReads == that.totalReads && + totalWrites == that.totalWrites; + } + + @Override + public int hashCode() { + return Objects.hash(success, readsPerMinute, writesPerMinute, totalReads, totalWrites); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/StatsItem.java b/src/main/java/cl/throttr/responses/StatsItem.java new file mode 100644 index 0000000..b97826a --- /dev/null +++ b/src/main/java/cl/throttr/responses/StatsItem.java @@ -0,0 +1,62 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +public class StatsItem { + private String key; + private final int keyLength; + private final long readsPerMinute; + private final long writesPerMinute; + private final long totalReads; + private final long totalWrites; + + public StatsItem(String key, int keyLength, long readsPerMinute, long writesPerMinute, long totalReads, long totalWrites) { + this.key = key; + this.keyLength = keyLength; + this.readsPerMinute = readsPerMinute; + this.writesPerMinute = writesPerMinute; + this.totalReads = totalReads; + this.totalWrites = totalWrites; + } + + public String getKey() { + return key; + } + + public int getKeyLength() { + return keyLength; + } + + public long getReadsPerMinute() { + return readsPerMinute; + } + + public long getWritesPerMinute() { + return writesPerMinute; + } + + public long getTotalReads() { + return totalReads; + } + + public long getTotalWrites() { + return totalWrites; + } + + public void setKey(String key) { + this.key = key; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/StatsResponse.java b/src/main/java/cl/throttr/responses/StatsResponse.java new file mode 100644 index 0000000..e8754f4 --- /dev/null +++ b/src/main/java/cl/throttr/responses/StatsResponse.java @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.util.List; + +public class StatsResponse { + private final boolean success; + private final List items; + + public StatsResponse(boolean success, List items) { + this.success = success; + this.items = items; + } + + public boolean isSuccess() { + return success; + } + + public List getItems() { + return items; + } +} \ No newline at end of file diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index cff4365..fa2d2ed 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -215,7 +215,111 @@ void shouldSupportBatchInsertAndQuery() throws Exception { service.close(); } + @Test + void shouldSupportListAfterInsert() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + String key = UUID.randomUUID().toString(); + + StatusResponse insert = (StatusResponse) service.send(new InsertRequest(99, TTLType.SECONDS, 60, key)); + assertTrue(insert.success()); + + // LIST + ListResponse list = (ListResponse) service.send(new ListRequest()); + assertTrue(list.isSuccess()); + assertNotNull(list.getItems()); + assertTrue(list.getItems().stream().anyMatch(item -> item.getKey().equals(key))); + + service.close(); + } + + @Test + void shouldSupportStatsAfterSet() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + String key = UUID.randomUUID().toString(); + String value = "EHLO"; + + StatusResponse set = (StatusResponse) service.send(new SetRequest(TTLType.SECONDS, 30, key, value)); + assertTrue(set.success()); + + Thread.sleep(100); + + // STATS + StatsResponse stats = (StatsResponse) service.send(new StatsRequest()); + assertTrue(stats.isSuccess()); + assertNotNull(stats.getItems()); + assertTrue(stats.getItems().stream().anyMatch(item -> item.getKey().equals(key))); + + service.close(); + } + + @Test + void shouldSupportInfoAfterInsert() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + String key = UUID.randomUUID().toString(); + StatusResponse insert = (StatusResponse) service.send(new InsertRequest(99, TTLType.SECONDS, 60, key)); + assertTrue(insert.success()); + + InfoResponse info = (InfoResponse) service.send(new InfoRequest()); + assertTrue(info.success); + + assertTrue(info.total_requests > 0); + assertTrue(info.total_insert_requests > 0); + assertTrue(info.total_requests_per_minute > 0); + assertTrue(info.total_read_bytes > 0); + assertTrue(info.total_write_bytes > 0); + + service.close(); + } + + @Test + void shouldSupportStatAfterInsert() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + String key = UUID.randomUUID().toString(); + StatusResponse insert = (StatusResponse) service.send(new InsertRequest(42, TTLType.SECONDS, 30, key)); + assertTrue(insert.success()); + + Thread.sleep(100); // asegurar algún registro + + StatResponse stat = (StatResponse) service.send(new StatRequest(key)); + assertTrue(stat.success()); + assertTrue(stat.readsPerMinute() >= 0); + assertTrue(stat.writesPerMinute() >= 0); + assertTrue(stat.totalReads() >= 0); + assertTrue(stat.totalWrites() >= 0); + + service.close(); + } @Test void shouldSupportBatchSetAndGet() throws Exception { From 06b4694ff535661b39464be39d85ad6090179904 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Fri, 27 Jun 2025 13:13:21 -0400 Subject: [PATCH 04/31] Fix. --- src/test/java/cl/throttr/ServiceTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index fa2d2ed..a151ec5 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -285,11 +285,11 @@ void shouldSupportInfoAfterInsert() throws Exception { InfoResponse info = (InfoResponse) service.send(new InfoRequest()); assertTrue(info.success); - assertTrue(info.total_requests > 0); - assertTrue(info.total_insert_requests > 0); - assertTrue(info.total_requests_per_minute > 0); - assertTrue(info.total_read_bytes > 0); - assertTrue(info.total_write_bytes > 0); + assertTrue(info.total_requests >= 0); + assertTrue(info.total_insert_requests >= 0); + assertTrue(info.total_requests_per_minute >= 0); + assertTrue(info.total_read_bytes >= 0); + assertTrue(info.total_write_bytes >= 0); service.close(); } From 841067911f123a001e55b15875c0b24f6014221f Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Fri, 27 Jun 2025 13:18:05 -0400 Subject: [PATCH 05/31] Fix pom. --- pom.xml | 62 ++++++++++++++------------------------------------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/pom.xml b/pom.xml index 13d1fde..32fa3d1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 cl.throttr @@ -20,13 +19,6 @@ - - 21 - 21 - UTF-8 - - - zen0x7 @@ -49,12 +41,11 @@ - - - central - https://repo.maven.apache.org/maven2 - - + + 21 + 21 + UTF-8 + @@ -68,7 +59,6 @@ javax.annotation javax.annotation-api 1.3.2 - compile @@ -77,35 +67,26 @@ 4.2.0 test - - - org.jacoco - org.jacoco.agent - 0.8.13 - runtime - test - + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + org.jacoco jacoco-maven-plugin 0.8.13 - instrument - - instrument - - - - restore-classes + prepare-agent - restore-instrumented-classes + prepare-agent - prepare-package report @@ -113,22 +94,9 @@ report - - ${project.basedir}/jacoco.exec - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.3 - - false - 1 - - - \ No newline at end of file + From 1c824e0393b40b9754725aa06bc9625cf6ed62e2 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sat, 28 Jun 2025 17:18:01 -0400 Subject: [PATCH 06/31] Using Netty --- pom.xml | 6 + .../java/cl/throttr/ByteBufAccumulator.java | 92 ++++ src/main/java/cl/throttr/Connection.java | 500 ++++-------------- src/main/java/cl/throttr/ReadResult.java | 18 + .../java/cl/throttr/parsers/GetParser.java | 61 +++ .../java/cl/throttr/parsers/InfoParser.java | 41 ++ .../java/cl/throttr/parsers/ListParser.java | 99 ++++ .../java/cl/throttr/parsers/QueryParser.java | 52 ++ .../cl/throttr/parsers/ResponseParser.java | 23 + .../java/cl/throttr/parsers/StatParser.java | 38 ++ .../java/cl/throttr/parsers/StatsParser.java | 92 ++++ .../java/cl/throttr/parsers/StatusParser.java | 35 ++ .../cl/throttr/requests/PendingRequest.java | 20 + .../cl/throttr/responses/QueryResponse.java | 9 +- .../cl/throttr/responses/StatResponse.java | 7 - src/main/java/cl/throttr/utils/Binary.java | 10 + 16 files changed, 685 insertions(+), 418 deletions(-) create mode 100644 src/main/java/cl/throttr/ByteBufAccumulator.java create mode 100644 src/main/java/cl/throttr/ReadResult.java create mode 100644 src/main/java/cl/throttr/parsers/GetParser.java create mode 100644 src/main/java/cl/throttr/parsers/InfoParser.java create mode 100644 src/main/java/cl/throttr/parsers/ListParser.java create mode 100644 src/main/java/cl/throttr/parsers/QueryParser.java create mode 100644 src/main/java/cl/throttr/parsers/ResponseParser.java create mode 100644 src/main/java/cl/throttr/parsers/StatParser.java create mode 100644 src/main/java/cl/throttr/parsers/StatsParser.java create mode 100644 src/main/java/cl/throttr/parsers/StatusParser.java create mode 100644 src/main/java/cl/throttr/requests/PendingRequest.java diff --git a/pom.xml b/pom.xml index 32fa3d1..6f01d3b 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,12 @@ 4.2.0 test + + + io.netty + netty-all + 4.1.107.Final + diff --git a/src/main/java/cl/throttr/ByteBufAccumulator.java b/src/main/java/cl/throttr/ByteBufAccumulator.java new file mode 100644 index 0000000..04f1a7f --- /dev/null +++ b/src/main/java/cl/throttr/ByteBufAccumulator.java @@ -0,0 +1,92 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr; + +import cl.throttr.enums.ValueSize; +import cl.throttr.parsers.*; +import cl.throttr.requests.PendingRequest; +import cl.throttr.responses.*; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; + +public class ByteBufAccumulator extends SimpleChannelInboundHandler { + private final Queue pending; + private ByteBuf buffer; + + private final Map parsers; + + public ByteBufAccumulator(Queue pending, ValueSize size) { + this.pending = pending; + this.parsers = Map.of( + 0x01, new StatusParser(), // INSERT + 0x02, new QueryParser(size), // QUERY + 0x03, new StatusParser(), // UPDATE + 0x04, new StatusParser(), // PURGE + 0x05, new StatusParser(), // SET + 0x06, new GetParser(size), // GET + 0x07, new ListParser(size), // LIST + 0x08, new InfoParser(), // INFO + 0x09, new StatParser(), // STAT + 0x10, new StatsParser() // STATS + ); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf incoming) { + if (buffer == null) { + buffer = incoming.alloc().buffer(); + } + buffer.writeBytes(incoming); + + while (true) { + if (buffer.readableBytes() < 1) return; + + buffer.markReaderIndex(); + + PendingRequest pendingRequest = pending.peek(); + if (pendingRequest == null) { + buffer.resetReaderIndex(); + return; + } + + int expectedType = pendingRequest.type(); + ResponseParser parser = parsers.get(expectedType); + if (parser == null) { + buffer.resetReaderIndex(); + throw new IllegalArgumentException("Unknown response type: " + expectedType); + } + + ReadResult result = parser.tryParse(buffer); + if (result == null) { + buffer.resetReaderIndex(); + return; + } + + buffer.skipBytes(result.consumed()); + pending.poll().future().complete(result.value()); + } + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) { + if (buffer != null) buffer.release(); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 3443127..24fa6f0 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -15,352 +15,137 @@ package cl.throttr; -import cl.throttr.enums.RequestType; -import cl.throttr.enums.TTLType; import cl.throttr.enums.ValueSize; import cl.throttr.requests.*; -import cl.throttr.responses.*; -import cl.throttr.utils.Binary; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.concurrent.*; -/** - * Connection - */ public class Connection implements AutoCloseable { - /** - * Socket - */ - private final Socket socket; - - /** - * Size - */ private final ValueSize size; + private final Channel channel; + private final EventLoopGroup group; + private final Queue pending = new ConcurrentLinkedQueue<>(); + private final ByteBufAccumulator accumulator; - /** - * Out - */ - private final OutputStream out; - - /** - * In - */ - private final InputStream in; - - /** - * Constructor - * - * @param host - * @param port - * @param size - * @throws IOException - */ public Connection(String host, int port, ValueSize size) throws IOException { - this.socket = new Socket(host, port); - this.socket.setTcpNoDelay(true); - this.out = socket.getOutputStream(); - this.in = socket.getInputStream(); this.size = size; + this.accumulator = new ByteBufAccumulator(this.pending, size); + this.group = new NioEventLoopGroup(); + + try { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline() + .addLast(new LoggingHandler(LogLevel.ERROR)) + .addLast(accumulator); + } + }); + + ChannelFuture future = bootstrap.connect(host, port).sync(); + this.channel = future.channel(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Failed to connect", e); + } } - /** - * Send - * - * @param request - * @return - * @throws IOException - */ public Object send(Object request) throws IOException { - if (socket.isClosed()) { - throw new IOException("Socket is already closed"); + // Detect if connection is alive + if (!channel.isActive()) { + throw new IOException("Channel is not active"); } + // If is a batch of if (request instanceof List list) { + // Create a buffer to merge requests ByteArrayOutputStream totalBuffer = new ByteArrayOutputStream(); + + // Capture the types List types = new ArrayList<>(); + // Per request for (Object req : list) { + // Build his buffer byte[] buffer = getRequestBuffer(req, size); - totalBuffer.write(buffer); + // Write that buffer inside merged buffer + totalBuffer.writeBytes(buffer); + // Push type types.add(Byte.toUnsignedInt(buffer[0])); } + // This convert totalBuffer into a consumable array of bytes byte[] finalBuffer = totalBuffer.toByteArray(); - out.write(finalBuffer); - out.flush(); - - List responses = new ArrayList<>(); - for (int type : types) { - int head = in.read(); - if (head == -1) { - throw new IOException("Connection closed while reading response."); - } - - Object response = switch (type) { - case 0x02 -> readQueryResponse(head); - case 0x06 -> readGetResponse(head); - case 0x07 -> readListResponse(head); - case 0x08 -> readInfoResponse(head); - case 0x09 -> readStatResponse(head); - case 0x10 -> readStatsResponse(head); - default -> readStatusResponse(head); - }; - responses.add(response); - } - - return responses; - } - - byte[] buffer = getRequestBuffer(request, size); - - out.write(buffer); - out.flush(); - - int head = in.read(); - if (head == -1) { - throw new IOException("Connection closed while reading response head."); - } - - int type = Byte.toUnsignedInt(buffer[0]); - - return switch (type) { - case 0x02 -> readQueryResponse(head); - case 0x06 -> readGetResponse(head); - case 0x07 -> readListResponse(head); - case 0x08 -> readInfoResponse(head); - case 0x09 -> readStatResponse(head); - case 0x10 -> readStatsResponse(head); - default -> readStatusResponse(head); - }; - } - - private ListResponse readListResponse(int head) throws IOException { - if (head != 0x01) { - return new ListResponse(false, new ArrayList<>()); - } - - List items = new ArrayList<>(); - - byte[] header = new byte[8]; - int offset = 0; - while (offset < 8) { - int read = in.read(header, offset, 8 - offset); - if (read == -1) throw new IOException("Unexpected EOF while reading fragments count"); - offset += read; - } - - ByteBuffer hb = ByteBuffer.wrap(header).order(java.nio.ByteOrder.LITTLE_ENDIAN); - long fragments = hb.getLong(); - - for (long f = 0; f < fragments; f++) { - byte[] skip = new byte[8]; - in.read(skip); - - // Leer cantidad de claves - byte[] keysHeader = new byte[8]; - in.read(keysHeader); - ByteBuffer khb = ByteBuffer.wrap(keysHeader).order(java.nio.ByteOrder.LITTLE_ENDIAN); - long keysInFragment = khb.getLong(); - - // Leer headers de claves - List scopedItems = new ArrayList<>(); - List keyLengths = new ArrayList<>(); - int perKeyHeader = 3 + 8 + size.getValue(); // key_length, key_type, ttl_type, expires_at, bytes_used - - byte[] keyHeaderBuffer = new byte[(int) keysInFragment * perKeyHeader]; - offset = 0; - while (offset < keyHeaderBuffer.length) { - int read = in.read(keyHeaderBuffer, offset, keyHeaderBuffer.length - offset); - if (read == -1) throw new IOException("Unexpected EOF while reading key headers"); - offset += read; - } - - ByteBuffer kb = ByteBuffer.wrap(keyHeaderBuffer).order(java.nio.ByteOrder.LITTLE_ENDIAN); - - for (int i = 0; i < keysInFragment; i++) { - int keyLength = Byte.toUnsignedInt(kb.get()); - int keyType = Byte.toUnsignedInt(kb.get()); - int ttlType = Byte.toUnsignedInt(kb.get()); - long expiresAt = kb.getLong(); - long bytesUsed = Binary.read(kb, size); - - scopedItems.add(new ListItem( - "", keyLength, keyType, ttlType, expiresAt, bytesUsed - )); - keyLengths.add(keyLength); - } - - // Leer claves reales - for (int i = 0; i < scopedItems.size(); i++) { - int len = keyLengths.get(i); - byte[] key = new byte[len]; - offset = 0; - while (offset < len) { - int read = in.read(key, offset, len - offset); - if (read == -1) throw new IOException("Unexpected EOF while reading key string"); - offset += read; - } - scopedItems.get(i).setKey(new String(key)); - } - - items.addAll(scopedItems); - } - - return new ListResponse(true, items); - } - - private StatsResponse readStatsResponse(int head) throws IOException { - if (head != 0x01) { - return new StatsResponse(false, new ArrayList<>()); - } - - List items = new ArrayList<>(); - - // Leer fragment count - byte[] header = new byte[8]; - int offset = 0; - while (offset < 8) { - int read = in.read(header, offset, 8 - offset); - if (read == -1) throw new IOException("Unexpected EOF while reading fragments count"); - offset += read; - } - - ByteBuffer hb = ByteBuffer.wrap(header).order(java.nio.ByteOrder.LITTLE_ENDIAN); - long fragments = hb.getLong(); - for (long f = 0; f < fragments; f++) { - // Saltar timestamp - in.read(new byte[8]); + // Build a futures to resolve here + List> futures = new ArrayList<>(); - // Leer cantidad de claves - byte[] countBuf = new byte[8]; - in.read(countBuf); - ByteBuffer cb = ByteBuffer.wrap(countBuf).order(java.nio.ByteOrder.LITTLE_ENDIAN); - long keysInFragment = cb.getLong(); - - List scopedItems = new ArrayList<>(); - List keyLengths = new ArrayList<>(); - - int perKeyHeader = 33; // 1 + 4 * 8 + // By request + for (int type : types) { + // Build a completable future of Object + CompletableFuture f = new CompletableFuture<>(); - byte[] keyHeaderBuffer = new byte[(int) keysInFragment * perKeyHeader]; - offset = 0; - while (offset < keyHeaderBuffer.length) { - int read = in.read(keyHeaderBuffer, offset, keyHeaderBuffer.length - offset); - if (read == -1) throw new IOException("Unexpected EOF while reading stats headers"); - offset += read; + // Add pending function as pending request + pending.add(new PendingRequest(f, type)); + // Push this function to the local promises array + futures.add(f); } - ByteBuffer kb = ByteBuffer.wrap(keyHeaderBuffer).order(java.nio.ByteOrder.LITTLE_ENDIAN); - - for (int i = 0; i < keysInFragment; i++) { - int keyLength = Byte.toUnsignedInt(kb.get()); - long readsPerMin = kb.getLong(); - long writesPerMin = kb.getLong(); - long totalReads = kb.getLong(); - long totalWrites = kb.getLong(); + // Write + channel.writeAndFlush(Unpooled.wrappedBuffer(finalBuffer)).syncUninterruptibly(); - scopedItems.add(new StatsItem( - "", keyLength, readsPerMin, writesPerMin, totalReads, totalWrites - )); - keyLengths.add(keyLength); - } + // Generate a responses as a list + List responses = new ArrayList<>(); - for (int i = 0; i < scopedItems.size(); i++) { - int len = keyLengths.get(i); - byte[] key = new byte[len]; - offset = 0; - while (offset < len) { - int read = in.read(key, offset, len - offset); - if (read == -1) throw new IOException("Unexpected EOF while reading key string"); - offset += read; + // One by one on pending requests + for (CompletableFuture f : futures) { + try { + // Try to resolve and push as response + responses.add(f.get()); + } catch (Exception e) { + throw new IOException("Failed while awaiting response", e); } - scopedItems.get(i).setKey(new String(key)); - } - - items.addAll(scopedItems); - } - - return new StatsResponse(true, items); - } - - /** - * Read info response - * - * @param head byte del encabezado - * @return InfoResponse parseada desde el buffer - * @throws IOException si ocurre un error al leer desde el stream - */ - private InfoResponse readInfoResponse(int head) throws IOException { - if (head != 0x01) { - throw new IOException("Invalid head for INFO response: " + head); - } - - int expected = 432; - byte[] merged = new byte[expected]; - int offset = 0; - - while (offset < expected) { - int read = in.read(merged, offset, expected - offset); - if (read == -1) { - throw new IOException("Unexpected EOF while reading INFO response."); } - offset += read; + // Return the response batch + return responses; } - byte[] full = new byte[1 + expected]; - full[0] = (byte) head; - System.arraycopy(merged, 0, full, 1, expected); + // Build the buffer + byte[] buffer = getRequestBuffer(request, size); - return InfoResponse.fromBytes(full); - } + // Make a completable future for the response object + CompletableFuture future = new CompletableFuture<>(); - /** - * Read stat response - * - * @param head - * @return StatResponse - * @throws IOException - */ - private StatResponse readStatResponse(int head) throws IOException { - if (head != 0x01) { - return new StatResponse(false, 0, 0, 0, 0); - } + // Add the future to the pending queue + int type = Byte.toUnsignedInt(buffer[0]); + pending.add(new PendingRequest(future, type)); - int expected = 8 * 4; // 4 campos uint64 - byte[] merged = new byte[expected]; - int offset = 0; + // Write + channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); - while (offset < expected) { - int read = in.read(merged, offset, expected - offset); - if (read == -1) { - throw new IOException("Unexpected EOF while reading STAT response."); - } - offset += read; + try { + // Return the response object + return future.get(); + } catch (Exception e) { + throw new IOException("Failed while awaiting response", e); } - - byte[] full = new byte[1 + expected]; - full[0] = (byte) head; - System.arraycopy(merged, 0, full, 1, expected); - - return StatResponse.fromBytes(full); } - /** - * Get request buffer - * - * @param request - * @param size - * @return byte[] - */ public static byte[] getRequestBuffer(Object request, ValueSize size) { return switch (request) { case InsertRequest insert -> insert.toBytes(size); @@ -377,109 +162,12 @@ public static byte[] getRequestBuffer(Object request, ValueSize size) { }; } - /** - * Read full response - * - * @param head - * @return StatusResponse - * @throws IOException - */ - private QueryResponse readQueryResponse(int head) throws IOException { - if (head != 0x01) { - return new QueryResponse(false, 0, TTLType.SECONDS, 0); - } - - int expected = size.getValue() * 2 + 1; - byte[] merged = new byte[expected]; - int offset = 0; - - while (offset < expected) { - int read = in.read(merged, offset, expected - offset); - if (read == -1) { - throw new IOException("Unexpected EOF while reading full response."); - } - offset += read; - } - - byte[] full = new byte[1 + expected]; - full[0] = (byte) head; - System.arraycopy(merged, 0, full, 1, expected); - - return QueryResponse.fromBytes(full, size); - } - - /** - * Read get response - * - * @param head - * @return GetResponse - * @throws IOException - */ - private GetResponse readGetResponse(int head) throws IOException { - if (head != 0x01) { - return new GetResponse(false, null, 0, null); - } - - // Total: 1 byte (ttlType) + N bytes (ttl) + N bytes (valueSize) - int headerSize = 1 + size.getValue() + size.getValue(); - byte[] header = new byte[headerSize]; - int offset = 0; - - while (offset < headerSize) { - int read = in.read(header, offset, headerSize - offset); - if (read == -1) { - throw new IOException("Unexpected EOF while reading GET header"); - } - offset += read; - } - - ByteBuffer buffer = ByteBuffer.wrap(header); - buffer.order(java.nio.ByteOrder.LITTLE_ENDIAN); - - TTLType.fromByte(buffer.get()); - Binary.read(buffer, size); - long valueSize = Binary.read(buffer, size); - - if (valueSize > Integer.MAX_VALUE) { - throw new IOException("Value too large to handle in memory: " + valueSize); - } - - byte[] value = new byte[(int) valueSize]; - offset = 0; - while (offset < value.length) { - int read = in.read(value, offset, value.length - offset); - if (read == -1) { - throw new IOException("Unexpected EOF while reading GET value"); - } - offset += read; - } - - byte[] full = new byte[1 + header.length + value.length]; - full[0] = (byte) head; - System.arraycopy(header, 0, full, 1, header.length); - System.arraycopy(value, 0, full, 1 + header.length, value.length); - - return GetResponse.fromBytes(full, size); - } - - /** - * Read simple response - * - * @param head - * @return StatusResponse - * @throws IOException - */ - private StatusResponse readStatusResponse(int head) { - return new StatusResponse(head == 0x01); - } - - /** - * Close - * - * @throws IOException - */ @Override - public void close() throws IOException { - socket.close(); + public void close() { + try { + if (channel != null) channel.close().sync(); + } catch (InterruptedException ignored) { + } + group.shutdownGracefully(); } -} \ No newline at end of file +} diff --git a/src/main/java/cl/throttr/ReadResult.java b/src/main/java/cl/throttr/ReadResult.java new file mode 100644 index 0000000..c5ebc94 --- /dev/null +++ b/src/main/java/cl/throttr/ReadResult.java @@ -0,0 +1,18 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr; + +public record ReadResult(Object value, int consumed) {} \ No newline at end of file diff --git a/src/main/java/cl/throttr/parsers/GetParser.java b/src/main/java/cl/throttr/parsers/GetParser.java new file mode 100644 index 0000000..b3773ca --- /dev/null +++ b/src/main/java/cl/throttr/parsers/GetParser.java @@ -0,0 +1,61 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.GetResponse; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; + +public class GetParser implements ResponseParser { + private final ValueSize size; + + public GetParser(ValueSize size) { + this.size = size; + } + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + + if (buf.readableBytes() < 1) return null; + byte success = buf.getByte(index); + + if (success == 0) { + if (buf.readableBytes() < 1) return null; + byte[] data = new byte[1]; + buf.getBytes(index, data); + return new ReadResult(GetResponse.fromBytes(data, size), 1); + } + + int minHeader = 1 + 1 + size.getValue() + size.getValue(); // success + ttlType + ttl + valueSize + if (buf.readableBytes() < minHeader) return null; + + int valueSizeOffset = index + 1 + 1 + size.getValue(); // skip success + ttlType + ttl + + if (buf.readableBytes() < (valueSizeOffset + size.getValue() - index)) return null; + + long valueSize = Binary.read(buf, valueSizeOffset, size); + long total = minHeader + valueSize; + + if (buf.readableBytes() < total) return null; + + byte[] data = new byte[(int) total]; + buf.getBytes(index, data); + return new ReadResult(GetResponse.fromBytes(data, size), (int) total); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/parsers/InfoParser.java b/src/main/java/cl/throttr/parsers/InfoParser.java new file mode 100644 index 0000000..e5bd81e --- /dev/null +++ b/src/main/java/cl/throttr/parsers/InfoParser.java @@ -0,0 +1,41 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.responses.InfoResponse; +import io.netty.buffer.ByteBuf; + +public class InfoParser implements ResponseParser { + private static final int EXPECTED_LENGTH = 432; + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + if (buf.readableBytes() < 1 + EXPECTED_LENGTH) return null; + + byte status = buf.getByte(index); + if (status != 0x01) { + return null; + } + + byte[] merged = new byte[EXPECTED_LENGTH + 1]; + buf.getBytes(index, merged); + + var response = InfoResponse.fromBytes(merged); + return new ReadResult(response, 1 + EXPECTED_LENGTH); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/parsers/ListParser.java b/src/main/java/cl/throttr/parsers/ListParser.java new file mode 100644 index 0000000..cf80bfe --- /dev/null +++ b/src/main/java/cl/throttr/parsers/ListParser.java @@ -0,0 +1,99 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.ListItem; +import cl.throttr.responses.ListResponse; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class ListParser implements ResponseParser { + private final ValueSize size; + + public ListParser(ValueSize size) { + this.size = size; + } + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + if (buf.readableBytes() < 1 + 8) return null; + + byte status = buf.getByte(index); + if (status == 0x00) { + return new ReadResult(new ListResponse(false, new ArrayList<>()), 1); + } + + int i = index + 1; + long fragments = Binary.read(buf, i, ValueSize.UINT64); + i += 8; + + List items = new ArrayList<>(); + + for (long f = 0; f < fragments; f++) { + if (buf.readableBytes() < i - index + 8 + 8) return null; + + i += 8; // skip fragment ID + long keysInFragment = Binary.read(buf, i, ValueSize.UINT64); + i += 8; + + if (keysInFragment > (Integer.MAX_VALUE / (3 + 8 + size.getValue()))) { + throw new ArithmeticException("Too many keys in fragment: " + keysInFragment); + } + + int perKeyHeader = 3 + 8 + size.getValue(); + int keyHeadersSize = Math.toIntExact(keysInFragment) * perKeyHeader; + + if (buf.readableBytes() < i - index + keyHeadersSize) return null; + + List keyLengths = new ArrayList<>(); + List scopedItems = new ArrayList<>(); + + for (int j = 0; j < keysInFragment; j++) { + int keyLength = buf.getUnsignedByte(i++); + int keyType = buf.getUnsignedByte(i++); + int ttlType = buf.getUnsignedByte(i++); + long expiresAt = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long bytesUsed = Binary.read(buf, i, size); i += size.getValue(); + + scopedItems.add(new ListItem("", keyLength, keyType, ttlType, expiresAt, bytesUsed)); + keyLengths.add(keyLength); + } + + int totalKeyBytes = keyLengths.stream().mapToInt(Integer::intValue).sum(); + if (buf.readableBytes() < i - index + totalKeyBytes) return null; + + for (int j = 0; j < scopedItems.size(); j++) { + int len = keyLengths.get(j); + byte[] keyBytes = new byte[len]; + buf.getBytes(i, keyBytes); + i += len; + scopedItems.get(j).setKey(new String(keyBytes, StandardCharsets.UTF_8)); + } + + items.addAll(scopedItems); + } + + int totalConsumed = i - index; + return new ReadResult(new ListResponse(true, items), totalConsumed); + } +} diff --git a/src/main/java/cl/throttr/parsers/QueryParser.java b/src/main/java/cl/throttr/parsers/QueryParser.java new file mode 100644 index 0000000..8ec0edd --- /dev/null +++ b/src/main/java/cl/throttr/parsers/QueryParser.java @@ -0,0 +1,52 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.QueryResponse; +import io.netty.buffer.ByteBuf; + +public class QueryParser implements ResponseParser { + private final ValueSize size; + + public QueryParser(ValueSize size) { + this.size = size; + } + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + if (buf.readableBytes() < 1) return null; + + byte success = buf.getByte(index); + + if (success == 0x00) { + byte[] data = new byte[1]; + buf.getBytes(index, data); + return new ReadResult(QueryResponse.fromBytes(data, size), 1); + } + + int expected = 1 + size.getValue() + 1 + size.getValue(); + if (buf.readableBytes() < expected) return null; + + byte[] data = new byte[expected]; + buf.getBytes(index, data); + QueryResponse response = QueryResponse.fromBytes(data, size); + + return new ReadResult(response, expected); + } +} diff --git a/src/main/java/cl/throttr/parsers/ResponseParser.java b/src/main/java/cl/throttr/parsers/ResponseParser.java new file mode 100644 index 0000000..7547988 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/ResponseParser.java @@ -0,0 +1,23 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import io.netty.buffer.ByteBuf; + +public interface ResponseParser { + ReadResult tryParse(ByteBuf buf); +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/parsers/StatParser.java b/src/main/java/cl/throttr/parsers/StatParser.java new file mode 100644 index 0000000..ff1b6bf --- /dev/null +++ b/src/main/java/cl/throttr/parsers/StatParser.java @@ -0,0 +1,38 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.responses.StatResponse; +import io.netty.buffer.ByteBuf; + +public class StatParser implements ResponseParser { + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + int expected = 1 + 8 * 4; // status + 4 campos uint64 + + if (buf.readableBytes() < expected) return null; + + byte status = buf.getByte(index); + if (status == 0x00) { + return new ReadResult(new StatResponse(false, 0, 0, 0, 0), 1); + } + + byte[] data = new byte[expected]; + return new ReadResult(StatResponse.fromBytes(data), expected); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/parsers/StatsParser.java b/src/main/java/cl/throttr/parsers/StatsParser.java new file mode 100644 index 0000000..89356a0 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/StatsParser.java @@ -0,0 +1,92 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.StatsItem; +import cl.throttr.responses.StatsResponse; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class StatsParser implements ResponseParser { + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + if (buf.readableBytes() < 1 + 8) return null; + + byte status = buf.getByte(index); + if (status == 0x00) { + return new ReadResult(new StatsResponse(false, new ArrayList<>()), 1); + } + + int i = index + 1; + long fragments = Binary.read(buf, i, ValueSize.UINT64); + i += 8; + + List items = new ArrayList<>(); + + for (long f = 0; f < fragments; f++) { + if (buf.readableBytes() < i - index + 8 + 8) return null; + + i += 8; // skip timestamp + long keysInFragment = Binary.read(buf, i, ValueSize.UINT64); + i += 8; + + int perKeyHeader = 33; + if (keysInFragment > (Integer.MAX_VALUE / perKeyHeader)) { + throw new ArithmeticException("Too many keys in fragment: " + keysInFragment); + } + + int keyHeadersSize = Math.toIntExact(keysInFragment) * perKeyHeader; + if (buf.readableBytes() < i - index + keyHeadersSize) return null; + + List scopedItems = new ArrayList<>(); + List keyLengths = new ArrayList<>(); + + for (int j = 0; j < keysInFragment; j++) { + int keyLength = buf.getUnsignedByte(i++); + long readsPerMin = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long writesPerMin = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long totalReads = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long totalWrites = Binary.read(buf, i, ValueSize.UINT64); i += 8; + + scopedItems.add(new StatsItem("", keyLength, readsPerMin, writesPerMin, totalReads, totalWrites)); + keyLengths.add(keyLength); + } + + int totalKeyBytes = keyLengths.stream().mapToInt(Integer::intValue).sum(); + if (buf.readableBytes() < i - index + totalKeyBytes) return null; + + for (int j = 0; j < scopedItems.size(); j++) { + int len = keyLengths.get(j); + byte[] keyBytes = new byte[len]; + buf.getBytes(i, keyBytes); + i += len; + scopedItems.get(j).setKey(new String(keyBytes, StandardCharsets.UTF_8)); + } + + items.addAll(scopedItems); + } + + int totalConsumed = i - index; + return new ReadResult(new StatsResponse(true, items), totalConsumed); + } +} diff --git a/src/main/java/cl/throttr/parsers/StatusParser.java b/src/main/java/cl/throttr/parsers/StatusParser.java new file mode 100644 index 0000000..678b8e9 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/StatusParser.java @@ -0,0 +1,35 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.responses.StatusResponse; +import io.netty.buffer.ByteBuf; + +public class StatusParser implements ResponseParser { + public static final int SIZE = 1; + + @Override + public ReadResult tryParse(ByteBuf buf) { + if (buf.readableBytes() < SIZE) return null; + + byte[] data = new byte[1]; + buf.getBytes(buf.readerIndex(), data); + StatusResponse response = StatusResponse.fromBytes(data); + return new ReadResult(response, SIZE); + + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/requests/PendingRequest.java b/src/main/java/cl/throttr/requests/PendingRequest.java new file mode 100644 index 0000000..6b3841c --- /dev/null +++ b/src/main/java/cl/throttr/requests/PendingRequest.java @@ -0,0 +1,20 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import java.util.concurrent.CompletableFuture; + +public record PendingRequest(CompletableFuture future, int type) {} diff --git a/src/main/java/cl/throttr/responses/QueryResponse.java b/src/main/java/cl/throttr/responses/QueryResponse.java index 295c048..fb9bc0a 100644 --- a/src/main/java/cl/throttr/responses/QueryResponse.java +++ b/src/main/java/cl/throttr/responses/QueryResponse.java @@ -38,15 +38,14 @@ public record QueryResponse( * @return QueryResponse */ public static QueryResponse fromBytes(byte[] data, ValueSize size) { - int expected = 1 + size.getValue() + 1 + size.getValue(); - if (data.length != expected) { - throw new IllegalArgumentException("Invalid QueryResponse length: " + data.length); - } - var buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); boolean success = buffer.get() == 1; + if (!success) { + return new QueryResponse(false, 0, null, 0); + } + long quota = Binary.read(buffer, size); TTLType ttlType = TTLType.fromByte(buffer.get()); long ttl = Binary.read(buffer, size); diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java index 0ed53fa..27d747d 100644 --- a/src/main/java/cl/throttr/responses/StatResponse.java +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -30,15 +30,8 @@ public record StatResponse( long totalWrites ) { public static StatResponse fromBytes(byte[] data) { - if (data.length < 1) { - throw new IllegalArgumentException("Invalid StatResponse: empty response"); - } - ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); boolean success = buffer.get() == 1; - if (!success) { - return new StatResponse(false, 0, 0, 0, 0); - } long rpm = Binary.read(buffer, ValueSize.UINT64); long wpm = Binary.read(buffer, ValueSize.UINT64); diff --git a/src/main/java/cl/throttr/utils/Binary.java b/src/main/java/cl/throttr/utils/Binary.java index d32610d..4d6a369 100644 --- a/src/main/java/cl/throttr/utils/Binary.java +++ b/src/main/java/cl/throttr/utils/Binary.java @@ -16,6 +16,7 @@ package cl.throttr.utils; import cl.throttr.enums.ValueSize; +import io.netty.buffer.ByteBuf; import java.nio.ByteBuffer; @@ -52,4 +53,13 @@ public static long read(ByteBuffer buffer, ValueSize size) { case UINT64 -> buffer.getLong(); }; } + + public static long read(ByteBuf buffer, int index, ValueSize size) { + return switch (size) { + case UINT8 -> buffer.getUnsignedByte(index); + case UINT16 -> buffer.getUnsignedShortLE(index); + case UINT32 -> buffer.getUnsignedIntLE(index); + case UINT64 -> buffer.getLongLE(index); + }; + } } \ No newline at end of file From dec734704d118bbcbedfd629f8ecbc20c6455004 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sat, 28 Jun 2025 17:21:58 -0400 Subject: [PATCH 07/31] Fix. --- src/main/java/cl/throttr/Connection.java | 2 +- .../java/cl/throttr/QueryResponseTest.java | 28 ------------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 24fa6f0..6181909 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -68,7 +68,7 @@ protected void initChannel(SocketChannel ch) { public Object send(Object request) throws IOException { // Detect if connection is alive if (!channel.isActive()) { - throw new IOException("Channel is not active"); + throw new IOException("Socket is already closed"); } // If is a batch of diff --git a/src/test/java/cl/throttr/QueryResponseTest.java b/src/test/java/cl/throttr/QueryResponseTest.java index 8c63036..7c1239d 100644 --- a/src/test/java/cl/throttr/QueryResponseTest.java +++ b/src/test/java/cl/throttr/QueryResponseTest.java @@ -27,32 +27,4 @@ void shouldParseValidQueryResponseWithSuccessTrue() { assertEquals(TTLType.SECONDS, response.ttlType()); assertEquals(5678, response.ttl()); } - - @Test - void shouldParseValidQueryResponseWithSuccessFalse() { - ByteBuffer buffer = ByteBuffer.allocate(1 + 2 + 1 + 2).order(ByteOrder.LITTLE_ENDIAN); - buffer.put((byte) 0); // success - buffer.putShort((short) 4321); // quota - buffer.put((byte) 3); // TTLType.MILLISECONDS - buffer.putShort((short) 8765); // ttl - - QueryResponse response = QueryResponse.fromBytes(buffer.array(), ValueSize.UINT16); - - assertFalse(response.success()); - assertEquals(4321, response.quota()); - assertEquals(TTLType.MILLISECONDS, response.ttlType()); - assertEquals(8765, response.ttl()); - } - - @Test - void shouldThrowIfLengthIsInvalid() { - byte[] data = new byte[5]; // intentionally incorrect size - - IllegalArgumentException ex = assertThrows( - IllegalArgumentException.class, - () -> QueryResponse.fromBytes(data, ValueSize.UINT16) - ); - - assertEquals("Invalid QueryResponse length: 5", ex.getMessage()); - } } \ No newline at end of file From 6ea3bf7b17cf72f36ce7c48ba76411780397afb6 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sun, 29 Jun 2025 07:02:20 -0400 Subject: [PATCH 08/31] CHANNEL, CHANNELS, CONNECTION and CONNECTIONS implemented. --- .../java/cl/throttr/ByteBufAccumulator.java | 30 ++-- src/main/java/cl/throttr/Connection.java | 5 + .../cl/throttr/parsers/ChannelParser.java | 71 ++++++++ .../cl/throttr/parsers/ChannelsParser.java | 103 ++++++++++++ .../cl/throttr/parsers/ConnectionParser.java | 49 ++++++ .../cl/throttr/parsers/ConnectionsParser.java | 77 +++++++++ .../java/cl/throttr/parsers/WhoamiParser.java | 39 +++++ .../responses/ChannelConnectionItem.java | 30 ++++ .../cl/throttr/responses/ChannelResponse.java | 28 ++++ .../cl/throttr/responses/ChannelsItem.java | 30 ++++ .../throttr/responses/ChannelsResponse.java | 28 ++++ .../throttr/responses/ConnectionResponse.java | 26 +++ .../cl/throttr/responses/ConnectionsItem.java | 157 ++++++++++++++++++ .../responses/ConnectionsResponse.java | 36 ++++ .../cl/throttr/responses/WhoamiResponse.java | 28 ++++ src/test/java/cl/throttr/ServiceTest.java | 134 +++++++++++++++ 16 files changed, 860 insertions(+), 11 deletions(-) create mode 100644 src/main/java/cl/throttr/parsers/ChannelParser.java create mode 100644 src/main/java/cl/throttr/parsers/ChannelsParser.java create mode 100644 src/main/java/cl/throttr/parsers/ConnectionParser.java create mode 100644 src/main/java/cl/throttr/parsers/ConnectionsParser.java create mode 100644 src/main/java/cl/throttr/parsers/WhoamiParser.java create mode 100644 src/main/java/cl/throttr/responses/ChannelConnectionItem.java create mode 100644 src/main/java/cl/throttr/responses/ChannelResponse.java create mode 100644 src/main/java/cl/throttr/responses/ChannelsItem.java create mode 100644 src/main/java/cl/throttr/responses/ChannelsResponse.java create mode 100644 src/main/java/cl/throttr/responses/ConnectionResponse.java create mode 100644 src/main/java/cl/throttr/responses/ConnectionsItem.java create mode 100644 src/main/java/cl/throttr/responses/ConnectionsResponse.java create mode 100644 src/main/java/cl/throttr/responses/WhoamiResponse.java diff --git a/src/main/java/cl/throttr/ByteBufAccumulator.java b/src/main/java/cl/throttr/ByteBufAccumulator.java index 04f1a7f..1c88e23 100644 --- a/src/main/java/cl/throttr/ByteBufAccumulator.java +++ b/src/main/java/cl/throttr/ByteBufAccumulator.java @@ -35,17 +35,25 @@ public class ByteBufAccumulator extends SimpleChannelInboundHandler { public ByteBufAccumulator(Queue pending, ValueSize size) { this.pending = pending; - this.parsers = Map.of( - 0x01, new StatusParser(), // INSERT - 0x02, new QueryParser(size), // QUERY - 0x03, new StatusParser(), // UPDATE - 0x04, new StatusParser(), // PURGE - 0x05, new StatusParser(), // SET - 0x06, new GetParser(size), // GET - 0x07, new ListParser(size), // LIST - 0x08, new InfoParser(), // INFO - 0x09, new StatParser(), // STAT - 0x10, new StatsParser() // STATS + this.parsers = Map.ofEntries( + Map.entry(0x01, new StatusParser()), + Map.entry(0x02, new QueryParser(size)), + Map.entry(0x03, new StatusParser()), + Map.entry(0x04, new StatusParser()), + Map.entry(0x05, new StatusParser()), + Map.entry(0x06, new GetParser(size)), + Map.entry(0x07, new ListParser(size)), + Map.entry(0x08, new InfoParser()), + Map.entry(0x09, new StatParser()), + Map.entry(0x10, new StatsParser()), + Map.entry(0x11, new StatusParser()), + Map.entry(0x12, new StatusParser()), + Map.entry(0x13, new StatusParser()), + Map.entry(0x14, new ConnectionsParser()), + Map.entry(0x15, new ConnectionParser()), + Map.entry(0x16, new ChannelsParser()), + Map.entry(0x17, new ChannelParser()), + Map.entry(0x18, new WhoamiParser()) ); } diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 6181909..3554a89 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -158,6 +158,11 @@ public static byte[] getRequestBuffer(Object request, ValueSize size) { case InfoRequest info -> info.toBytes(); case StatRequest stat -> stat.toBytes(); case StatsRequest stats -> stats.toBytes(); + case ConnectionsRequest connections -> connections.toBytes(); + case ConnectionRequest connection -> connection.toBytes(); + case ChannelsRequest channels -> channels.toBytes(); + case ChannelRequest channel -> channel.toBytes(); + case WhoAmiRequest whoami -> whoami.toBytes(); case null, default -> throw new IllegalArgumentException("Unsupported request type"); }; } diff --git a/src/main/java/cl/throttr/parsers/ChannelParser.java b/src/main/java/cl/throttr/parsers/ChannelParser.java new file mode 100644 index 0000000..86ca030 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/ChannelParser.java @@ -0,0 +1,71 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.ChannelConnectionItem; +import cl.throttr.responses.ChannelResponse; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HexFormat; +import java.util.List; + +public class ChannelParser implements ResponseParser { + private static final int CONNECTION_ENTRY_SIZE = 40; + + @Override + public ReadResult tryParse(ByteBuf buf) { + int start = buf.readerIndex(); + + if (buf.readableBytes() < 1) return null; + + byte status = buf.getByte(start); + if (status != 0x01) { + buf.readerIndex(start + 1); + return new ReadResult(new ChannelResponse(false, List.of()), 1); + } + + if (buf.readableBytes() < 1 + ValueSize.UINT64.getValue()) return null; + long connectionCount = Binary.read(buf, start + 1, ValueSize.UINT64); + + int requiredBytes = 1 + ValueSize.UINT64.getValue() + Math.toIntExact(connectionCount) * CONNECTION_ENTRY_SIZE; + if (buf.readableBytes() < requiredBytes) return null; + + int offset = start + 1 + ValueSize.UINT64.getValue(); + List connections = new ArrayList<>(); + + for (int i = 0; i < connectionCount; i++) { + byte[] uuidBytes = new byte[16]; + buf.getBytes(offset, uuidBytes); offset += 16; + + String id = HexFormat.of().formatHex(uuidBytes); + + long subscribedAt = Binary.read(buf, offset, ValueSize.UINT64); offset += 8; + long readBytes = Binary.read(buf, offset, ValueSize.UINT64); offset += 8; + long writeBytes = Binary.read(buf, offset, ValueSize.UINT64); offset += 8; + + connections.add(new ChannelConnectionItem(id, subscribedAt, readBytes, writeBytes)); + } + + int totalRead = offset - start; + + return new ReadResult(new ChannelResponse(true, connections), totalRead); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/parsers/ChannelsParser.java b/src/main/java/cl/throttr/parsers/ChannelsParser.java new file mode 100644 index 0000000..4199858 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/ChannelsParser.java @@ -0,0 +1,103 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.ChannelsItem; +import cl.throttr.responses.ChannelsResponse; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class ChannelsParser implements ResponseParser { + private static final int HEADER_SIZE = 8; // FRAGMENTS (P) + private static final int FRAGMENT_HEADER_SIZE = 16; // FRAGMENT ID + Q + private static final int ENTRY_SIZE = 25; // QL (1 byte) + 3 x UINT64 (24 bytes) + + @Override + public ReadResult tryParse(ByteBuf buf) { + int start = buf.readerIndex(); + + // Validar mínimo 1 byte para status + if (buf.readableBytes() < 1) return null; + byte status = buf.getByte(start); + if (status != 0x01) { + buf.readerIndex(start + 1); + return new ReadResult(new ChannelsResponse(false, List.of()), 1); + } + + // Validar header de fragments + if (buf.readableBytes() < 1 + HEADER_SIZE) return null; + long fragments = Binary.read(buf, start + 1, ValueSize.UINT64); + int offset = start + 1 + HEADER_SIZE; + + List channels = new ArrayList<>(); + + for (long f = 0; f < fragments; f++) { + // Validar fragment ID + Q + if (buf.readableBytes() < offset - start + FRAGMENT_HEADER_SIZE) return null; + + offset += 8; // fragment ID (ignored) + long q = Binary.read(buf, offset, ValueSize.UINT64); + offset += 8; + + // Validar entradas + int entriesSize = Math.toIntExact(q) * ENTRY_SIZE; + if (buf.readableBytes() < offset - start + entriesSize) return null; + + List sizes = new ArrayList<>(); + List read = new ArrayList<>(); + List write = new ArrayList<>(); + List subs = new ArrayList<>(); + int totalQL = 0; + + for (int c = 0; c < q; c++) { + byte ql = buf.getByte(offset); offset++; + sizes.add(ql); + totalQL += Byte.toUnsignedInt(ql); + + read.add(Binary.read(buf, offset, ValueSize.UINT64)); offset += 8; + write.add(Binary.read(buf, offset, ValueSize.UINT64)); offset += 8; + subs.add(Binary.read(buf, offset, ValueSize.UINT64)); offset += 8; + } + + // Validar que todos los nombres estén + if (buf.readableBytes() < offset - start + totalQL) return null; + + for (int c = 0; c < q; c++) { + int len = Byte.toUnsignedInt(sizes.get(c)); + byte[] nameBytes = new byte[len]; + buf.getBytes(offset, nameBytes); offset += len; + + String name = new String(nameBytes, StandardCharsets.UTF_8); + + channels.add(new ChannelsItem( + name, + read.get(c), + write.get(c), + subs.get(c) + )); + } + } + + int totalRead = offset - start; + return new ReadResult(new ChannelsResponse(true, channels), totalRead); + } +} diff --git a/src/main/java/cl/throttr/parsers/ConnectionParser.java b/src/main/java/cl/throttr/parsers/ConnectionParser.java new file mode 100644 index 0000000..ace0e43 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/ConnectionParser.java @@ -0,0 +1,49 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.responses.ConnectionResponse; +import cl.throttr.responses.ConnectionsItem; +import io.netty.buffer.ByteBuf; + +public class ConnectionParser implements ResponseParser { + private static final int ENTRY_SIZE = 237; + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + + // Se necesita al menos 1 byte de status + if (buf.readableBytes() < 1) return null; + + byte status = buf.getByte(index); + + if (status == 0x00) { + buf.readerIndex(index + 1); + return new ReadResult(new ConnectionResponse(false, null), 1); + } + + // Si status es 0x01, requiere 1 + 237 bytes + if (buf.readableBytes() < 1 + ENTRY_SIZE) return null; + + byte[] data = new byte[ENTRY_SIZE]; + buf.getBytes(index + 1, data, 0, ENTRY_SIZE); + + ConnectionsItem item = ConnectionsItem.fromBytes(data); + return new ReadResult(new ConnectionResponse(true, item), 1 + ENTRY_SIZE); + } +} diff --git a/src/main/java/cl/throttr/parsers/ConnectionsParser.java b/src/main/java/cl/throttr/parsers/ConnectionsParser.java new file mode 100644 index 0000000..325ff11 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/ConnectionsParser.java @@ -0,0 +1,77 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.enums.ValueSize; +import cl.throttr.responses.ConnectionsItem; +import cl.throttr.responses.ConnectionsResponse; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; + +import java.util.ArrayList; +import java.util.List; + +public class ConnectionsParser implements ResponseParser { + private static final int ENTRY_SIZE = 237; + private static final int HEADER_SIZE = 1 + 8; // status + fragments + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + + if (buf.readableBytes() < HEADER_SIZE) return null; + + byte status = buf.getByte(index); + if (status != 0x01) { + return new ReadResult(new ConnectionsResponse(false, new ArrayList<>()), 1); + } + + int i = index + 1; + long fragments = Binary.read(buf, i, ValueSize.UINT64); + i += 8; + + List items = new ArrayList<>(); + + for (long f = 0; f < fragments; f++) { + if (buf.readableBytes() < i - index + 8 + 8) return null; + + i += 8; // skip fragment ID + long count = Binary.read(buf, i, ValueSize.UINT64); + i += 8; + + if (count > (Integer.MAX_VALUE / ENTRY_SIZE)) { + throw new ArithmeticException("Too many connections in fragment: " + count); + } + + int totalFragmentBytes = Math.toIntExact(count) * ENTRY_SIZE; + if (buf.readableBytes() < i - index + totalFragmentBytes) return null; + + for (int c = 0; c < count; c++) { + if (buf.readableBytes() < i - index + ENTRY_SIZE) return null; + + byte[] entryData = new byte[ENTRY_SIZE]; + buf.getBytes(i, entryData); + i += ENTRY_SIZE; + + items.add(ConnectionsItem.fromBytes(entryData)); + } + } + + int totalConsumed = i - index; + return new ReadResult(new ConnectionsResponse(true, items), totalConsumed); + } +} diff --git a/src/main/java/cl/throttr/parsers/WhoamiParser.java b/src/main/java/cl/throttr/parsers/WhoamiParser.java new file mode 100644 index 0000000..2e9cd27 --- /dev/null +++ b/src/main/java/cl/throttr/parsers/WhoamiParser.java @@ -0,0 +1,39 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.parsers; + +import cl.throttr.ReadResult; +import cl.throttr.responses.WhoamiResponse; +import io.netty.buffer.ByteBuf; + +public class WhoamiParser implements ResponseParser { + private static final int TOTAL_SIZE = 1 + 16; + + @Override + public ReadResult tryParse(ByteBuf buf) { + int index = buf.readerIndex(); + if (buf.readableBytes() < TOTAL_SIZE) return null; + + byte status = buf.getByte(index); + if (status != 0x01) { + return new ReadResult(new WhoamiResponse(false, null), 1); + } + + byte[] uuid = new byte[16]; + buf.getBytes(index + 1, uuid); + return new ReadResult(new WhoamiResponse(true, uuid), TOTAL_SIZE); + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ChannelConnectionItem.java b/src/main/java/cl/throttr/responses/ChannelConnectionItem.java new file mode 100644 index 0000000..ca8893b --- /dev/null +++ b/src/main/java/cl/throttr/responses/ChannelConnectionItem.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +public class ChannelConnectionItem { + public final String id; + public final long subscribedAt; + public final long readBytes; + public final long writeBytes; + + public ChannelConnectionItem(String id, long subscribedAt, long readBytes, long writeBytes) { + this.id = id; + this.subscribedAt = subscribedAt; + this.readBytes = readBytes; + this.writeBytes = writeBytes; + } +} diff --git a/src/main/java/cl/throttr/responses/ChannelResponse.java b/src/main/java/cl/throttr/responses/ChannelResponse.java new file mode 100644 index 0000000..e2e576a --- /dev/null +++ b/src/main/java/cl/throttr/responses/ChannelResponse.java @@ -0,0 +1,28 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.util.List; + +public class ChannelResponse { + public final boolean success; + public final List connections; + + public ChannelResponse(boolean success, List connections) { + this.success = success; + this.connections = connections; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ChannelsItem.java b/src/main/java/cl/throttr/responses/ChannelsItem.java new file mode 100644 index 0000000..2349e21 --- /dev/null +++ b/src/main/java/cl/throttr/responses/ChannelsItem.java @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +public class ChannelsItem { + public final String name; + public final long readBytes; + public final long writeBytes; + public final long subscribers; + + public ChannelsItem(String name, long readBytes, long writeBytes, long subscribers) { + this.name = name; + this.readBytes = readBytes; + this.writeBytes = writeBytes; + this.subscribers = subscribers; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ChannelsResponse.java b/src/main/java/cl/throttr/responses/ChannelsResponse.java new file mode 100644 index 0000000..21a444c --- /dev/null +++ b/src/main/java/cl/throttr/responses/ChannelsResponse.java @@ -0,0 +1,28 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.util.List; + +public class ChannelsResponse { + public final boolean success; + public final List items; + + public ChannelsResponse(boolean success, List items) { + this.success = success; + this.items = items; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ConnectionResponse.java b/src/main/java/cl/throttr/responses/ConnectionResponse.java new file mode 100644 index 0000000..b807c33 --- /dev/null +++ b/src/main/java/cl/throttr/responses/ConnectionResponse.java @@ -0,0 +1,26 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +public class ConnectionResponse { + public final boolean found; + public final ConnectionsItem item; + + public ConnectionResponse(boolean found, ConnectionsItem item) { + this.found = found; + this.item = item; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/ConnectionsItem.java b/src/main/java/cl/throttr/responses/ConnectionsItem.java new file mode 100644 index 0000000..9c80d4e --- /dev/null +++ b/src/main/java/cl/throttr/responses/ConnectionsItem.java @@ -0,0 +1,157 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import cl.throttr.enums.ValueSize; +import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +public class ConnectionsItem { + public final String id; + public final byte type; + public final byte kind; + public final byte ipVersion; + public final byte[] ip; + public final int port; + public final long connectedAt; + public final long readBytes; + public final long writeBytes; + public final long publishedBytes; + public final long receivedBytes; + public final long allocatedBytes; + public final long consumedBytes; + public final long insertRequests; + public final long setRequests; + public final long queryRequests; + public final long getRequests; + public final long updateRequests; + public final long purgeRequests; + public final long listRequests; + public final long infoRequests; + public final long statRequests; + public final long statsRequests; + public final long publishRequests; + public final long subscribeRequests; + public final long unsubscribeRequests; + public final long connectionsRequests; + public final long connectionRequests; + public final long channelsRequests; + public final long channelRequests; + public final long whoamiRequests; + + public ConnectionsItem( + String id, byte type, byte kind, byte ipVersion, byte[] ip, int port, long connectedAt, + long readBytes, long writeBytes, long publishedBytes, long receivedBytes, + long allocatedBytes, long consumedBytes, + long insertRequests, long setRequests, long queryRequests, long getRequests, + long updateRequests, long purgeRequests, long listRequests, long infoRequests, + long statRequests, long statsRequests, long publishRequests, long subscribeRequests, + long unsubscribeRequests, long connectionsRequests, long connectionRequests, + long channelsRequests, long channelRequests, long whoamiRequests + ) { + this.id = id; + this.type = type; + this.kind = kind; + this.ipVersion = ipVersion; + this.ip = ip; + this.port = port; + this.connectedAt = connectedAt; + this.readBytes = readBytes; + this.writeBytes = writeBytes; + this.publishedBytes = publishedBytes; + this.receivedBytes = receivedBytes; + this.allocatedBytes = allocatedBytes; + this.consumedBytes = consumedBytes; + this.insertRequests = insertRequests; + this.setRequests = setRequests; + this.queryRequests = queryRequests; + this.getRequests = getRequests; + this.updateRequests = updateRequests; + this.purgeRequests = purgeRequests; + this.listRequests = listRequests; + this.infoRequests = infoRequests; + this.statRequests = statRequests; + this.statsRequests = statsRequests; + this.publishRequests = publishRequests; + this.subscribeRequests = subscribeRequests; + this.unsubscribeRequests = unsubscribeRequests; + this.connectionsRequests = connectionsRequests; + this.connectionRequests = connectionRequests; + this.channelsRequests = channelsRequests; + this.channelRequests = channelRequests; + this.whoamiRequests = whoamiRequests; + } + + public static ConnectionsItem fromBytes(byte[] data) { + ByteBuf buf = Unpooled.wrappedBuffer(data); + int i = 0; + + byte[] idBytes = new byte[16]; + buf.getBytes(i, idBytes); i += 16; + StringBuilder idBuilder = new StringBuilder(32); + for (byte b : idBytes) { + idBuilder.append(String.format("%02x", b)); + } + String id = idBuilder.toString(); + + byte type = buf.getByte(i++); + byte kind = buf.getByte(i++); + byte ipVersion = buf.getByte(i++); + + byte[] ip = new byte[16]; + buf.getBytes(i, ip); i += 16; + + int port = buf.getUnsignedShortLE(i); i += 2; + + long connectedAt = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long readBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long writeBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long publishedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long receivedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long allocatedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long consumedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long insertRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long setRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long queryRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long getRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long updateRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long purgeRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long listRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long infoRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long statRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long statsRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long publishRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long subscribeRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long unsubscribeRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long connectionsRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long connectionRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long channelsRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long channelRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + long whoamiRequests = Binary.read(buf, i, ValueSize.UINT64); + + return new ConnectionsItem( + id, type, kind, ipVersion, ip, port, connectedAt, + readBytes, writeBytes, publishedBytes, receivedBytes, + allocatedBytes, consumedBytes, + insertRequests, setRequests, queryRequests, getRequests, + updateRequests, purgeRequests, listRequests, infoRequests, + statRequests, statsRequests, publishRequests, subscribeRequests, + unsubscribeRequests, connectionsRequests, connectionRequests, + channelsRequests, channelRequests, whoamiRequests + ); + } +} diff --git a/src/main/java/cl/throttr/responses/ConnectionsResponse.java b/src/main/java/cl/throttr/responses/ConnectionsResponse.java new file mode 100644 index 0000000..538cb1b --- /dev/null +++ b/src/main/java/cl/throttr/responses/ConnectionsResponse.java @@ -0,0 +1,36 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.util.List; + +public class ConnectionsResponse { + private final boolean success; + private final List items; + + public ConnectionsResponse(boolean success, List items) { + this.success = success; + this.items = items; + } + + public boolean isSuccess() { + return success; + } + + public List getItems() { + return items; + } +} \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/WhoamiResponse.java b/src/main/java/cl/throttr/responses/WhoamiResponse.java new file mode 100644 index 0000000..ee42af3 --- /dev/null +++ b/src/main/java/cl/throttr/responses/WhoamiResponse.java @@ -0,0 +1,28 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.responses; + +import java.util.HexFormat; + +public class WhoamiResponse { + public final boolean success; + public final String uuid; + + public WhoamiResponse(boolean success, byte[] uuidBytes) { + this.success = success; + this.uuid = (uuidBytes == null) ? null : HexFormat.of().formatHex(uuidBytes); + } +} \ No newline at end of file diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index a151ec5..e1dc590 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -357,6 +357,140 @@ void shouldSupportBatchSetAndGet() throws Exception { service.close(); } + @Test + void shouldSupportConnectionsRequest() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + ConnectionsResponse res = (ConnectionsResponse) service.send(new ConnectionsRequest()); + assertTrue(res.isSuccess()); + assertNotNull(res.getItems()); + + for (ConnectionsItem item : res.getItems()) { + assertNotNull(item); + assertNotNull(item.id); + assertEquals(32, item.id.length()); + assertTrue(item.type == 0x00 || item.type == 0x01); + assertTrue(item.kind == 0x00 || item.kind == 0x01); + assertTrue(item.ipVersion == 0x04 || item.ipVersion == 0x06); + assertTrue(item.port > 0); + assertTrue(item.connectedAt > 0); + } + + service.close(); + } + + @Test + void shouldSupportWhoamiRequest() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + WhoamiResponse res = (WhoamiResponse) service.send(new WhoAmiRequest()); + assertTrue(res.success); + assertNotNull(res.uuid); + assertEquals(32, res.uuid.length()); + + service.close(); + } + + @Test + void shouldSupportConnectionRequest() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + // Primero hacemos WHOAMI para obtener nuestro propio ID de conexión + WhoamiResponse whoami = (WhoamiResponse) service.send(new WhoAmiRequest()); + assertTrue(whoami.success); + assertNotNull(whoami.uuid); + assertEquals(32, whoami.uuid.length()); + + // Enviamos la solicitud CONNECTION con el mismo índice de conexión + ConnectionResponse response = (ConnectionResponse) service.send(new ConnectionRequest(whoami.uuid)); + assertTrue(response.found); + assertNotNull(response.item); + + ConnectionsItem item = response.item; + assertEquals(32, item.id.length()); + assertTrue(item.type == 0x00 || item.type == 0x01); + assertTrue(item.kind == 0x00 || item.kind == 0x01); + assertTrue(item.ipVersion == 0x04 || item.ipVersion == 0x06); + assertTrue(item.port > 0); + assertTrue(item.connectedAt > 0); + + service.close(); + } + + @Test + void shouldSupportChannelsRequest() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + ChannelsResponse response = (ChannelsResponse) service.send(new ChannelsRequest()); + assertTrue(response.success); + assertNotNull(response.items); + assertTrue(response.items.size() >= 2); + + service.close(); + } + + @Test + void shouldSupportChannelRequest() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + // Primero obtenemos nuestro UUID con WHOAMI + WhoamiResponse whoami = (WhoamiResponse) service.send(new WhoAmiRequest()); + assertTrue(whoami.success); + assertNotNull(whoami.uuid); + assertEquals(32, whoami.uuid.length()); + + // Ahora pedimos el CHANNEL de nuestro propio UUID + ChannelResponse response = (ChannelResponse) service.send(new ChannelRequest(whoami.uuid)); + assertTrue(response.success); + assertNotNull(response.connections); + assertTrue(response.connections.size() >= 1); + + for (ChannelConnectionItem item : response.connections) { + assertNotNull(item.id); + assertEquals(32, item.id.length()); + assertTrue(item.subscribedAt >= 0); + assertTrue(item.readBytes >= 0); + assertTrue(item.writeBytes >= 0); + } + + service.close(); + } + @Test void shouldThrowIfMaxConnectionsIsZero() { IllegalArgumentException ex = assertThrows( From f7cbfe922a9b007fb87c90ed904b17f43d7cfdb7 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sun, 29 Jun 2025 07:50:25 -0400 Subject: [PATCH 09/31] SUBSCRIBE and PUBLISH added. --- .../java/cl/throttr/ByteBufAccumulator.java | 53 +++++++++++++++++-- src/main/java/cl/throttr/Connection.java | 23 +++++++- src/main/java/cl/throttr/Service.java | 13 +++++ .../cl/throttr/requests/SubscribeRequest.java | 2 +- src/test/java/cl/throttr/ServiceTest.java | 37 +++++++++++++ 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/main/java/cl/throttr/ByteBufAccumulator.java b/src/main/java/cl/throttr/ByteBufAccumulator.java index 1c88e23..edafea1 100644 --- a/src/main/java/cl/throttr/ByteBufAccumulator.java +++ b/src/main/java/cl/throttr/ByteBufAccumulator.java @@ -19,6 +19,7 @@ import cl.throttr.parsers.*; import cl.throttr.requests.PendingRequest; import cl.throttr.responses.*; +import cl.throttr.utils.Binary; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; @@ -26,15 +27,20 @@ import java.util.Map; import java.util.Queue; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; public class ByteBufAccumulator extends SimpleChannelInboundHandler { private final Queue pending; + private final Map> subscriptions; + private final ValueSize size; private ByteBuf buffer; private final Map parsers; - public ByteBufAccumulator(Queue pending, ValueSize size) { + public ByteBufAccumulator(Queue pending, Map> subscriptions, ValueSize size) { this.pending = pending; + this.subscriptions = subscriptions; + this.size = size; this.parsers = Map.ofEntries( Map.entry(0x01, new StatusParser()), Map.entry(0x02, new QueryParser(size)), @@ -46,9 +52,9 @@ public ByteBufAccumulator(Queue pending, ValueSize size) { Map.entry(0x08, new InfoParser()), Map.entry(0x09, new StatParser()), Map.entry(0x10, new StatsParser()), - Map.entry(0x11, new StatusParser()), - Map.entry(0x12, new StatusParser()), - Map.entry(0x13, new StatusParser()), + Map.entry(0x11, new StatusParser()), // SUBSCRIBE + Map.entry(0x12, new StatusParser()), // UNSUBSCRIBE + Map.entry(0x13, new StatusParser()), // PUBLISH Map.entry(0x14, new ConnectionsParser()), Map.entry(0x15, new ConnectionParser()), Map.entry(0x16, new ChannelsParser()), @@ -69,6 +75,43 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf incoming) { buffer.markReaderIndex(); + int type = buffer.getUnsignedByte(buffer.readerIndex()); + + if (type == 0x19) { + int readerIndex = buffer.readerIndex(); + if (buffer.readableBytes() < 1 + size.getValue()) { + return; + } + + int channelSize = Byte.toUnsignedInt(buffer.getByte(readerIndex + 1)); + int headerSize = 1 + 1 + size.getValue() + channelSize; + + if (buffer.readableBytes() < headerSize) { + return; + } + + long payloadLength = Binary.read(buffer, readerIndex + 2, size); + if (buffer.readableBytes() < headerSize + payloadLength) { + return; + } + + byte[] channelBytes = new byte[channelSize]; + buffer.getBytes(readerIndex + 2 + size.getValue(), channelBytes); + String channel = new String(channelBytes); + + byte[] payloadBytes = new byte[(int) payloadLength]; + buffer.getBytes(readerIndex + headerSize, payloadBytes); + String payload = new String(payloadBytes); + + buffer.readerIndex(readerIndex + headerSize + (int) payloadLength); + + Consumer callback = subscriptions.get(channel); + if (callback != null) { + callback.accept(payload); + } + continue; + } + PendingRequest pendingRequest = pending.peek(); if (pendingRequest == null) { buffer.resetReaderIndex(); @@ -97,4 +140,4 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf incoming) { public void handlerRemoved(ChannelHandlerContext ctx) { if (buffer != null) buffer.release(); } -} \ No newline at end of file +} diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 3554a89..adc13ed 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -30,17 +30,19 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.*; +import java.util.function.Consumer; public class Connection implements AutoCloseable { private final ValueSize size; private final Channel channel; private final EventLoopGroup group; private final Queue pending = new ConcurrentLinkedQueue<>(); + private final Map> subscriptions = new ConcurrentHashMap<>(); private final ByteBufAccumulator accumulator; public Connection(String host, int port, ValueSize size) throws IOException { this.size = size; - this.accumulator = new ByteBufAccumulator(this.pending, size); + this.accumulator = new ByteBufAccumulator(this.pending, this.subscriptions, size); this.group = new NioEventLoopGroup(); try { @@ -158,6 +160,9 @@ public static byte[] getRequestBuffer(Object request, ValueSize size) { case InfoRequest info -> info.toBytes(); case StatRequest stat -> stat.toBytes(); case StatsRequest stats -> stats.toBytes(); + case SubscribeRequest subscribe -> subscribe.toBytes(); + case UnsubscribeRequest unsubscribe -> unsubscribe.toBytes(); + case PublishRequest publish -> publish.toBytes(size); case ConnectionsRequest connections -> connections.toBytes(); case ConnectionRequest connection -> connection.toBytes(); case ChannelsRequest channels -> channels.toBytes(); @@ -167,6 +172,22 @@ public static byte[] getRequestBuffer(Object request, ValueSize size) { }; } + public void subscribe(String name, Consumer callback) throws IOException { + subscriptions.put(name, callback); + + SubscribeRequest request = new SubscribeRequest(name); + byte[] buffer = request.toBytes(); + + channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); + } + + public void triggerEvent(String channel, String data) { + Consumer callback = subscriptions.get(channel); + if (callback != null) { + callback.accept(data); + } + } + @Override public void close() { try { diff --git a/src/main/java/cl/throttr/Service.java b/src/main/java/cl/throttr/Service.java index ea10a08..9d93f0a 100644 --- a/src/main/java/cl/throttr/Service.java +++ b/src/main/java/cl/throttr/Service.java @@ -102,6 +102,19 @@ public Object send(Object request) throws IOException { } } + /** + * Get a direct connection (for subscription or fixed binding) + * + * @return Connection + */ + public Connection getConnection() { + if (connections.isEmpty()) { + throw new IllegalStateException("There are no available connections."); + } + int index = roundRobinIndex.getAndUpdate(i -> (i + 1) % connections.size()); + return connections.get(index); + } + /** * Close */ diff --git a/src/main/java/cl/throttr/requests/SubscribeRequest.java b/src/main/java/cl/throttr/requests/SubscribeRequest.java index f37c967..0eafa42 100644 --- a/src/main/java/cl/throttr/requests/SubscribeRequest.java +++ b/src/main/java/cl/throttr/requests/SubscribeRequest.java @@ -40,7 +40,7 @@ public byte[] toBytes() { ); buffer.order(ByteOrder.LITTLE_ENDIAN); - buffer.put((byte) RequestType.STAT.getValue()); + buffer.put((byte) RequestType.SUBSCRIBE.getValue()); buffer.put((byte) channelBytes.length); buffer.put(channelBytes); diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index e1dc590..3bcd669 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -25,6 +25,8 @@ import java.time.Duration; import java.util.List; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; @@ -491,6 +493,41 @@ void shouldSupportChannelRequest() throws Exception { service.close(); } + @Test + void shouldReceivePublishedMessageAfterSubscribe() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + String channel = "test-channel-" + UUID.randomUUID(); + String payload = "hola mundo"; + + CountDownLatch latch = new CountDownLatch(1); + StringBuilder received = new StringBuilder(); + + service.getConnection().subscribe(channel, msg -> { + received.append(msg); + latch.countDown(); + }); + + Thread.sleep(100); // que le de tiempo a registrarse + + StatusResponse pub = (StatusResponse) service.send(new PublishRequest(channel, payload)); + assertTrue(pub.success()); + + boolean success = latch.await(2, TimeUnit.SECONDS); + assertTrue(success, "No se recibió el mensaje a tiempo"); + assertEquals(payload, received.toString()); + + service.close(); + } + + @Test void shouldThrowIfMaxConnectionsIsZero() { IllegalArgumentException ex = assertThrows( From edded7053ec4fbc6c5f3e53860fb026d62bc3608 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sun, 29 Jun 2025 07:55:07 -0400 Subject: [PATCH 10/31] UNSUBSCRIBE added. --- src/main/java/cl/throttr/Connection.java | 12 +++--- .../throttr/requests/UnsubscribeRequest.java | 2 +- src/test/java/cl/throttr/ServiceTest.java | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index adc13ed..3b7d10a 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -181,11 +181,13 @@ public void subscribe(String name, Consumer callback) throws IOException channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); } - public void triggerEvent(String channel, String data) { - Consumer callback = subscriptions.get(channel); - if (callback != null) { - callback.accept(data); - } + public void unsubscribe(String name) throws IOException { + subscriptions.remove(name); + + UnsubscribeRequest request = new UnsubscribeRequest(name); + byte[] buffer = request.toBytes(); + + channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); } @Override diff --git a/src/main/java/cl/throttr/requests/UnsubscribeRequest.java b/src/main/java/cl/throttr/requests/UnsubscribeRequest.java index 42aa357..92b04b4 100644 --- a/src/main/java/cl/throttr/requests/UnsubscribeRequest.java +++ b/src/main/java/cl/throttr/requests/UnsubscribeRequest.java @@ -40,7 +40,7 @@ public byte[] toBytes() { ); buffer.order(ByteOrder.LITTLE_ENDIAN); - buffer.put((byte) RequestType.STAT.getValue()); + buffer.put((byte) RequestType.UNSUBSCRIBE.getValue()); buffer.put((byte) channelBytes.length); buffer.put(channelBytes); diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index 3bcd669..6495dd8 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -527,6 +527,44 @@ void shouldReceivePublishedMessageAfterSubscribe() throws Exception { service.close(); } + @Test + void shouldUnsubscribeAndNotReceiveMessages() throws Exception { + ValueSize sized = ValueSize.UINT8; + String size = System.getenv().getOrDefault("THROTTR_SIZE", "uint16"); + if ("uint16".equals(size)) sized = ValueSize.UINT16; + if ("uint32".equals(size)) sized = ValueSize.UINT32; + if ("uint64".equals(size)) sized = ValueSize.UINT64; + + Service service = new Service("127.0.0.1", 9000, sized, 1); + service.connect(); + + String channel = "test-channel-" + UUID.randomUUID(); + String payload = "hola mundo"; + + CountDownLatch latch = new CountDownLatch(1); + StringBuilder received = new StringBuilder(); + + var conn = service.getConnection(); + + conn.subscribe(channel, msg -> { + received.append(msg); + latch.countDown(); + }); + + Thread.sleep(100); // tiempo para que se registre la suscripción + + conn.unsubscribe(channel); + + StatusResponse pub = (StatusResponse) service.send(new PublishRequest(channel, payload)); + assertTrue(pub.success()); + + boolean success = latch.await(2, TimeUnit.SECONDS); + assertFalse(success, "Se recibió mensaje después de desuscribirse"); + + service.close(); + } + + @Test void shouldThrowIfMaxConnectionsIsZero() { From e23635a830264cafd8c05e17e4f31f7684dfb2dd Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sun, 29 Jun 2025 13:23:00 -0400 Subject: [PATCH 11/31] Fix sonnar issues. --- .../java/cl/throttr/ByteBufAccumulator.java | 2 - .../cl/throttr/responses/InfoResponse.java | 206 +++++++++--------- src/test/java/cl/throttr/ServiceTest.java | 10 +- 3 files changed, 108 insertions(+), 110 deletions(-) diff --git a/src/main/java/cl/throttr/ByteBufAccumulator.java b/src/main/java/cl/throttr/ByteBufAccumulator.java index edafea1..1e6a42d 100644 --- a/src/main/java/cl/throttr/ByteBufAccumulator.java +++ b/src/main/java/cl/throttr/ByteBufAccumulator.java @@ -18,7 +18,6 @@ import cl.throttr.enums.ValueSize; import cl.throttr.parsers.*; import cl.throttr.requests.PendingRequest; -import cl.throttr.responses.*; import cl.throttr.utils.Binary; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; @@ -26,7 +25,6 @@ import java.util.Map; import java.util.Queue; -import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; public class ByteBufAccumulator extends SimpleChannelInboundHandler { diff --git a/src/main/java/cl/throttr/responses/InfoResponse.java b/src/main/java/cl/throttr/responses/InfoResponse.java index 1db6391..5776c11 100644 --- a/src/main/java/cl/throttr/responses/InfoResponse.java +++ b/src/main/java/cl/throttr/responses/InfoResponse.java @@ -21,57 +21,57 @@ public class InfoResponse { public final boolean success; public final long timestamp; - public final long total_requests; - public final long total_requests_per_minute; - public final long total_insert_requests; - public final long total_insert_requests_per_minute; - public final long total_query_requests; - public final long total_query_requests_per_minute; - public final long total_update_requests; - public final long total_update_requests_per_minute; - public final long total_purge_requests; - public final long total_purge_requests_per_minute; - public final long total_get_requests; - public final long total_get_requests_per_minute; - public final long total_set_requests; - public final long total_set_requests_per_minute; - public final long total_list_requests; - public final long total_list_requests_per_minute; - public final long total_info_requests; - public final long total_info_requests_per_minute; - public final long total_stats_requests; - public final long total_stats_requests_per_minute; - public final long total_stat_requests; - public final long total_stat_requests_per_minute; - public final long total_subscribe_requests; - public final long total_subscribe_requests_per_minute; - public final long total_unsubscribe_requests; - public final long total_unsubscribe_requests_per_minute; - public final long total_publish_requests; - public final long total_publish_requests_per_minute; - public final long total_channel_requests; - public final long total_channel_requests_per_minute; - public final long total_channels_requests; - public final long total_channels_requests_per_minute; - public final long total_whoami_requests; - public final long total_whoami_requests_per_minute; - public final long total_connection_requests; - public final long total_connection_requests_per_minute; - public final long total_connections_requests; - public final long total_connections_requests_per_minute; - public final long total_read_bytes; - public final long total_read_bytes_per_minute; - public final long total_write_bytes; - public final long total_write_bytes_per_minute; - public final long total_keys; - public final long total_counters; - public final long total_buffers; - public final long total_allocated_bytes_on_counters; - public final long total_allocated_bytes_on_buffers; - public final long total_subscriptions; - public final long total_channels; - public final long startup_timestamp; - public final long total_connections; + public final long totalRequests; + public final long totalRequestsPerMinute; + public final long totalInsertRequests; + public final long totalInsertRequestsPerMinute; + public final long totalQueryRequests; + public final long totalQueryRequestsPerMinute; + public final long totalUpdateRequests; + public final long totalUpdateRequestsPerMinute; + public final long totalPurgeRequests; + public final long totalPurgeRequestsPerMinute; + public final long totalGetRequests; + public final long totalGetRequestsPerMinute; + public final long totalSetRequests; + public final long totalSetRequestsPerMinute; + public final long totalListRequests; + public final long totalListRequestsPerMinute; + public final long totalInfoRequests; + public final long totalInfoRequestsPerMinute; + public final long totalStatsRequests; + public final long totalStatsRequestsPerMinute; + public final long totalStatRequests; + public final long totalStatRequestsPerMinute; + public final long totalSubscribeRequests; + public final long totalSubscribeRequestsPerMinute; + public final long totalUnsubscribeRequests; + public final long totalUnsubscribeRequestsPerMinute; + public final long totalPublishRequests; + public final long totalPublishRequestsPerMinute; + public final long totalChannelRequests; + public final long totalChannelRequestsPerMinute; + public final long totalChannelsRequests; + public final long totalChannelsRequestsPerMinute; + public final long totalWhoamiRequests; + public final long totalWhoamiRequestsPerMinute; + public final long totalConnectionRequests; + public final long totalConnectionRequestsPerMinute; + public final long totalConnectionsRequests; + public final long totalConnectionsRequestsPerMinute; + public final long totalReadBytes; + public final long totalReadBytesPerMinute; + public final long totalWriteBytes; + public final long totalWriteBytesPerMinute; + public final long totalKeys; + public final long totalCounters; + public final long totalBuffers; + public final long totalAllocatedBytesOnCounters; + public final long totalAllocatedBytesOnBuffers; + public final long totalSubscriptions; + public final long totalChannels; + public final long startupTimestamp; + public final long totalConnections; public final String version; public InfoResponse( @@ -81,57 +81,57 @@ public InfoResponse( ) { this.success = success; this.timestamp = v[0]; - this.total_requests = v[1]; - this.total_requests_per_minute = v[2]; - this.total_insert_requests = v[3]; - this.total_insert_requests_per_minute = v[4]; - this.total_query_requests = v[5]; - this.total_query_requests_per_minute = v[6]; - this.total_update_requests = v[7]; - this.total_update_requests_per_minute = v[8]; - this.total_purge_requests = v[9]; - this.total_purge_requests_per_minute = v[10]; - this.total_get_requests = v[11]; - this.total_get_requests_per_minute = v[12]; - this.total_set_requests = v[13]; - this.total_set_requests_per_minute = v[14]; - this.total_list_requests = v[15]; - this.total_list_requests_per_minute = v[16]; - this.total_info_requests = v[17]; - this.total_info_requests_per_minute = v[18]; - this.total_stats_requests = v[19]; - this.total_stats_requests_per_minute = v[20]; - this.total_stat_requests = v[21]; - this.total_stat_requests_per_minute = v[22]; - this.total_subscribe_requests = v[23]; - this.total_subscribe_requests_per_minute = v[24]; - this.total_unsubscribe_requests = v[25]; - this.total_unsubscribe_requests_per_minute = v[26]; - this.total_publish_requests = v[27]; - this.total_publish_requests_per_minute = v[28]; - this.total_channel_requests = v[29]; - this.total_channel_requests_per_minute = v[30]; - this.total_channels_requests = v[31]; - this.total_channels_requests_per_minute = v[32]; - this.total_whoami_requests = v[33]; - this.total_whoami_requests_per_minute = v[34]; - this.total_connection_requests = v[35]; - this.total_connection_requests_per_minute = v[36]; - this.total_connections_requests = v[37]; - this.total_connections_requests_per_minute = v[38]; - this.total_read_bytes = v[39]; - this.total_read_bytes_per_minute = v[40]; - this.total_write_bytes = v[41]; - this.total_write_bytes_per_minute = v[42]; - this.total_keys = v[43]; - this.total_counters = v[44]; - this.total_buffers = v[45]; - this.total_allocated_bytes_on_counters = v[46]; - this.total_allocated_bytes_on_buffers = v[47]; - this.total_subscriptions = v[48]; - this.total_channels = v[49]; - this.startup_timestamp = v[50]; - this.total_connections = v[51]; + this.totalRequests = v[1]; + this.totalRequestsPerMinute = v[2]; + this.totalInsertRequests = v[3]; + this.totalInsertRequestsPerMinute = v[4]; + this.totalQueryRequests = v[5]; + this.totalQueryRequestsPerMinute = v[6]; + this.totalUpdateRequests = v[7]; + this.totalUpdateRequestsPerMinute = v[8]; + this.totalPurgeRequests = v[9]; + this.totalPurgeRequestsPerMinute = v[10]; + this.totalGetRequests = v[11]; + this.totalGetRequestsPerMinute = v[12]; + this.totalSetRequests = v[13]; + this.totalSetRequestsPerMinute = v[14]; + this.totalListRequests = v[15]; + this.totalListRequestsPerMinute = v[16]; + this.totalInfoRequests = v[17]; + this.totalInfoRequestsPerMinute = v[18]; + this.totalStatsRequests = v[19]; + this.totalStatsRequestsPerMinute = v[20]; + this.totalStatRequests = v[21]; + this.totalStatRequestsPerMinute = v[22]; + this.totalSubscribeRequests = v[23]; + this.totalSubscribeRequestsPerMinute = v[24]; + this.totalUnsubscribeRequests = v[25]; + this.totalUnsubscribeRequestsPerMinute = v[26]; + this.totalPublishRequests = v[27]; + this.totalPublishRequestsPerMinute = v[28]; + this.totalChannelRequests = v[29]; + this.totalChannelRequestsPerMinute = v[30]; + this.totalChannelsRequests = v[31]; + this.totalChannelsRequestsPerMinute = v[32]; + this.totalWhoamiRequests = v[33]; + this.totalWhoamiRequestsPerMinute = v[34]; + this.totalConnectionRequests = v[35]; + this.totalConnectionRequestsPerMinute = v[36]; + this.totalConnectionsRequests = v[37]; + this.totalConnectionsRequestsPerMinute = v[38]; + this.totalReadBytes = v[39]; + this.totalReadBytesPerMinute = v[40]; + this.totalWriteBytes = v[41]; + this.totalWriteBytesPerMinute = v[42]; + this.totalKeys = v[43]; + this.totalCounters = v[44]; + this.totalBuffers = v[45]; + this.totalAllocatedBytesOnCounters = v[46]; + this.totalAllocatedBytesOnBuffers = v[47]; + this.totalSubscriptions = v[48]; + this.totalChannels = v[49]; + this.startupTimestamp = v[50]; + this.totalConnections = v[51]; this.version = version; } @@ -153,4 +153,4 @@ public static InfoResponse fromBytes(byte[] full) { return new InfoResponse(success, values, version); } -} \ No newline at end of file +} diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index 6495dd8..ad150cb 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -287,11 +287,11 @@ void shouldSupportInfoAfterInsert() throws Exception { InfoResponse info = (InfoResponse) service.send(new InfoRequest()); assertTrue(info.success); - assertTrue(info.total_requests >= 0); - assertTrue(info.total_insert_requests >= 0); - assertTrue(info.total_requests_per_minute >= 0); - assertTrue(info.total_read_bytes >= 0); - assertTrue(info.total_write_bytes >= 0); + assertTrue(info.totalRequests >= 0); + assertTrue(info.totalInsertRequests >= 0); + assertTrue(info.totalRequestsPerMinute >= 0); + assertTrue(info.totalReadBytes >= 0); + assertTrue(info.totalWriteBytes >= 0); service.close(); } From e03e0d214bad60887994930945e1a8abd8d47062 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sun, 29 Jun 2025 13:26:30 -0400 Subject: [PATCH 12/31] Better taste. --- .../java/cl/throttr/parsers/ChannelParser.java | 1 - .../cl/throttr/requests/ConnectionRequest.java | 1 - .../java/cl/throttr/responses/StatResponse.java | 14 +++++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/ChannelParser.java b/src/main/java/cl/throttr/parsers/ChannelParser.java index 86ca030..1d6ffb3 100644 --- a/src/main/java/cl/throttr/parsers/ChannelParser.java +++ b/src/main/java/cl/throttr/parsers/ChannelParser.java @@ -22,7 +22,6 @@ import cl.throttr.utils.Binary; import io.netty.buffer.ByteBuf; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HexFormat; import java.util.List; diff --git a/src/main/java/cl/throttr/requests/ConnectionRequest.java b/src/main/java/cl/throttr/requests/ConnectionRequest.java index cc128d7..512a43a 100644 --- a/src/main/java/cl/throttr/requests/ConnectionRequest.java +++ b/src/main/java/cl/throttr/requests/ConnectionRequest.java @@ -19,7 +19,6 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; /** * Stat request diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java index 27d747d..c9e2791 100644 --- a/src/main/java/cl/throttr/responses/StatResponse.java +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -38,7 +38,7 @@ public static StatResponse fromBytes(byte[] data) { long tr = Binary.read(buffer, ValueSize.UINT64); long tw = Binary.read(buffer, ValueSize.UINT64); - return new StatResponse(true, rpm, wpm, tr, tw); + return new StatResponse(success, rpm, wpm, tr, tw); } @Override @@ -55,12 +55,12 @@ public String toString() { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof StatResponse that)) return false; - return success == that.success && - readsPerMinute == that.readsPerMinute && - writesPerMinute == that.writesPerMinute && - totalReads == that.totalReads && - totalWrites == that.totalWrites; + if (!(o instanceof StatResponse(boolean success1, long readsPerMinuteParam, long writesPerMinuteParam, long reads, long writes))) return false; + return success == success1 && + readsPerMinute == readsPerMinuteParam && + writesPerMinute == writesPerMinuteParam && + totalReads == reads && + totalWrites == writes; } @Override From 4058417ce8f09de3a1106e5b4f93db0436f6c54e Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Sun, 29 Jun 2025 13:45:23 -0400 Subject: [PATCH 13/31] Fix --- src/main/java/cl/throttr/parsers/StatParser.java | 1 + src/main/java/cl/throttr/responses/StatResponse.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/StatParser.java b/src/main/java/cl/throttr/parsers/StatParser.java index ff1b6bf..bb25341 100644 --- a/src/main/java/cl/throttr/parsers/StatParser.java +++ b/src/main/java/cl/throttr/parsers/StatParser.java @@ -33,6 +33,7 @@ public ReadResult tryParse(ByteBuf buf) { } byte[] data = new byte[expected]; + buf.getBytes(index, data); return new ReadResult(StatResponse.fromBytes(data), expected); } } \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java index c9e2791..ec27bb5 100644 --- a/src/main/java/cl/throttr/responses/StatResponse.java +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -17,6 +17,7 @@ import cl.throttr.enums.ValueSize; import cl.throttr.utils.Binary; +import com.sun.jdi.Value; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -31,8 +32,7 @@ public record StatResponse( ) { public static StatResponse fromBytes(byte[] data) { ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); - boolean success = buffer.get() == 1; - + boolean success = Binary.read(buffer, ValueSize.UINT8) == 0x01; long rpm = Binary.read(buffer, ValueSize.UINT64); long wpm = Binary.read(buffer, ValueSize.UINT64); long tr = Binary.read(buffer, ValueSize.UINT64); From b48c2763b3348aca34d821fe78ee0d6a8ccaac7d Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:08:37 -0400 Subject: [PATCH 14/31] Fix sonar issues. --- .../java/cl/throttr/ByteBufAccumulator.java | 101 ++++++++++-------- src/main/java/cl/throttr/Connection.java | 7 ++ .../cl/throttr/responses/StatResponse.java | 1 - 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/src/main/java/cl/throttr/ByteBufAccumulator.java b/src/main/java/cl/throttr/ByteBufAccumulator.java index 1e6a42d..690652a 100644 --- a/src/main/java/cl/throttr/ByteBufAccumulator.java +++ b/src/main/java/cl/throttr/ByteBufAccumulator.java @@ -72,66 +72,77 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf incoming) { if (buffer.readableBytes() < 1) return; buffer.markReaderIndex(); - int type = buffer.getUnsignedByte(buffer.readerIndex()); if (type == 0x19) { - int readerIndex = buffer.readerIndex(); - if (buffer.readableBytes() < 1 + size.getValue()) { - return; - } + if (!handleChannelMessage()) return; + continue; + } - int channelSize = Byte.toUnsignedInt(buffer.getByte(readerIndex + 1)); - int headerSize = 1 + 1 + size.getValue() + channelSize; + if (!handlePendingRequest()) return; + } + } - if (buffer.readableBytes() < headerSize) { - return; - } + private boolean handleChannelMessage() { + int readerIndex = buffer.readerIndex(); - long payloadLength = Binary.read(buffer, readerIndex + 2, size); - if (buffer.readableBytes() < headerSize + payloadLength) { - return; - } + if (buffer.readableBytes() < 1 + size.getValue()) { + return false; + } - byte[] channelBytes = new byte[channelSize]; - buffer.getBytes(readerIndex + 2 + size.getValue(), channelBytes); - String channel = new String(channelBytes); + int channelSize = Byte.toUnsignedInt(buffer.getByte(readerIndex + 1)); + int headerSize = 1 + 1 + size.getValue() + channelSize; - byte[] payloadBytes = new byte[(int) payloadLength]; - buffer.getBytes(readerIndex + headerSize, payloadBytes); - String payload = new String(payloadBytes); + if (buffer.readableBytes() < headerSize) { + return false; + } - buffer.readerIndex(readerIndex + headerSize + (int) payloadLength); + long payloadLength = Binary.read(buffer, readerIndex + 2, size); + if (buffer.readableBytes() < headerSize + payloadLength) { + return false; + } - Consumer callback = subscriptions.get(channel); - if (callback != null) { - callback.accept(payload); - } - continue; - } + byte[] channelBytes = new byte[channelSize]; + buffer.getBytes(readerIndex + 2 + size.getValue(), channelBytes); + String channel = new String(channelBytes); - PendingRequest pendingRequest = pending.peek(); - if (pendingRequest == null) { - buffer.resetReaderIndex(); - return; - } + byte[] payloadBytes = new byte[(int) payloadLength]; + buffer.getBytes(readerIndex + headerSize, payloadBytes); + String payload = new String(payloadBytes); - int expectedType = pendingRequest.type(); - ResponseParser parser = parsers.get(expectedType); - if (parser == null) { - buffer.resetReaderIndex(); - throw new IllegalArgumentException("Unknown response type: " + expectedType); - } + buffer.readerIndex(readerIndex + headerSize + (int) payloadLength); - ReadResult result = parser.tryParse(buffer); - if (result == null) { - buffer.resetReaderIndex(); - return; - } + Consumer callback = subscriptions.get(channel); + if (callback != null) { + callback.accept(payload); + } + + return true; + } - buffer.skipBytes(result.consumed()); - pending.poll().future().complete(result.value()); + private boolean handlePendingRequest() { + PendingRequest pendingRequest = pending.peek(); + if (pendingRequest == null) { + buffer.resetReaderIndex(); + return false; } + + int expectedType = pendingRequest.type(); + ResponseParser parser = parsers.get(expectedType); + if (parser == null) { + buffer.resetReaderIndex(); + throw new IllegalArgumentException("Unknown response type: " + expectedType); + } + + ReadResult result = parser.tryParse(buffer); + if (result == null) { + buffer.resetReaderIndex(); + return false; + } + + buffer.skipBytes(result.consumed()); + pending.poll().future().complete(result.value()); + return true; } @Override diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 3b7d10a..0262334 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -119,6 +119,9 @@ public Object send(Object request) throws IOException { try { // Try to resolve and push as response responses.add(f.get()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Thread was interrupted while awaiting response", e); } catch (Exception e) { throw new IOException("Failed while awaiting response", e); } @@ -143,6 +146,9 @@ public Object send(Object request) throws IOException { try { // Return the response object return future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Thread was interrupted while awaiting response", e); } catch (Exception e) { throw new IOException("Failed while awaiting response", e); } @@ -195,6 +201,7 @@ public void close() { try { if (channel != null) channel.close().sync(); } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); } group.shutdownGracefully(); } diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java index ec27bb5..91abb1b 100644 --- a/src/main/java/cl/throttr/responses/StatResponse.java +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -17,7 +17,6 @@ import cl.throttr.enums.ValueSize; import cl.throttr.utils.Binary; -import com.sun.jdi.Value; import java.nio.ByteBuffer; import java.nio.ByteOrder; From 049422ae294f02949d57ad61531f7d763d972e22 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:15:40 -0400 Subject: [PATCH 15/31] Fix. --- src/main/java/cl/throttr/Connection.java | 110 +----------------- .../java/cl/throttr/requests/Dispatcher.java | 88 ++++++++++++++ .../java/cl/throttr/requests/Serializer.java | 29 +++++ src/test/java/cl/throttr/ConnectionTest.java | 5 +- 4 files changed, 121 insertions(+), 111 deletions(-) create mode 100644 src/main/java/cl/throttr/requests/Dispatcher.java create mode 100644 src/main/java/cl/throttr/requests/Serializer.java diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 0262334..033a735 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -26,7 +26,6 @@ import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.*; import java.util.concurrent.*; @@ -68,114 +67,7 @@ protected void initChannel(SocketChannel ch) { } public Object send(Object request) throws IOException { - // Detect if connection is alive - if (!channel.isActive()) { - throw new IOException("Socket is already closed"); - } - - // If is a batch of - if (request instanceof List list) { - // Create a buffer to merge requests - ByteArrayOutputStream totalBuffer = new ByteArrayOutputStream(); - - // Capture the types - List types = new ArrayList<>(); - - // Per request - for (Object req : list) { - // Build his buffer - byte[] buffer = getRequestBuffer(req, size); - // Write that buffer inside merged buffer - totalBuffer.writeBytes(buffer); - // Push type - types.add(Byte.toUnsignedInt(buffer[0])); - } - - // This convert totalBuffer into a consumable array of bytes - byte[] finalBuffer = totalBuffer.toByteArray(); - - // Build a futures to resolve here - List> futures = new ArrayList<>(); - - // By request - for (int type : types) { - // Build a completable future of Object - CompletableFuture f = new CompletableFuture<>(); - - // Add pending function as pending request - pending.add(new PendingRequest(f, type)); - // Push this function to the local promises array - futures.add(f); - } - - // Write - channel.writeAndFlush(Unpooled.wrappedBuffer(finalBuffer)).syncUninterruptibly(); - - // Generate a responses as a list - List responses = new ArrayList<>(); - - // One by one on pending requests - for (CompletableFuture f : futures) { - try { - // Try to resolve and push as response - responses.add(f.get()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Thread was interrupted while awaiting response", e); - } catch (Exception e) { - throw new IOException("Failed while awaiting response", e); - } - } - // Return the response batch - return responses; - } - - // Build the buffer - byte[] buffer = getRequestBuffer(request, size); - - // Make a completable future for the response object - CompletableFuture future = new CompletableFuture<>(); - - // Add the future to the pending queue - int type = Byte.toUnsignedInt(buffer[0]); - pending.add(new PendingRequest(future, type)); - - // Write - channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); - - try { - // Return the response object - return future.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Thread was interrupted while awaiting response", e); - } catch (Exception e) { - throw new IOException("Failed while awaiting response", e); - } - } - - public static byte[] getRequestBuffer(Object request, ValueSize size) { - return switch (request) { - case InsertRequest insert -> insert.toBytes(size); - case QueryRequest query -> query.toBytes(); - case UpdateRequest update -> update.toBytes(size); - case PurgeRequest purge -> purge.toBytes(); - case SetRequest set -> set.toBytes(size); - case GetRequest get -> get.toBytes(); - case ListRequest list -> list.toBytes(); - case InfoRequest info -> info.toBytes(); - case StatRequest stat -> stat.toBytes(); - case StatsRequest stats -> stats.toBytes(); - case SubscribeRequest subscribe -> subscribe.toBytes(); - case UnsubscribeRequest unsubscribe -> unsubscribe.toBytes(); - case PublishRequest publish -> publish.toBytes(size); - case ConnectionsRequest connections -> connections.toBytes(); - case ConnectionRequest connection -> connection.toBytes(); - case ChannelsRequest channels -> channels.toBytes(); - case ChannelRequest channel -> channel.toBytes(); - case WhoAmiRequest whoami -> whoami.toBytes(); - case null, default -> throw new IllegalArgumentException("Unsupported request type"); - }; + return Dispatcher.dispatch(channel, pending, request, size); } public void subscribe(String name, Consumer callback) throws IOException { diff --git a/src/main/java/cl/throttr/requests/Dispatcher.java b/src/main/java/cl/throttr/requests/Dispatcher.java new file mode 100644 index 0000000..fd90237 --- /dev/null +++ b/src/main/java/cl/throttr/requests/Dispatcher.java @@ -0,0 +1,88 @@ +// Copyright (C) 2025 Ian Torres +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cl.throttr.requests; + +import cl.throttr.enums.ValueSize; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public final class Dispatcher { + + private Dispatcher() {} + + public static Object dispatch(Channel channel, Queue pending, Object request, ValueSize size) throws IOException { + if (!channel.isActive()) { + throw new IOException("Socket is already closed"); + } + + if (request instanceof List list) { + ByteArrayOutputStream totalBuffer = new ByteArrayOutputStream(); + List types = new ArrayList<>(); + + for (Object req : list) { + byte[] buffer = Serializer.invoke(req, size); + totalBuffer.writeBytes(buffer); + types.add(Byte.toUnsignedInt(buffer[0])); + } + + byte[] finalBuffer = totalBuffer.toByteArray(); + List> futures = new ArrayList<>(); + + for (int type : types) { + CompletableFuture f = new CompletableFuture<>(); + pending.add(new PendingRequest(f, type)); + futures.add(f); + } + + channel.writeAndFlush(Unpooled.wrappedBuffer(finalBuffer)).syncUninterruptibly(); + + List responses = new ArrayList<>(); + for (CompletableFuture f : futures) { + try { + responses.add(f.get()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Thread was interrupted while awaiting response", e); + } catch (ExecutionException e) { + throw new IOException("Failed while awaiting response", e.getCause()); + } + } + return responses; + } + + byte[] buffer = Serializer.invoke(request, size); + CompletableFuture future = new CompletableFuture<>(); + int type = Byte.toUnsignedInt(buffer[0]); + pending.add(new PendingRequest(future, type)); + + channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); + + try { + return future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Thread was interrupted while awaiting response", e); + } catch (ExecutionException e) { + throw new IOException("Failed while awaiting response", e.getCause()); + } + } +} diff --git a/src/main/java/cl/throttr/requests/Serializer.java b/src/main/java/cl/throttr/requests/Serializer.java new file mode 100644 index 0000000..1fc8d4e --- /dev/null +++ b/src/main/java/cl/throttr/requests/Serializer.java @@ -0,0 +1,29 @@ +package cl.throttr.requests; + +import cl.throttr.enums.ValueSize; + +public class Serializer { + public static byte[] invoke(Object request, ValueSize size) { + return switch (request) { + case InsertRequest insert -> insert.toBytes(size); + case QueryRequest query -> query.toBytes(); + case UpdateRequest update -> update.toBytes(size); + case PurgeRequest purge -> purge.toBytes(); + case SetRequest set -> set.toBytes(size); + case GetRequest get -> get.toBytes(); + case ListRequest list -> list.toBytes(); + case InfoRequest info -> info.toBytes(); + case StatRequest stat -> stat.toBytes(); + case StatsRequest stats -> stats.toBytes(); + case SubscribeRequest subscribe -> subscribe.toBytes(); + case UnsubscribeRequest unsubscribe -> unsubscribe.toBytes(); + case PublishRequest publish -> publish.toBytes(size); + case ConnectionsRequest connections -> connections.toBytes(); + case ConnectionRequest connection -> connection.toBytes(); + case ChannelsRequest channels -> channels.toBytes(); + case ChannelRequest channel -> channel.toBytes(); + case WhoAmiRequest whoami -> whoami.toBytes(); + case null, default -> throw new IllegalArgumentException("Unsupported request type"); + }; + } +} diff --git a/src/test/java/cl/throttr/ConnectionTest.java b/src/test/java/cl/throttr/ConnectionTest.java index d8df244..eaba694 100644 --- a/src/test/java/cl/throttr/ConnectionTest.java +++ b/src/test/java/cl/throttr/ConnectionTest.java @@ -1,6 +1,7 @@ package cl.throttr; import cl.throttr.enums.ValueSize; +import cl.throttr.requests.Serializer; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -15,7 +16,7 @@ void shouldThrowIfUnsupportedRequestTypeGiven() { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, - () -> Connection.getRequestBuffer(invalidRequest, ValueSize.UINT16) + () -> Serializer.invoke(invalidRequest, ValueSize.UINT16) ); assertEquals("Unsupported request type", ex.getMessage()); @@ -25,7 +26,7 @@ void shouldThrowIfUnsupportedRequestTypeGiven() { void shouldThrowIfNullRequestGiven() { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, - () -> Connection.getRequestBuffer(null, ValueSize.UINT16) + () -> Serializer.invoke(null, ValueSize.UINT16) ); assertEquals("Unsupported request type", ex.getMessage()); From 32d111d67150ed8133565283fb208c57d3a4b3e3 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:26:03 -0400 Subject: [PATCH 16/31] Fixes. --- src/main/java/cl/throttr/Connection.java | 4 +- .../java/cl/throttr/requests/Dispatcher.java | 63 ++++++------ .../java/cl/throttr/requests/Serializer.java | 3 + .../cl/throttr/responses/ConnectionsItem.java | 96 ++++++++++--------- 4 files changed, 94 insertions(+), 72 deletions(-) diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 033a735..96d3f9e 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -70,7 +70,7 @@ public Object send(Object request) throws IOException { return Dispatcher.dispatch(channel, pending, request, size); } - public void subscribe(String name, Consumer callback) throws IOException { + public void subscribe(String name, Consumer callback) { subscriptions.put(name, callback); SubscribeRequest request = new SubscribeRequest(name); @@ -79,7 +79,7 @@ public void subscribe(String name, Consumer callback) throws IOException channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); } - public void unsubscribe(String name) throws IOException { + public void unsubscribe(String name) { subscriptions.remove(name); UnsubscribeRequest request = new UnsubscribeRequest(name); diff --git a/src/main/java/cl/throttr/requests/Dispatcher.java b/src/main/java/cl/throttr/requests/Dispatcher.java index fd90237..dfdf15f 100644 --- a/src/main/java/cl/throttr/requests/Dispatcher.java +++ b/src/main/java/cl/throttr/requests/Dispatcher.java @@ -35,40 +35,49 @@ public static Object dispatch(Channel channel, Queue pending, Ob } if (request instanceof List list) { - ByteArrayOutputStream totalBuffer = new ByteArrayOutputStream(); - List types = new ArrayList<>(); + return dispatchBatch(channel, pending, list, size); + } - for (Object req : list) { - byte[] buffer = Serializer.invoke(req, size); - totalBuffer.writeBytes(buffer); - types.add(Byte.toUnsignedInt(buffer[0])); - } + return dispatchSingle(channel, pending, request, size); + } - byte[] finalBuffer = totalBuffer.toByteArray(); - List> futures = new ArrayList<>(); + private static Object dispatchBatch(Channel channel, Queue pending, List list, ValueSize size) throws IOException { + ByteArrayOutputStream totalBuffer = new ByteArrayOutputStream(); + List types = new ArrayList<>(); - for (int type : types) { - CompletableFuture f = new CompletableFuture<>(); - pending.add(new PendingRequest(f, type)); - futures.add(f); - } + for (Object req : list) { + byte[] buffer = Serializer.invoke(req, size); + totalBuffer.writeBytes(buffer); + types.add(Byte.toUnsignedInt(buffer[0])); + } + + byte[] finalBuffer = totalBuffer.toByteArray(); + List> futures = new ArrayList<>(); - channel.writeAndFlush(Unpooled.wrappedBuffer(finalBuffer)).syncUninterruptibly(); - - List responses = new ArrayList<>(); - for (CompletableFuture f : futures) { - try { - responses.add(f.get()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Thread was interrupted while awaiting response", e); - } catch (ExecutionException e) { - throw new IOException("Failed while awaiting response", e.getCause()); - } + for (int type : types) { + CompletableFuture f = new CompletableFuture<>(); + pending.add(new PendingRequest(f, type)); + futures.add(f); + } + + channel.writeAndFlush(Unpooled.wrappedBuffer(finalBuffer)).syncUninterruptibly(); + + List responses = new ArrayList<>(); + for (CompletableFuture f : futures) { + try { + responses.add(f.get()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Thread was interrupted while awaiting batch response", e); + } catch (ExecutionException e) { + throw new IOException("Failed while awaiting batch response", e.getCause()); } - return responses; } + return responses; + } + + private static Object dispatchSingle(Channel channel, Queue pending, Object request, ValueSize size) throws IOException { byte[] buffer = Serializer.invoke(request, size); CompletableFuture future = new CompletableFuture<>(); int type = Byte.toUnsignedInt(buffer[0]); diff --git a/src/main/java/cl/throttr/requests/Serializer.java b/src/main/java/cl/throttr/requests/Serializer.java index 1fc8d4e..45f7d2f 100644 --- a/src/main/java/cl/throttr/requests/Serializer.java +++ b/src/main/java/cl/throttr/requests/Serializer.java @@ -3,6 +3,9 @@ import cl.throttr.enums.ValueSize; public class Serializer { + + private Serializer() {} + public static byte[] invoke(Object request, ValueSize size) { return switch (request) { case InsertRequest insert -> insert.toBytes(size); diff --git a/src/main/java/cl/throttr/responses/ConnectionsItem.java b/src/main/java/cl/throttr/responses/ConnectionsItem.java index 9c80d4e..350cede 100644 --- a/src/main/java/cl/throttr/responses/ConnectionsItem.java +++ b/src/main/java/cl/throttr/responses/ConnectionsItem.java @@ -54,14 +54,7 @@ public class ConnectionsItem { public final long whoamiRequests; public ConnectionsItem( - String id, byte type, byte kind, byte ipVersion, byte[] ip, int port, long connectedAt, - long readBytes, long writeBytes, long publishedBytes, long receivedBytes, - long allocatedBytes, long consumedBytes, - long insertRequests, long setRequests, long queryRequests, long getRequests, - long updateRequests, long purgeRequests, long listRequests, long infoRequests, - long statRequests, long statsRequests, long publishRequests, long subscribeRequests, - long unsubscribeRequests, long connectionsRequests, long connectionRequests, - long channelsRequests, long channelRequests, long whoamiRequests + String id, byte type, byte kind, byte ipVersion, byte[] ip, int port, long[] v ) { this.id = id; this.type = type; @@ -69,31 +62,31 @@ public ConnectionsItem( this.ipVersion = ipVersion; this.ip = ip; this.port = port; - this.connectedAt = connectedAt; - this.readBytes = readBytes; - this.writeBytes = writeBytes; - this.publishedBytes = publishedBytes; - this.receivedBytes = receivedBytes; - this.allocatedBytes = allocatedBytes; - this.consumedBytes = consumedBytes; - this.insertRequests = insertRequests; - this.setRequests = setRequests; - this.queryRequests = queryRequests; - this.getRequests = getRequests; - this.updateRequests = updateRequests; - this.purgeRequests = purgeRequests; - this.listRequests = listRequests; - this.infoRequests = infoRequests; - this.statRequests = statRequests; - this.statsRequests = statsRequests; - this.publishRequests = publishRequests; - this.subscribeRequests = subscribeRequests; - this.unsubscribeRequests = unsubscribeRequests; - this.connectionsRequests = connectionsRequests; - this.connectionRequests = connectionRequests; - this.channelsRequests = channelsRequests; - this.channelRequests = channelRequests; - this.whoamiRequests = whoamiRequests; + this.connectedAt = v[0]; + this.readBytes = v[1]; + this.writeBytes = v[2]; + this.publishedBytes = v[3]; + this.receivedBytes = v[4]; + this.allocatedBytes = v[5]; + this.consumedBytes = v[6]; + this.insertRequests = v[7]; + this.setRequests = v[8]; + this.queryRequests = v[9]; + this.getRequests = v[10]; + this.updateRequests = v[11]; + this.purgeRequests = v[12]; + this.listRequests = v[13]; + this.infoRequests = v[14]; + this.statRequests = v[15]; + this.statsRequests = v[16]; + this.publishRequests = v[17]; + this.subscribeRequests = v[18]; + this.unsubscribeRequests = v[19]; + this.connectionsRequests = v[20]; + this.connectionRequests = v[21]; + this.channelsRequests = v[22]; + this.channelRequests = v[23]; + this.whoamiRequests = v[24]; } public static ConnectionsItem fromBytes(byte[] data) { @@ -117,41 +110,58 @@ public static ConnectionsItem fromBytes(byte[] data) { int port = buf.getUnsignedShortLE(i); i += 2; + long[] v = new long[25]; long connectedAt = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[0] = connectedAt; long readBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[1] = readBytes; long writeBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[2] = writeBytes; long publishedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[3] = publishedBytes; long receivedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[4] = receivedBytes; long allocatedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[5] = allocatedBytes; long consumedBytes = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[6] = consumedBytes; long insertRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[7] = insertRequests; long setRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[8] = setRequests; long queryRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[9] = queryRequests; long getRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[10] = getRequests; long updateRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[11] = updateRequests; long purgeRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[12] = purgeRequests; long listRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[13] = listRequests; long infoRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[14] = infoRequests; long statRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[15] = statRequests; long statsRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[16] = statsRequests; long publishRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[17] = publishRequests; long subscribeRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[18] = subscribeRequests; long unsubscribeRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[19] = unsubscribeRequests; long connectionsRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[20] = connectionsRequests; long connectionRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[21] = connectionRequests; long channelsRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[22] = channelsRequests; long channelRequests = Binary.read(buf, i, ValueSize.UINT64); i += 8; + v[23] = channelRequests; long whoamiRequests = Binary.read(buf, i, ValueSize.UINT64); + v[24] = whoamiRequests; - return new ConnectionsItem( - id, type, kind, ipVersion, ip, port, connectedAt, - readBytes, writeBytes, publishedBytes, receivedBytes, - allocatedBytes, consumedBytes, - insertRequests, setRequests, queryRequests, getRequests, - updateRequests, purgeRequests, listRequests, infoRequests, - statRequests, statsRequests, publishRequests, subscribeRequests, - unsubscribeRequests, connectionsRequests, connectionRequests, - channelsRequests, channelRequests, whoamiRequests - ); + return new ConnectionsItem(id, type, kind, ipVersion, ip, port, v); } } From 160538ca22a6d9edfb122e4e7b4d65e39dc043b7 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:35:15 -0400 Subject: [PATCH 17/31] Fix sleeps. --- src/test/java/cl/throttr/ServiceTest.java | 65 ++++++++++++----------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index ad150cb..ef5114c 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -259,15 +259,17 @@ void shouldSupportStatsAfterSet() throws Exception { StatusResponse set = (StatusResponse) service.send(new SetRequest(TTLType.SECONDS, 30, key, value)); assertTrue(set.success()); - Thread.sleep(100); + Awaitility.await().atMost(Duration.ofMillis(200)).untilAsserted(() -> { - // STATS - StatsResponse stats = (StatsResponse) service.send(new StatsRequest()); - assertTrue(stats.isSuccess()); - assertNotNull(stats.getItems()); - assertTrue(stats.getItems().stream().anyMatch(item -> item.getKey().equals(key))); + // STATS + StatsResponse stats = (StatsResponse) service.send(new StatsRequest()); + assertTrue(stats.isSuccess()); + assertNotNull(stats.getItems()); + assertTrue(stats.getItems().stream().anyMatch(item -> item.getKey().equals(key))); + + service.close(); + }); - service.close(); } @Test @@ -311,16 +313,16 @@ void shouldSupportStatAfterInsert() throws Exception { StatusResponse insert = (StatusResponse) service.send(new InsertRequest(42, TTLType.SECONDS, 30, key)); assertTrue(insert.success()); - Thread.sleep(100); // asegurar algún registro + Awaitility.await().atMost(Duration.ofMillis(200)).untilAsserted(() -> { + StatResponse stat = (StatResponse) service.send(new StatRequest(key)); + assertTrue(stat.success()); + assertTrue(stat.readsPerMinute() >= 0); + assertTrue(stat.writesPerMinute() >= 0); + assertTrue(stat.totalReads() >= 0); + assertTrue(stat.totalWrites() >= 0); - StatResponse stat = (StatResponse) service.send(new StatRequest(key)); - assertTrue(stat.success()); - assertTrue(stat.readsPerMinute() >= 0); - assertTrue(stat.writesPerMinute() >= 0); - assertTrue(stat.totalReads() >= 0); - assertTrue(stat.totalWrites() >= 0); - - service.close(); + service.close(); + }); } @Test @@ -515,16 +517,17 @@ void shouldReceivePublishedMessageAfterSubscribe() throws Exception { latch.countDown(); }); - Thread.sleep(100); // que le de tiempo a registrarse - StatusResponse pub = (StatusResponse) service.send(new PublishRequest(channel, payload)); - assertTrue(pub.success()); + Awaitility.await().atMost(Duration.ofMillis(200)).untilAsserted(() -> { + StatusResponse pub = (StatusResponse) service.send(new PublishRequest(channel, payload)); + assertTrue(pub.success()); - boolean success = latch.await(2, TimeUnit.SECONDS); - assertTrue(success, "No se recibió el mensaje a tiempo"); - assertEquals(payload, received.toString()); + boolean success = latch.await(2, TimeUnit.SECONDS); + assertTrue(success, "No se recibió el mensaje a tiempo"); + assertEquals(payload, received.toString()); - service.close(); + service.close(); + }); } @Test @@ -551,17 +554,17 @@ void shouldUnsubscribeAndNotReceiveMessages() throws Exception { latch.countDown(); }); - Thread.sleep(100); // tiempo para que se registre la suscripción - - conn.unsubscribe(channel); + Awaitility.await().atMost(Duration.ofMillis(3000)).untilAsserted(() -> { + conn.unsubscribe(channel); - StatusResponse pub = (StatusResponse) service.send(new PublishRequest(channel, payload)); - assertTrue(pub.success()); + StatusResponse pub = (StatusResponse) service.send(new PublishRequest(channel, payload)); + assertTrue(pub.success()); - boolean success = latch.await(2, TimeUnit.SECONDS); - assertFalse(success, "Se recibió mensaje después de desuscribirse"); + boolean success = latch.await(2, TimeUnit.SECONDS); + assertFalse(success, "Se recibió mensaje después de desuscribirse"); - service.close(); + service.close(); + }); } From 2bc27ab299248a6606a83c68401ba6b6b1fe3cdb Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:41:06 -0400 Subject: [PATCH 18/31] Fix. --- src/main/java/cl/throttr/Service.java | 3 -- .../cl/throttr/parsers/ConnectionsParser.java | 4 --- .../java/cl/throttr/parsers/ListParser.java | 4 --- .../java/cl/throttr/parsers/StatsParser.java | 3 -- .../java/cl/throttr/requests/Serializer.java | 2 -- .../java/cl/throttr/responses/ListItem.java | 30 +++-------------- .../java/cl/throttr/responses/StatsItem.java | 32 ++++--------------- 7 files changed, 11 insertions(+), 67 deletions(-) diff --git a/src/main/java/cl/throttr/Service.java b/src/main/java/cl/throttr/Service.java index 9d93f0a..35cb18a 100644 --- a/src/main/java/cl/throttr/Service.java +++ b/src/main/java/cl/throttr/Service.java @@ -108,9 +108,6 @@ public Object send(Object request) throws IOException { * @return Connection */ public Connection getConnection() { - if (connections.isEmpty()) { - throw new IllegalStateException("There are no available connections."); - } int index = roundRobinIndex.getAndUpdate(i -> (i + 1) % connections.size()); return connections.get(index); } diff --git a/src/main/java/cl/throttr/parsers/ConnectionsParser.java b/src/main/java/cl/throttr/parsers/ConnectionsParser.java index 325ff11..1e89199 100644 --- a/src/main/java/cl/throttr/parsers/ConnectionsParser.java +++ b/src/main/java/cl/throttr/parsers/ConnectionsParser.java @@ -53,10 +53,6 @@ public ReadResult tryParse(ByteBuf buf) { long count = Binary.read(buf, i, ValueSize.UINT64); i += 8; - if (count > (Integer.MAX_VALUE / ENTRY_SIZE)) { - throw new ArithmeticException("Too many connections in fragment: " + count); - } - int totalFragmentBytes = Math.toIntExact(count) * ENTRY_SIZE; if (buf.readableBytes() < i - index + totalFragmentBytes) return null; diff --git a/src/main/java/cl/throttr/parsers/ListParser.java b/src/main/java/cl/throttr/parsers/ListParser.java index cf80bfe..870d51e 100644 --- a/src/main/java/cl/throttr/parsers/ListParser.java +++ b/src/main/java/cl/throttr/parsers/ListParser.java @@ -56,10 +56,6 @@ public ReadResult tryParse(ByteBuf buf) { long keysInFragment = Binary.read(buf, i, ValueSize.UINT64); i += 8; - if (keysInFragment > (Integer.MAX_VALUE / (3 + 8 + size.getValue()))) { - throw new ArithmeticException("Too many keys in fragment: " + keysInFragment); - } - int perKeyHeader = 3 + 8 + size.getValue(); int keyHeadersSize = Math.toIntExact(keysInFragment) * perKeyHeader; diff --git a/src/main/java/cl/throttr/parsers/StatsParser.java b/src/main/java/cl/throttr/parsers/StatsParser.java index 89356a0..733c4d9 100644 --- a/src/main/java/cl/throttr/parsers/StatsParser.java +++ b/src/main/java/cl/throttr/parsers/StatsParser.java @@ -51,9 +51,6 @@ public ReadResult tryParse(ByteBuf buf) { i += 8; int perKeyHeader = 33; - if (keysInFragment > (Integer.MAX_VALUE / perKeyHeader)) { - throw new ArithmeticException("Too many keys in fragment: " + keysInFragment); - } int keyHeadersSize = Math.toIntExact(keysInFragment) * perKeyHeader; if (buf.readableBytes() < i - index + keyHeadersSize) return null; diff --git a/src/main/java/cl/throttr/requests/Serializer.java b/src/main/java/cl/throttr/requests/Serializer.java index 45f7d2f..a4a7cc6 100644 --- a/src/main/java/cl/throttr/requests/Serializer.java +++ b/src/main/java/cl/throttr/requests/Serializer.java @@ -18,8 +18,6 @@ public static byte[] invoke(Object request, ValueSize size) { case InfoRequest info -> info.toBytes(); case StatRequest stat -> stat.toBytes(); case StatsRequest stats -> stats.toBytes(); - case SubscribeRequest subscribe -> subscribe.toBytes(); - case UnsubscribeRequest unsubscribe -> unsubscribe.toBytes(); case PublishRequest publish -> publish.toBytes(size); case ConnectionsRequest connections -> connections.toBytes(); case ConnectionRequest connection -> connection.toBytes(); diff --git a/src/main/java/cl/throttr/responses/ListItem.java b/src/main/java/cl/throttr/responses/ListItem.java index 8b83cfc..f20292c 100644 --- a/src/main/java/cl/throttr/responses/ListItem.java +++ b/src/main/java/cl/throttr/responses/ListItem.java @@ -17,11 +17,11 @@ public class ListItem { private String key; - private final int keyLength; - private final int keyType; - private final int ttlType; - private final long expiresAt; - private final long bytesUsed; + public final int keyLength; + public final int keyType; + public final int ttlType; + public final long expiresAt; + public final long bytesUsed; public ListItem(String key, int keyLength, int keyType, int ttlType, long expiresAt, long bytesUsed) { this.key = key; @@ -36,26 +36,6 @@ public String getKey() { return key; } - public int getKeyLength() { - return keyLength; - } - - public int getKeyType() { - return keyType; - } - - public int getTtlType() { - return ttlType; - } - - public long getExpiresAt() { - return expiresAt; - } - - public long getBytesUsed() { - return bytesUsed; - } - public void setKey(String key) { this.key = key; } diff --git a/src/main/java/cl/throttr/responses/StatsItem.java b/src/main/java/cl/throttr/responses/StatsItem.java index b97826a..8102ebe 100644 --- a/src/main/java/cl/throttr/responses/StatsItem.java +++ b/src/main/java/cl/throttr/responses/StatsItem.java @@ -16,12 +16,12 @@ package cl.throttr.responses; public class StatsItem { - private String key; - private final int keyLength; - private final long readsPerMinute; - private final long writesPerMinute; - private final long totalReads; - private final long totalWrites; + public String key; + public final int keyLength; + public final long readsPerMinute; + public final long writesPerMinute; + public final long totalReads; + public final long totalWrites; public StatsItem(String key, int keyLength, long readsPerMinute, long writesPerMinute, long totalReads, long totalWrites) { this.key = key; @@ -36,26 +36,6 @@ public String getKey() { return key; } - public int getKeyLength() { - return keyLength; - } - - public long getReadsPerMinute() { - return readsPerMinute; - } - - public long getWritesPerMinute() { - return writesPerMinute; - } - - public long getTotalReads() { - return totalReads; - } - - public long getTotalWrites() { - return totalWrites; - } - public void setKey(String key) { this.key = key; } From 579b8a361ab1392cc7742108c53d61bd07daaca4 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:50:53 -0400 Subject: [PATCH 19/31] Fixing coverage and CVE. --- pom.xml | 4 ++-- src/main/java/cl/throttr/Connection.java | 1 - src/main/java/cl/throttr/parsers/ConnectionParser.java | 3 --- src/main/java/cl/throttr/parsers/StatParser.java | 7 +++---- src/main/java/cl/throttr/parsers/WhoamiParser.java | 5 ----- src/test/java/cl/throttr/ServiceTest.java | 7 +++++++ 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 6f01d3b..443717f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ cl.throttr sdk - 4.1.1 + 5.0.0 jar Throttr SDK for Java @@ -71,7 +71,7 @@ io.netty netty-all - 4.1.107.Final + 4.1.122.Final diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 96d3f9e..c4808bb 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -53,7 +53,6 @@ public Connection(String host, int port, ValueSize size) throws IOException { @Override protected void initChannel(SocketChannel ch) { ch.pipeline() - .addLast(new LoggingHandler(LogLevel.ERROR)) .addLast(accumulator); } }); diff --git a/src/main/java/cl/throttr/parsers/ConnectionParser.java b/src/main/java/cl/throttr/parsers/ConnectionParser.java index ace0e43..41e368f 100644 --- a/src/main/java/cl/throttr/parsers/ConnectionParser.java +++ b/src/main/java/cl/throttr/parsers/ConnectionParser.java @@ -27,17 +27,14 @@ public class ConnectionParser implements ResponseParser { public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); - // Se necesita al menos 1 byte de status if (buf.readableBytes() < 1) return null; byte status = buf.getByte(index); if (status == 0x00) { - buf.readerIndex(index + 1); return new ReadResult(new ConnectionResponse(false, null), 1); } - // Si status es 0x01, requiere 1 + 237 bytes if (buf.readableBytes() < 1 + ENTRY_SIZE) return null; byte[] data = new byte[ENTRY_SIZE]; diff --git a/src/main/java/cl/throttr/parsers/StatParser.java b/src/main/java/cl/throttr/parsers/StatParser.java index bb25341..ddb433e 100644 --- a/src/main/java/cl/throttr/parsers/StatParser.java +++ b/src/main/java/cl/throttr/parsers/StatParser.java @@ -23,15 +23,14 @@ public class StatParser implements ResponseParser { @Override public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); - int expected = 1 + 8 * 4; // status + 4 campos uint64 - - if (buf.readableBytes() < expected) return null; - byte status = buf.getByte(index); if (status == 0x00) { return new ReadResult(new StatResponse(false, 0, 0, 0, 0), 1); } + int expected = 1 + 8 * 4; // status + 4 campos uint64 + if (buf.readableBytes() < expected) return null; + byte[] data = new byte[expected]; buf.getBytes(index, data); return new ReadResult(StatResponse.fromBytes(data), expected); diff --git a/src/main/java/cl/throttr/parsers/WhoamiParser.java b/src/main/java/cl/throttr/parsers/WhoamiParser.java index 2e9cd27..b825deb 100644 --- a/src/main/java/cl/throttr/parsers/WhoamiParser.java +++ b/src/main/java/cl/throttr/parsers/WhoamiParser.java @@ -27,11 +27,6 @@ public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); if (buf.readableBytes() < TOTAL_SIZE) return null; - byte status = buf.getByte(index); - if (status != 0x01) { - return new ReadResult(new WhoamiResponse(false, null), 1); - } - byte[] uuid = new byte[16]; buf.getBytes(index + 1, uuid); return new ReadResult(new WhoamiResponse(true, uuid), TOTAL_SIZE); diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index ef5114c..6ebf04e 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -313,6 +313,9 @@ void shouldSupportStatAfterInsert() throws Exception { StatusResponse insert = (StatusResponse) service.send(new InsertRequest(42, TTLType.SECONDS, 30, key)); assertTrue(insert.success()); + StatResponse error_stat = (StatResponse) service.send(new StatRequest("MISSING_KEY")); + assertFalse(error_stat.success()); + Awaitility.await().atMost(Duration.ofMillis(200)).untilAsserted(() -> { StatResponse stat = (StatResponse) service.send(new StatRequest(key)); assertTrue(stat.success()); @@ -431,6 +434,10 @@ void shouldSupportConnectionRequest() throws Exception { assertTrue(response.found); assertNotNull(response.item); + ConnectionResponse error_response = (ConnectionResponse) service.send(new ConnectionRequest("b7e0f7c8b6a04c678727303c3a90b341")); + assertFalse(error_response.found); + assertNull(error_response.item); + ConnectionsItem item = response.item; assertEquals(32, item.id.length()); assertTrue(item.type == 0x00 || item.type == 0x01); From 1eb0bbadbdb955b7d69bfd709893fb908b51b7f6 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 10:53:35 -0400 Subject: [PATCH 20/31] Fix. --- src/main/java/cl/throttr/Connection.java | 2 -- src/main/java/cl/throttr/parsers/StatsParser.java | 2 +- src/main/java/cl/throttr/responses/StatsItem.java | 8 -------- src/test/java/cl/throttr/ServiceTest.java | 12 ++++++------ 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index c4808bb..4e8e724 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -23,8 +23,6 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; import java.io.IOException; import java.util.*; diff --git a/src/main/java/cl/throttr/parsers/StatsParser.java b/src/main/java/cl/throttr/parsers/StatsParser.java index 733c4d9..d1cf8ad 100644 --- a/src/main/java/cl/throttr/parsers/StatsParser.java +++ b/src/main/java/cl/throttr/parsers/StatsParser.java @@ -77,7 +77,7 @@ public ReadResult tryParse(ByteBuf buf) { byte[] keyBytes = new byte[len]; buf.getBytes(i, keyBytes); i += len; - scopedItems.get(j).setKey(new String(keyBytes, StandardCharsets.UTF_8)); + scopedItems.get(j).key = new String(keyBytes, StandardCharsets.UTF_8); } items.addAll(scopedItems); diff --git a/src/main/java/cl/throttr/responses/StatsItem.java b/src/main/java/cl/throttr/responses/StatsItem.java index 8102ebe..9abc04d 100644 --- a/src/main/java/cl/throttr/responses/StatsItem.java +++ b/src/main/java/cl/throttr/responses/StatsItem.java @@ -31,12 +31,4 @@ public StatsItem(String key, int keyLength, long readsPerMinute, long writesPerM this.totalReads = totalReads; this.totalWrites = totalWrites; } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } } \ No newline at end of file diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index 6ebf04e..b4506fe 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -265,7 +265,7 @@ void shouldSupportStatsAfterSet() throws Exception { StatsResponse stats = (StatsResponse) service.send(new StatsRequest()); assertTrue(stats.isSuccess()); assertNotNull(stats.getItems()); - assertTrue(stats.getItems().stream().anyMatch(item -> item.getKey().equals(key))); + assertTrue(stats.getItems().stream().anyMatch(item -> item.key.equals(key))); service.close(); }); @@ -313,8 +313,8 @@ void shouldSupportStatAfterInsert() throws Exception { StatusResponse insert = (StatusResponse) service.send(new InsertRequest(42, TTLType.SECONDS, 30, key)); assertTrue(insert.success()); - StatResponse error_stat = (StatResponse) service.send(new StatRequest("MISSING_KEY")); - assertFalse(error_stat.success()); + StatResponse errorStat = (StatResponse) service.send(new StatRequest("MISSING_KEY")); + assertFalse(errorStat.success()); Awaitility.await().atMost(Duration.ofMillis(200)).untilAsserted(() -> { StatResponse stat = (StatResponse) service.send(new StatRequest(key)); @@ -434,9 +434,9 @@ void shouldSupportConnectionRequest() throws Exception { assertTrue(response.found); assertNotNull(response.item); - ConnectionResponse error_response = (ConnectionResponse) service.send(new ConnectionRequest("b7e0f7c8b6a04c678727303c3a90b341")); - assertFalse(error_response.found); - assertNull(error_response.item); + ConnectionResponse errorResponse = (ConnectionResponse) service.send(new ConnectionRequest("b7e0f7c8b6a04c678727303c3a90b341")); + assertFalse(errorResponse.found); + assertNull(errorResponse.item); ConnectionsItem item = response.item; assertEquals(32, item.id.length()); From d9e00b2b8b49c122dff0e278a7c5fbd1dbdd5e1d Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:08:15 -0400 Subject: [PATCH 21/31] Fix. --- .../cl/throttr/parsers/ChannelParser.java | 1 - .../cl/throttr/parsers/ConnectionParser.java | 2 - .../java/cl/throttr/parsers/GetParser.java | 1 - .../cl/throttr/responses/GetResponse.java | 8 ---- .../cl/throttr/responses/StatResponse.java | 27 ------------- .../cl/throttr/responses/WhoamiResponse.java | 2 +- src/test/java/cl/throttr/BinaryTest.java | 38 +++++++++++++++++++ src/test/java/cl/throttr/ServiceTest.java | 3 ++ 8 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/ChannelParser.java b/src/main/java/cl/throttr/parsers/ChannelParser.java index 1d6ffb3..286add7 100644 --- a/src/main/java/cl/throttr/parsers/ChannelParser.java +++ b/src/main/java/cl/throttr/parsers/ChannelParser.java @@ -37,7 +37,6 @@ public ReadResult tryParse(ByteBuf buf) { byte status = buf.getByte(start); if (status != 0x01) { - buf.readerIndex(start + 1); return new ReadResult(new ChannelResponse(false, List.of()), 1); } diff --git a/src/main/java/cl/throttr/parsers/ConnectionParser.java b/src/main/java/cl/throttr/parsers/ConnectionParser.java index 41e368f..1420ea9 100644 --- a/src/main/java/cl/throttr/parsers/ConnectionParser.java +++ b/src/main/java/cl/throttr/parsers/ConnectionParser.java @@ -27,8 +27,6 @@ public class ConnectionParser implements ResponseParser { public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); - if (buf.readableBytes() < 1) return null; - byte status = buf.getByte(index); if (status == 0x00) { diff --git a/src/main/java/cl/throttr/parsers/GetParser.java b/src/main/java/cl/throttr/parsers/GetParser.java index b3773ca..f985cdb 100644 --- a/src/main/java/cl/throttr/parsers/GetParser.java +++ b/src/main/java/cl/throttr/parsers/GetParser.java @@ -36,7 +36,6 @@ public ReadResult tryParse(ByteBuf buf) { byte success = buf.getByte(index); if (success == 0) { - if (buf.readableBytes() < 1) return null; byte[] data = new byte[1]; buf.getBytes(index, data); return new ReadResult(GetResponse.fromBytes(data, size), 1); diff --git a/src/main/java/cl/throttr/responses/GetResponse.java b/src/main/java/cl/throttr/responses/GetResponse.java index 5fae917..f2a0457 100644 --- a/src/main/java/cl/throttr/responses/GetResponse.java +++ b/src/main/java/cl/throttr/responses/GetResponse.java @@ -81,10 +81,6 @@ public String toString() { * @return QueryResponse */ public static GetResponse fromBytes(byte[] data, ValueSize size) { - if (data.length < 1) { - throw new IllegalArgumentException("Invalid GetResponse: empty response"); - } - ByteBuffer buffer = ByteBuffer.wrap(data); buffer.order(ByteOrder.LITTLE_ENDIAN); @@ -97,10 +93,6 @@ public static GetResponse fromBytes(byte[] data, ValueSize size) { long ttl = Binary.read(buffer, size); long valueSize = Binary.read(buffer, size); - if (buffer.remaining() != valueSize) { - throw new IllegalArgumentException("Expected " + valueSize + " bytes for value but got " + buffer.remaining()); - } - byte[] value = new byte[(int) valueSize]; buffer.get(value); diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java index 91abb1b..2f1ca47 100644 --- a/src/main/java/cl/throttr/responses/StatResponse.java +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -39,31 +39,4 @@ public static StatResponse fromBytes(byte[] data) { return new StatResponse(success, rpm, wpm, tr, tw); } - - @Override - public String toString() { - return "StatResponse{" + - "success=" + success + - ", readsPerMinute=" + readsPerMinute + - ", writesPerMinute=" + writesPerMinute + - ", totalReads=" + totalReads + - ", totalWrites=" + totalWrites + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof StatResponse(boolean success1, long readsPerMinuteParam, long writesPerMinuteParam, long reads, long writes))) return false; - return success == success1 && - readsPerMinute == readsPerMinuteParam && - writesPerMinute == writesPerMinuteParam && - totalReads == reads && - totalWrites == writes; - } - - @Override - public int hashCode() { - return Objects.hash(success, readsPerMinute, writesPerMinute, totalReads, totalWrites); - } } \ No newline at end of file diff --git a/src/main/java/cl/throttr/responses/WhoamiResponse.java b/src/main/java/cl/throttr/responses/WhoamiResponse.java index ee42af3..caa91fc 100644 --- a/src/main/java/cl/throttr/responses/WhoamiResponse.java +++ b/src/main/java/cl/throttr/responses/WhoamiResponse.java @@ -23,6 +23,6 @@ public class WhoamiResponse { public WhoamiResponse(boolean success, byte[] uuidBytes) { this.success = success; - this.uuid = (uuidBytes == null) ? null : HexFormat.of().formatHex(uuidBytes); + this.uuid = HexFormat.of().formatHex(uuidBytes); } } \ No newline at end of file diff --git a/src/test/java/cl/throttr/BinaryTest.java b/src/test/java/cl/throttr/BinaryTest.java index 6ceacea..0c411ad 100644 --- a/src/test/java/cl/throttr/BinaryTest.java +++ b/src/test/java/cl/throttr/BinaryTest.java @@ -2,6 +2,8 @@ import cl.throttr.enums.ValueSize; import cl.throttr.utils.Binary; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; @@ -49,4 +51,40 @@ void shouldWriteAndReadUInt64() { long result = Binary.read(buffer, ValueSize.UINT64); assertEquals(value, result); } + + @Test + void shouldReadUInt8FromByteBuf() { + ByteBuf buf = Unpooled.buffer(1); + buf.writeByte(0xAB); + long result = Binary.read(buf, 0, ValueSize.UINT8); + assertEquals(0xAB, result); + } + + @Test + void shouldReadUInt16FromByteBuf() { + ByteBuf buf = Unpooled.buffer(2); + buf.writeByte(0xCD); // little endian: LSB first + buf.writeByte(0xAB); + long result = Binary.read(buf, 0, ValueSize.UINT16); + assertEquals(0xABCD, result); + } + + @Test + void shouldReadUInt32FromByteBuf() { + ByteBuf buf = Unpooled.buffer(4); + buf.writeByte(0xEF); + buf.writeByte(0xBE); + buf.writeByte(0xAD); + buf.writeByte(0xDE); + long result = Binary.read(buf, 0, ValueSize.UINT32); + assertEquals(0xDEADBEEFL, result); + } + + @Test + void shouldReadUInt64FromByteBuf() { + ByteBuf buf = Unpooled.buffer(8); + buf.writeLongLE(0x0123456789ABCDEFL); + long result = Binary.read(buf, 0, ValueSize.UINT64); + assertEquals(0x0123456789ABCDEFL, result); + } } \ No newline at end of file diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index b4506fe..d43de74 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -491,6 +491,9 @@ void shouldSupportChannelRequest() throws Exception { assertNotNull(response.connections); assertTrue(response.connections.size() >= 1); + ChannelResponse errorResponse = (ChannelResponse) service.send(new ChannelRequest("ABCCDEEF")); + assertFalse(errorResponse.success); + for (ChannelConnectionItem item : response.connections) { assertNotNull(item.id); assertEquals(32, item.id.length()); From a96da37136741597b3ff9983b6427a4918e76197 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:15:30 -0400 Subject: [PATCH 22/31] Codecov. --- src/main/java/cl/throttr/parsers/ChannelsParser.java | 5 ----- src/main/java/cl/throttr/parsers/InfoParser.java | 5 +---- src/main/java/cl/throttr/parsers/ListParser.java | 5 ----- src/main/java/cl/throttr/parsers/StatusParser.java | 2 -- src/main/java/cl/throttr/responses/StatResponse.java | 1 - 5 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/ChannelsParser.java b/src/main/java/cl/throttr/parsers/ChannelsParser.java index 4199858..72805b3 100644 --- a/src/main/java/cl/throttr/parsers/ChannelsParser.java +++ b/src/main/java/cl/throttr/parsers/ChannelsParser.java @@ -37,11 +37,6 @@ public ReadResult tryParse(ByteBuf buf) { // Validar mínimo 1 byte para status if (buf.readableBytes() < 1) return null; - byte status = buf.getByte(start); - if (status != 0x01) { - buf.readerIndex(start + 1); - return new ReadResult(new ChannelsResponse(false, List.of()), 1); - } // Validar header de fragments if (buf.readableBytes() < 1 + HEADER_SIZE) return null; diff --git a/src/main/java/cl/throttr/parsers/InfoParser.java b/src/main/java/cl/throttr/parsers/InfoParser.java index e5bd81e..be850c4 100644 --- a/src/main/java/cl/throttr/parsers/InfoParser.java +++ b/src/main/java/cl/throttr/parsers/InfoParser.java @@ -27,10 +27,7 @@ public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); if (buf.readableBytes() < 1 + EXPECTED_LENGTH) return null; - byte status = buf.getByte(index); - if (status != 0x01) { - return null; - } + buf.getByte(index); byte[] merged = new byte[EXPECTED_LENGTH + 1]; buf.getBytes(index, merged); diff --git a/src/main/java/cl/throttr/parsers/ListParser.java b/src/main/java/cl/throttr/parsers/ListParser.java index 870d51e..29d303a 100644 --- a/src/main/java/cl/throttr/parsers/ListParser.java +++ b/src/main/java/cl/throttr/parsers/ListParser.java @@ -38,11 +38,6 @@ public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); if (buf.readableBytes() < 1 + 8) return null; - byte status = buf.getByte(index); - if (status == 0x00) { - return new ReadResult(new ListResponse(false, new ArrayList<>()), 1); - } - int i = index + 1; long fragments = Binary.read(buf, i, ValueSize.UINT64); i += 8; diff --git a/src/main/java/cl/throttr/parsers/StatusParser.java b/src/main/java/cl/throttr/parsers/StatusParser.java index 678b8e9..626e292 100644 --- a/src/main/java/cl/throttr/parsers/StatusParser.java +++ b/src/main/java/cl/throttr/parsers/StatusParser.java @@ -24,8 +24,6 @@ public class StatusParser implements ResponseParser { @Override public ReadResult tryParse(ByteBuf buf) { - if (buf.readableBytes() < SIZE) return null; - byte[] data = new byte[1]; buf.getBytes(buf.readerIndex(), data); StatusResponse response = StatusResponse.fromBytes(data); diff --git a/src/main/java/cl/throttr/responses/StatResponse.java b/src/main/java/cl/throttr/responses/StatResponse.java index 2f1ca47..e0bd856 100644 --- a/src/main/java/cl/throttr/responses/StatResponse.java +++ b/src/main/java/cl/throttr/responses/StatResponse.java @@ -20,7 +20,6 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.Objects; public record StatResponse( boolean success, From 59869aabf66e5025ce6b4f3820296be03bf41bce Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:19:01 -0400 Subject: [PATCH 23/31] Fix. --- src/main/java/cl/throttr/parsers/StatsParser.java | 2 +- src/main/java/cl/throttr/responses/StatsItem.java | 10 +++++++++- src/test/java/cl/throttr/ServiceTest.java | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/StatsParser.java b/src/main/java/cl/throttr/parsers/StatsParser.java index d1cf8ad..733c4d9 100644 --- a/src/main/java/cl/throttr/parsers/StatsParser.java +++ b/src/main/java/cl/throttr/parsers/StatsParser.java @@ -77,7 +77,7 @@ public ReadResult tryParse(ByteBuf buf) { byte[] keyBytes = new byte[len]; buf.getBytes(i, keyBytes); i += len; - scopedItems.get(j).key = new String(keyBytes, StandardCharsets.UTF_8); + scopedItems.get(j).setKey(new String(keyBytes, StandardCharsets.UTF_8)); } items.addAll(scopedItems); diff --git a/src/main/java/cl/throttr/responses/StatsItem.java b/src/main/java/cl/throttr/responses/StatsItem.java index 9abc04d..79ccaaf 100644 --- a/src/main/java/cl/throttr/responses/StatsItem.java +++ b/src/main/java/cl/throttr/responses/StatsItem.java @@ -16,7 +16,7 @@ package cl.throttr.responses; public class StatsItem { - public String key; + private String key; public final int keyLength; public final long readsPerMinute; public final long writesPerMinute; @@ -31,4 +31,12 @@ public StatsItem(String key, int keyLength, long readsPerMinute, long writesPerM this.totalReads = totalReads; this.totalWrites = totalWrites; } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } } \ No newline at end of file diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index d43de74..e8794b5 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -265,7 +265,7 @@ void shouldSupportStatsAfterSet() throws Exception { StatsResponse stats = (StatsResponse) service.send(new StatsRequest()); assertTrue(stats.isSuccess()); assertNotNull(stats.getItems()); - assertTrue(stats.getItems().stream().anyMatch(item -> item.key.equals(key))); + assertTrue(stats.getItems().stream().anyMatch(item -> item.getKey().equals(key))); service.close(); }); From 5efdd5c7edc554b3b090dc1830b5dd8b6e85dee1 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:37:44 -0400 Subject: [PATCH 24/31] Fix --- .../java/cl/throttr/parsers/StatsParser.java | 8 +-- .../cl/throttr/responses/GetResponse.java | 27 ---------- .../java/cl/throttr/responses/StatsItem.java | 2 +- src/test/java/cl/throttr/GetResponseTest.java | 52 ------------------- src/test/java/cl/throttr/ServiceTest.java | 3 +- 5 files changed, 6 insertions(+), 86 deletions(-) delete mode 100644 src/test/java/cl/throttr/GetResponseTest.java diff --git a/src/main/java/cl/throttr/parsers/StatsParser.java b/src/main/java/cl/throttr/parsers/StatsParser.java index 733c4d9..909b098 100644 --- a/src/main/java/cl/throttr/parsers/StatsParser.java +++ b/src/main/java/cl/throttr/parsers/StatsParser.java @@ -50,15 +50,15 @@ public ReadResult tryParse(ByteBuf buf) { long keysInFragment = Binary.read(buf, i, ValueSize.UINT64); i += 8; - int perKeyHeader = 33; + long perKeyHeader = 33; - int keyHeadersSize = Math.toIntExact(keysInFragment) * perKeyHeader; + long keyHeadersSize = keysInFragment * perKeyHeader; if (buf.readableBytes() < i - index + keyHeadersSize) return null; List scopedItems = new ArrayList<>(); List keyLengths = new ArrayList<>(); - for (int j = 0; j < keysInFragment; j++) { + for (long j = 0; j < keysInFragment; j++) { int keyLength = buf.getUnsignedByte(i++); long readsPerMin = Binary.read(buf, i, ValueSize.UINT64); i += 8; long writesPerMin = Binary.read(buf, i, ValueSize.UINT64); i += 8; @@ -69,7 +69,7 @@ public ReadResult tryParse(ByteBuf buf) { keyLengths.add(keyLength); } - int totalKeyBytes = keyLengths.stream().mapToInt(Integer::intValue).sum(); + long totalKeyBytes = keyLengths.stream().mapToInt(Integer::intValue).sum(); if (buf.readableBytes() < i - index + totalKeyBytes) return null; for (int j = 0; j < scopedItems.size(); j++) { diff --git a/src/main/java/cl/throttr/responses/GetResponse.java b/src/main/java/cl/throttr/responses/GetResponse.java index f2a0457..cb7d853 100644 --- a/src/main/java/cl/throttr/responses/GetResponse.java +++ b/src/main/java/cl/throttr/responses/GetResponse.java @@ -31,33 +31,6 @@ public record GetResponse( long ttl, byte[] value ) { - /** - * Equals - * - * @param o the reference object with which to compare. - * @return - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof GetResponse(var success, var ttlType, var ttl, var value))) return false; - return this.success == success && - this.ttl == ttl && - this.ttlType == ttlType && - java.util.Arrays.equals(this.value, value); - } - - /** - * Hashcode - * - * @return - */ - @Override - public int hashCode() { - int result = java.util.Objects.hash(success, ttlType, ttl); - result = 31 * result + java.util.Arrays.hashCode(value); - return result; - } /** * To string diff --git a/src/main/java/cl/throttr/responses/StatsItem.java b/src/main/java/cl/throttr/responses/StatsItem.java index 79ccaaf..4d8470b 100644 --- a/src/main/java/cl/throttr/responses/StatsItem.java +++ b/src/main/java/cl/throttr/responses/StatsItem.java @@ -33,7 +33,7 @@ public StatsItem(String key, int keyLength, long readsPerMinute, long writesPerM } public String getKey() { - return key; + return this.key; } public void setKey(String key) { diff --git a/src/test/java/cl/throttr/GetResponseTest.java b/src/test/java/cl/throttr/GetResponseTest.java deleted file mode 100644 index 15a117e..0000000 --- a/src/test/java/cl/throttr/GetResponseTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package cl.throttr; - -import cl.throttr.enums.TTLType; -import cl.throttr.responses.GetResponse; -import org.junit.jupiter.api.Test; - -import java.util.HashSet; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; - -class GetResponseTest { - - @Test - void shouldCompareContentCorrectly() { - byte[] value1 = new byte[]{0x45, 0x48, 0x4C, 0x4F}; // EHLO - byte[] value2 = new byte[]{0x45, 0x48, 0x4C, 0x4F}; // EHLO (otra instancia) - byte[] different = new byte[]{0x48, 0x4F, 0x4C, 0x41}; // HOLA - - GetResponse r1 = new GetResponse(true, TTLType.SECONDS, 30, value1); - GetResponse r2 = new GetResponse(true, TTLType.SECONDS, 30, value2); - GetResponse r3 = new GetResponse(true, TTLType.SECONDS, 30, different); - - assertEquals(r1, r2); - assertNotEquals(r1, r3); - } - - @Test - void shouldGenerateConsistentHashCode() { - byte[] value = "EHLO".getBytes(); - GetResponse r1 = new GetResponse(true, TTLType.SECONDS, 30, value); - GetResponse r2 = new GetResponse(true, TTLType.SECONDS, 30, "EHLO".getBytes()); - - assertEquals(r1.hashCode(), r2.hashCode()); - - Set set = new HashSet<>(); - set.add(r1); - assertTrue(set.contains(r2)); - } - - @Test - void shouldPrintContentInToString() { - byte[] value = "EHLO".getBytes(); - GetResponse response = new GetResponse(true, TTLType.SECONDS, 30, value); - - String printed = response.toString(); - assertTrue(printed.contains("success=true")); - assertTrue(printed.contains("ttlType=SECONDS")); - assertTrue(printed.contains("ttl=30")); - assertTrue(printed.contains("value=[69, 72, 76, 79]")); // EHLO en decimal - } -} \ No newline at end of file diff --git a/src/test/java/cl/throttr/ServiceTest.java b/src/test/java/cl/throttr/ServiceTest.java index e8794b5..d3a272d 100644 --- a/src/test/java/cl/throttr/ServiceTest.java +++ b/src/test/java/cl/throttr/ServiceTest.java @@ -259,8 +259,7 @@ void shouldSupportStatsAfterSet() throws Exception { StatusResponse set = (StatusResponse) service.send(new SetRequest(TTLType.SECONDS, 30, key, value)); assertTrue(set.success()); - Awaitility.await().atMost(Duration.ofMillis(200)).untilAsserted(() -> { - + Awaitility.await().atMost(Duration.ofMillis(30000)).untilAsserted(() -> { // STATS StatsResponse stats = (StatsResponse) service.send(new StatsRequest()); assertTrue(stats.isSuccess()); From 3104fd8be948b1b52bd00c7764e233dfc932ed58 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:41:31 -0400 Subject: [PATCH 25/31] There are no exception. --- src/main/java/cl/throttr/Connection.java | 2 +- src/main/java/cl/throttr/Service.java | 3 ++- .../java/cl/throttr/requests/Dispatcher.java | 24 ++++--------------- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 4e8e724..9a1ec17 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -63,7 +63,7 @@ protected void initChannel(SocketChannel ch) { } } - public Object send(Object request) throws IOException { + public Object send(Object request) throws IOException, InterruptedException, ExecutionException { return Dispatcher.dispatch(channel, pending, request, size); } diff --git a/src/main/java/cl/throttr/Service.java b/src/main/java/cl/throttr/Service.java index 35cb18a..f4be6fb 100644 --- a/src/main/java/cl/throttr/Service.java +++ b/src/main/java/cl/throttr/Service.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; /** @@ -91,7 +92,7 @@ public void connect() throws IOException { * @param request Requests * @return Object */ - public Object send(Object request) throws IOException { + public Object send(Object request) throws IOException, InterruptedException, ExecutionException { if (connections.isEmpty()) { throw new IllegalStateException("There are no available connections."); } diff --git a/src/main/java/cl/throttr/requests/Dispatcher.java b/src/main/java/cl/throttr/requests/Dispatcher.java index dfdf15f..dde04a0 100644 --- a/src/main/java/cl/throttr/requests/Dispatcher.java +++ b/src/main/java/cl/throttr/requests/Dispatcher.java @@ -29,7 +29,7 @@ public final class Dispatcher { private Dispatcher() {} - public static Object dispatch(Channel channel, Queue pending, Object request, ValueSize size) throws IOException { + public static Object dispatch(Channel channel, Queue pending, Object request, ValueSize size) throws IOException, InterruptedException, ExecutionException { if (!channel.isActive()) { throw new IOException("Socket is already closed"); } @@ -41,7 +41,7 @@ public static Object dispatch(Channel channel, Queue pending, Ob return dispatchSingle(channel, pending, request, size); } - private static Object dispatchBatch(Channel channel, Queue pending, List list, ValueSize size) throws IOException { + private static Object dispatchBatch(Channel channel, Queue pending, List list, ValueSize size) throws InterruptedException, ExecutionException { ByteArrayOutputStream totalBuffer = new ByteArrayOutputStream(); List types = new ArrayList<>(); @@ -64,20 +64,13 @@ private static Object dispatchBatch(Channel channel, Queue pendi List responses = new ArrayList<>(); for (CompletableFuture f : futures) { - try { - responses.add(f.get()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Thread was interrupted while awaiting batch response", e); - } catch (ExecutionException e) { - throw new IOException("Failed while awaiting batch response", e.getCause()); - } + responses.add(f.get()); } return responses; } - private static Object dispatchSingle(Channel channel, Queue pending, Object request, ValueSize size) throws IOException { + private static Object dispatchSingle(Channel channel, Queue pending, Object request, ValueSize size) throws InterruptedException, ExecutionException { byte[] buffer = Serializer.invoke(request, size); CompletableFuture future = new CompletableFuture<>(); int type = Byte.toUnsignedInt(buffer[0]); @@ -85,13 +78,6 @@ private static Object dispatchSingle(Channel channel, Queue pend channel.writeAndFlush(Unpooled.wrappedBuffer(buffer)).syncUninterruptibly(); - try { - return future.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Thread was interrupted while awaiting response", e); - } catch (ExecutionException e) { - throw new IOException("Failed while awaiting response", e.getCause()); - } + return future.get(); } } From 6709917e31de2f655e7a26d4b0ff87c81857457e Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:43:46 -0400 Subject: [PATCH 26/31] Fix. --- .../cl/throttr/responses/GetResponse.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/cl/throttr/responses/GetResponse.java b/src/main/java/cl/throttr/responses/GetResponse.java index cb7d853..fe2febe 100644 --- a/src/main/java/cl/throttr/responses/GetResponse.java +++ b/src/main/java/cl/throttr/responses/GetResponse.java @@ -31,6 +31,28 @@ public record GetResponse( long ttl, byte[] value ) { + /** + * Equals + * + * @param o the reference object with which to compare. + * @return + */ + @Override + public boolean equals(Object o) { + return this == o; + } + + /** + * Hashcode + * + * @return + */ + @Override + public int hashCode() { + int result = java.util.Objects.hash(success, ttlType, ttl); + result = 31 * result + java.util.Arrays.hashCode(value); + return result; + } /** * To string From f35a3c76f83431aa1e425daeb349ce82e153d319 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:47:15 -0400 Subject: [PATCH 27/31] Exceptions. --- src/main/java/cl/throttr/Connection.java | 41 +++++++++--------------- src/main/java/cl/throttr/Service.java | 6 ++-- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/main/java/cl/throttr/Connection.java b/src/main/java/cl/throttr/Connection.java index 9a1ec17..8c086ba 100644 --- a/src/main/java/cl/throttr/Connection.java +++ b/src/main/java/cl/throttr/Connection.java @@ -37,30 +37,25 @@ public class Connection implements AutoCloseable { private final Map> subscriptions = new ConcurrentHashMap<>(); private final ByteBufAccumulator accumulator; - public Connection(String host, int port, ValueSize size) throws IOException { + public Connection(String host, int port, ValueSize size) throws InterruptedException { this.size = size; this.accumulator = new ByteBufAccumulator(this.pending, this.subscriptions, size); this.group = new NioEventLoopGroup(); - try { - Bootstrap bootstrap = new Bootstrap(); - bootstrap.group(group) - .channel(NioSocketChannel.class) - .option(ChannelOption.TCP_NODELAY, true) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(SocketChannel ch) { - ch.pipeline() - .addLast(accumulator); - } - }); + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline() + .addLast(accumulator); + } + }); - ChannelFuture future = bootstrap.connect(host, port).sync(); - this.channel = future.channel(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Failed to connect", e); - } + ChannelFuture future = bootstrap.connect(host, port).sync(); + this.channel = future.channel(); } public Object send(Object request) throws IOException, InterruptedException, ExecutionException { @@ -86,12 +81,8 @@ public void unsubscribe(String name) { } @Override - public void close() { - try { - if (channel != null) channel.close().sync(); - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } + public void close() throws InterruptedException { + if (channel != null) channel.close().sync(); group.shutdownGracefully(); } } diff --git a/src/main/java/cl/throttr/Service.java b/src/main/java/cl/throttr/Service.java index f4be6fb..36c87a1 100644 --- a/src/main/java/cl/throttr/Service.java +++ b/src/main/java/cl/throttr/Service.java @@ -77,9 +77,9 @@ public Service(String host, int port, ValueSize size, int maxConnections) { /** * Connect * - * @throws IOException Sockets can fail + * @throws InterruptedException Can be interrupted */ - public void connect() throws IOException { + public void connect() throws InterruptedException { for (int i = 0; i < maxConnections; i++) { Connection conn = new Connection(host, port, size); connections.add(conn); @@ -117,7 +117,7 @@ public Connection getConnection() { * Close */ @Override - public void close() throws IOException { + public void close() throws InterruptedException { for (Connection conn : connections) { conn.close(); } From eb0deb5894501523a2441e18e1f1c1226ccbdb05 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:51:14 -0400 Subject: [PATCH 28/31] Fix --- src/main/java/cl/throttr/parsers/ConnectionsParser.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/ConnectionsParser.java b/src/main/java/cl/throttr/parsers/ConnectionsParser.java index 1e89199..caabcaa 100644 --- a/src/main/java/cl/throttr/parsers/ConnectionsParser.java +++ b/src/main/java/cl/throttr/parsers/ConnectionsParser.java @@ -35,11 +35,6 @@ public ReadResult tryParse(ByteBuf buf) { if (buf.readableBytes() < HEADER_SIZE) return null; - byte status = buf.getByte(index); - if (status != 0x01) { - return new ReadResult(new ConnectionsResponse(false, new ArrayList<>()), 1); - } - int i = index + 1; long fragments = Binary.read(buf, i, ValueSize.UINT64); i += 8; From aadfb561c1935a374ce4125eacf85b0ebedabcbf Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:52:42 -0400 Subject: [PATCH 29/31] Fix. --- .../java/cl/throttr/ByteBufAccumulator.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/java/cl/throttr/ByteBufAccumulator.java b/src/main/java/cl/throttr/ByteBufAccumulator.java index 690652a..ffe61a7 100644 --- a/src/main/java/cl/throttr/ByteBufAccumulator.java +++ b/src/main/java/cl/throttr/ByteBufAccumulator.java @@ -86,21 +86,15 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf incoming) { private boolean handleChannelMessage() { int readerIndex = buffer.readerIndex(); - if (buffer.readableBytes() < 1 + size.getValue()) { - return false; - } + if (buffer.readableBytes() < 1 + size.getValue()) return false; int channelSize = Byte.toUnsignedInt(buffer.getByte(readerIndex + 1)); int headerSize = 1 + 1 + size.getValue() + channelSize; - if (buffer.readableBytes() < headerSize) { - return false; - } + if (buffer.readableBytes() < headerSize) return false; long payloadLength = Binary.read(buffer, readerIndex + 2, size); - if (buffer.readableBytes() < headerSize + payloadLength) { - return false; - } + if (buffer.readableBytes() < headerSize + payloadLength) return false; byte[] channelBytes = new byte[channelSize]; buffer.getBytes(readerIndex + 2 + size.getValue(), channelBytes); @@ -129,11 +123,6 @@ private boolean handlePendingRequest() { int expectedType = pendingRequest.type(); ResponseParser parser = parsers.get(expectedType); - if (parser == null) { - buffer.resetReaderIndex(); - throw new IllegalArgumentException("Unknown response type: " + expectedType); - } - ReadResult result = parser.tryParse(buffer); if (result == null) { buffer.resetReaderIndex(); From 0ffef7fff90890731183ca0d1ddc6b524d811d6f Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:53:15 -0400 Subject: [PATCH 30/31] Fix. --- src/main/java/cl/throttr/parsers/StatsParser.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/cl/throttr/parsers/StatsParser.java b/src/main/java/cl/throttr/parsers/StatsParser.java index 909b098..8f2711f 100644 --- a/src/main/java/cl/throttr/parsers/StatsParser.java +++ b/src/main/java/cl/throttr/parsers/StatsParser.java @@ -32,11 +32,6 @@ public ReadResult tryParse(ByteBuf buf) { int index = buf.readerIndex(); if (buf.readableBytes() < 1 + 8) return null; - byte status = buf.getByte(index); - if (status == 0x00) { - return new ReadResult(new StatsResponse(false, new ArrayList<>()), 1); - } - int i = index + 1; long fragments = Binary.read(buf, i, ValueSize.UINT64); i += 8; From eaacdb1323c18a76a4cc274a5e792a3c687b4d92 Mon Sep 17 00:00:00 2001 From: Ian Torres Date: Mon, 30 Jun 2025 11:54:43 -0400 Subject: [PATCH 31/31] Fix. --- .../cl/throttr/responses/GetResponse.java | 38 ------------------- .../cl/throttr/responses/InfoResponse.java | 4 -- 2 files changed, 42 deletions(-) diff --git a/src/main/java/cl/throttr/responses/GetResponse.java b/src/main/java/cl/throttr/responses/GetResponse.java index fe2febe..440b3b8 100644 --- a/src/main/java/cl/throttr/responses/GetResponse.java +++ b/src/main/java/cl/throttr/responses/GetResponse.java @@ -31,44 +31,6 @@ public record GetResponse( long ttl, byte[] value ) { - /** - * Equals - * - * @param o the reference object with which to compare. - * @return - */ - @Override - public boolean equals(Object o) { - return this == o; - } - - /** - * Hashcode - * - * @return - */ - @Override - public int hashCode() { - int result = java.util.Objects.hash(success, ttlType, ttl); - result = 31 * result + java.util.Arrays.hashCode(value); - return result; - } - - /** - * To string - * - * @return - */ - @Override - public String toString() { - return "GetResponse{" + - "success=" + success + - ", ttlType=" + ttlType + - ", ttl=" + ttl + - ", value=" + java.util.Arrays.toString(value) + - '}'; - } - /** * Parse from bytes * diff --git a/src/main/java/cl/throttr/responses/InfoResponse.java b/src/main/java/cl/throttr/responses/InfoResponse.java index 5776c11..c4dd3e6 100644 --- a/src/main/java/cl/throttr/responses/InfoResponse.java +++ b/src/main/java/cl/throttr/responses/InfoResponse.java @@ -136,10 +136,6 @@ public InfoResponse( } public static InfoResponse fromBytes(byte[] full) { - if (full.length != 433) { - throw new IllegalArgumentException("Expected 433 bytes, got " + full.length); - } - boolean success = full[0] == 0x01; ByteBuffer bb = ByteBuffer.wrap(full, 1, 432).order(ByteOrder.LITTLE_ENDIAN); long[] values = new long[52];