Skip to content

Commit 2311d30

Browse files
authored
Merge pull request #15381 from rabbitmq/stream-resolve-offset-spec
Add resolve_stream_offset stream protocol command
2 parents 76840ab + 4957f40 commit 2311d30

8 files changed

Lines changed: 269 additions & 23 deletions

File tree

deps/rabbitmq_stream/docs/PROTOCOL.adoc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ used to make the difference between a request (0) and a response (1). Example fo
240240
|0x001e
241241
|Yes
242242

243+
|<<resolveoffsetspec>>
244+
|Client
245+
|0x001f
246+
|Yes
247+
243248
|===
244249

245250
=== DeclarePublisher
@@ -795,6 +800,43 @@ Delete => Key Version CorrelationId Name
795800
Name => string
796801
```
797802

803+
=== ResolveOffsetSpec
804+
805+
```
806+
ResolveOffsetSpecRequest => Key Version CorrelationId Stream OffsetSpecification Properties
807+
Key => uint16 // 0x001f
808+
Version => uint16
809+
CorrelationId => uint32
810+
Stream => string
811+
OffsetSpecification => OffsetType Offset
812+
OffsetType => uint16 // 1 (first), 2 (last), 3 (next), 4 (offset), 5 (timestamp)
813+
Offset => uint64 (for offset) | int64 (for timestamp)
814+
Properties => [Property]
815+
Property => Key Value
816+
Key => string
817+
Value => string
818+
819+
ResolveOffsetSpecResponse => Key Version CorrelationId ResponseCode OffsetType Offset
820+
Key => uint16 // 0x801f
821+
Version => uint16
822+
CorrelationId => uint32
823+
ResponseCode => uint16
824+
OffsetType => uint16 // 4 (offset)
825+
Offset => uint64
826+
```
827+
828+
Resolves an offset specification to a concrete offset in a stream.
829+
The client sends an offset specification (first, last, next, a specific offset, or a timestamp) and the server resolves it to the actual offset in the stream.
830+
831+
The response includes the type of the resolved value and the value itself.
832+
Currently, the resolved value type is always `offset` (4).
833+
834+
Supported request properties:
835+
836+
* `filter.` (e.g. `filter.0`, `filter.1`, etc): prefix to use to define filter values
837+
* `match-unfiltered`: whether to consider messages without any filter value or not
838+
* `chunk_selector`: chunk selector to use for offset resolution
839+
798840
== Authentication
799841

800842
Once a client is connected to the server, it initiates an authentication

deps/rabbitmq_stream/src/rabbit_stream_reader.erl

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2123,6 +2123,50 @@ handle_frame_post_auth(Transport,
21232123
{query_offset, ResponseCode, Offset}}),
21242124
send(Transport, S, Frame),
21252125
{Connection1, State};
2126+
handle_frame_post_auth(Transport,
2127+
#stream_connection{socket = S,
2128+
virtual_host = VirtualHost,
2129+
user = User} =
2130+
Connection0,
2131+
State,
2132+
{request, CorrelationId,
2133+
{resolve_offset_spec, Stream, OffsetSpec, Properties}}) ->
2134+
{ResponseCode, Offset, Connection1} =
2135+
case rabbit_stream_utils:check_read_permitted(#resource{name = Stream,
2136+
kind = queue,
2137+
virtual_host =
2138+
VirtualHost},
2139+
User, #{})
2140+
of
2141+
ok ->
2142+
case rabbit_stream_manager:lookup_member(VirtualHost, Stream) of
2143+
{error, not_found} ->
2144+
increase_protocol_counter(?STREAM_DOES_NOT_EXIST),
2145+
{?RESPONSE_CODE_STREAM_DOES_NOT_EXIST, 0, Connection0};
2146+
{error, not_available} ->
2147+
increase_protocol_counter(?STREAM_NOT_AVAILABLE),
2148+
{?RESPONSE_CODE_STREAM_NOT_AVAILABLE, 0, Connection0};
2149+
{ok, MemberPid} ->
2150+
Opts0 = #{chunk_selector => get_chunk_selector(Properties)},
2151+
Opts1 = maps:merge(Opts0,
2152+
rabbit_stream_utils:filter_spec(Properties)),
2153+
case resolve_offset_spec(MemberPid, OffsetSpec, Opts1) of
2154+
{ok, ResolvedOffset} ->
2155+
{?RESPONSE_CODE_OK, ResolvedOffset, Connection0};
2156+
{error, _} ->
2157+
{?RESPONSE_CODE_NO_OFFSET, 0, Connection0}
2158+
end
2159+
end;
2160+
error ->
2161+
increase_protocol_counter(?ACCESS_REFUSED),
2162+
{?RESPONSE_CODE_ACCESS_REFUSED, 0, Connection0}
2163+
end,
2164+
Frame =
2165+
rabbit_stream_core:frame({response, CorrelationId,
2166+
{resolve_offset_spec, ResponseCode,
2167+
?OFFSET_TYPE_OFFSET, Offset}}),
2168+
send(Transport, S, Frame),
2169+
{Connection1, State};
21262170
handle_frame_post_auth(Transport,
21272171
#stream_connection{stream_subscriptions =
21282172
StreamSubscriptions} =
@@ -2821,7 +2865,7 @@ init_reader(ConnectionTransport,
28212865
{ok, Segment} = osiris:init_reader(LocalMemberPid, OffsetSpec,
28222866
CounterSpec, Options1),
28232867
?LOG_DEBUG("Next offset for subscription ~tp is ~tp",
2824-
[SubscriptionId, osiris_log:next_offset(Segment)]),
2868+
[SubscriptionId, osiris_log:next_offset(Segment)]),
28252869
Segment.
28262870

28272871
single_active_consumer(#consumer{configuration =
@@ -3995,6 +4039,19 @@ i(connected_at, #stream_connection{connected_at = T}, _) ->
39954039
i(_Unknown, _, _) ->
39964040
?UNKNOWN_FIELD.
39974041

4042+
resolve_offset_spec(MemberPid, OffsetSpec, Options) ->
4043+
case node(MemberPid) of
4044+
Node when Node =:= node() ->
4045+
osiris:resolve_offset_spec(MemberPid, OffsetSpec, Options);
4046+
Node ->
4047+
try erpc:call(Node, osiris, resolve_offset_spec,
4048+
[MemberPid, OffsetSpec, Options])
4049+
catch
4050+
error:{erpc, _} ->
4051+
{error, erpc_error}
4052+
end
4053+
end.
4054+
39984055
-spec send(module(), rabbit_net:socket(), iodata()) -> ok.
39994056
send(Transport, Socket, Data) when is_atom(Transport) ->
40004057
Transport:send(Socket, Data).

deps/rabbitmq_stream/src/rabbit_stream_utils.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,8 @@ command_versions() ->
316316
{partitions, ?VERSION_1, ?VERSION_1},
317317
{stream_stats, ?VERSION_1, ?VERSION_1},
318318
{create_super_stream, ?VERSION_1, ?VERSION_1},
319-
{delete_super_stream, ?VERSION_1, ?VERSION_1}].
319+
{delete_super_stream, ?VERSION_1, ?VERSION_1},
320+
{resolve_offset_spec, ?VERSION_1, ?VERSION_1}].
320321

321322
q(VirtualHost, Name) ->
322323
rabbit_misc:r(VirtualHost, queue, Name).

deps/rabbitmq_stream/test/rabbit_stream_SUITE.erl

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,14 @@ groups() ->
7676
test_stream_test_utils,
7777
sac_subscription_with_partition_index_conflict_should_return_error,
7878
test_metadata_with_advertised_hints,
79-
test_connection_properties_with_advertised_hints
79+
test_connection_properties_with_advertised_hints,
80+
test_resolve_offset_spec
8081
]},
8182
%% Run `test_global_counters` on its own so the global metrics are
8283
%% initialised to 0 for each testcase
8384
{single_node_1, [], [test_global_counters]},
84-
{cluster, [], [test_stream, test_stream_tls, test_metadata, java]}].
85+
{cluster, [], [test_stream, test_stream_tls, test_metadata, java,
86+
test_resolve_offset_spec]}].
8587

8688
init_per_suite(Config) ->
8789
case rabbit_ct_helpers:is_mixed_versions() of
@@ -1237,6 +1239,56 @@ test_connection_properties_with_advertised_hints(Config) ->
12371239

12381240
ok.
12391241

1242+
test_resolve_offset_spec(Config) ->
1243+
Stream = atom_to_binary(?FUNCTION_NAME, utf8),
1244+
Transport = gen_tcp,
1245+
Port = get_stream_port(Config),
1246+
Opts = [{active, false}, {mode, binary}],
1247+
{ok, S} = Transport:connect("localhost", Port, Opts),
1248+
C0 = rabbit_stream_core:init(0),
1249+
C1 = test_peer_properties(Transport, S, C0),
1250+
C2 = test_authenticate(Transport, S, C1),
1251+
C3 = test_create_stream(Transport, S, Stream, C2),
1252+
1253+
%% Test resolve_offset_spec on empty stream
1254+
C4 = test_resolve_offset_spec(Transport, S, Stream, first, #{},
1255+
?RESPONSE_CODE_OK, 0, C3),
1256+
C5 = test_resolve_offset_spec(Transport, S, Stream, last, #{},
1257+
?RESPONSE_CODE_OK, 0, C4),
1258+
C6 = test_resolve_offset_spec(Transport, S, Stream, next, #{},
1259+
?RESPONSE_CODE_OK, 0, C5),
1260+
1261+
%% Publish some messages
1262+
PublisherId = 1,
1263+
C7 = test_declare_publisher(Transport, S, PublisherId, Stream, C6),
1264+
Body = <<"hello">>,
1265+
C8 = test_publish_confirm(Transport, S, PublisherId, 1, Body, C7),
1266+
C9 = test_publish_confirm(Transport, S, PublisherId, 2, Body, C8),
1267+
1268+
%% Test resolve_offset_spec after publishing
1269+
C10 = test_resolve_offset_spec(Transport, S, Stream, first, #{},
1270+
?RESPONSE_CODE_OK, 0, C9),
1271+
C11 = test_resolve_offset_spec(Transport, S, Stream, last, #{},
1272+
?RESPONSE_CODE_OK, C10),
1273+
C12 = test_resolve_offset_spec(Transport, S, Stream, next, #{},
1274+
?RESPONSE_CODE_OK, 2, C11),
1275+
C13 = test_resolve_offset_spec(Transport, S, Stream, 0, #{},
1276+
?RESPONSE_CODE_OK, 0, C12),
1277+
1278+
%% Test with timestamp (far future should return next offset)
1279+
FutureTimestamp = os:system_time(millisecond) + 3600000,
1280+
C14 = test_resolve_offset_spec(Transport, S, Stream, {timestamp, FutureTimestamp}, #{},
1281+
?RESPONSE_CODE_OK, 2, C13),
1282+
1283+
%% Test on non-existent stream
1284+
C15 = test_resolve_offset_spec(Transport, S, <<"non_existent_stream">>, first, #{},
1285+
?RESPONSE_CODE_STREAM_DOES_NOT_EXIST, 0, C14),
1286+
1287+
C16 = test_delete_stream(Transport, S, Stream, C15),
1288+
_C17 = test_close(Transport, S, C16),
1289+
closed = wait_for_socket_close(Transport, S, 10),
1290+
ok.
1291+
12401292
filtered_events(Config, EventType) ->
12411293
Events = rabbit_ct_broker_helpers:rpc(Config, 0,
12421294
gen_event,
@@ -1730,6 +1782,26 @@ test_stream_stats(Transport, S, Stream, C0) ->
17301782
Cmd),
17311783
C.
17321784

1785+
test_resolve_offset_spec(Transport, S, Stream, OffsetSpec, Properties,
1786+
ExpectedResponseCode, C0) ->
1787+
Frame = request({resolve_offset_spec, Stream, OffsetSpec, Properties}),
1788+
ok = Transport:send(S, Frame),
1789+
{Cmd, C} = receive_commands(Transport, S, C0),
1790+
?assertMatch({response, 1, {resolve_offset_spec, ExpectedResponseCode,
1791+
?OFFSET_TYPE_OFFSET, _}},
1792+
Cmd),
1793+
C.
1794+
1795+
test_resolve_offset_spec(Transport, S, Stream, OffsetSpec, Properties,
1796+
ExpectedResponseCode, ExpectedOffset, C0) ->
1797+
Frame = request({resolve_offset_spec, Stream, OffsetSpec, Properties}),
1798+
ok = Transport:send(S, Frame),
1799+
{Cmd, C} = receive_commands(Transport, S, C0),
1800+
?assertMatch({response, 1, {resolve_offset_spec, ExpectedResponseCode,
1801+
?OFFSET_TYPE_OFFSET, ExpectedOffset}},
1802+
Cmd),
1803+
C.
1804+
17331805
test_close(Transport, S, C0) ->
17341806
CloseReason = <<"OK">>,
17351807
CloseFrame = request({close, ?RESPONSE_CODE_OK, CloseReason}),

deps/rabbitmq_stream_common/include/rabbit_stream.hrl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
-define(COMMAND_STREAM_STATS, 28).
4545
-define(COMMAND_CREATE_SUPER_STREAM, 29).
4646
-define(COMMAND_DELETE_SUPER_STREAM, 30).
47+
-define(COMMAND_RESOLVE_OFFSET_SPEC, 31).
4748

4849
-define(REQUEST, 0).
4950
-define(RESPONSE, 1).

0 commit comments

Comments
 (0)