Skip to content

Commit 4ff9c8f

Browse files
committed
Support publishing AMQP 1.0 to Event Exchange
Add new config to have the Exchange Event Plugin optionally publish AMQP 1.0 messages.
1 parent 2795293 commit 4ff9c8f

File tree

8 files changed

+506
-200
lines changed

8 files changed

+506
-200
lines changed

deps/rabbit/src/mc_amqpl.erl

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
-define(AMQP10_FOOTER, <<"x-amqp-1.0-footer">>).
4444
-define(PROTOMOD, rabbit_framing_amqp_0_9_1).
4545
-define(CLASS_ID, 60).
46-
-define(LONGSTR_UTF8_LIMIT, 4096).
4746

4847
-opaque state() :: #content{}.
4948

@@ -682,19 +681,13 @@ wrap(_Type, undefined) ->
682681
wrap(Type, Val) ->
683682
{Type, Val}.
684683

685-
from_091(longstr, V)
686-
when is_binary(V) andalso
687-
byte_size(V) =< ?LONGSTR_UTF8_LIMIT ->
688-
%% if a longstr is longer than 4096 bytes we just assume it is binary
689-
%% it _may_ still be valid utf8 but checking this for every longstr header
690-
%% value is going to be excessively slow
691-
case mc_util:is_utf8_no_null(V) of
684+
from_091(longstr, V) ->
685+
case mc_util:is_utf8_no_null_limited(V) of
692686
true ->
693687
{utf8, V};
694688
false ->
695689
{binary, V}
696690
end;
697-
from_091(longstr, V) -> {binary, V};
698691
from_091(long, V) -> {long, V};
699692
from_091(unsignedbyte, V) -> {ubyte, V};
700693
from_091(short, V) -> {short, V};

deps/rabbit/src/mc_util.erl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
-include("mc.hrl").
44

55
-export([is_valid_shortstr/1,
6+
is_utf8_no_null_limited/1,
67
is_utf8_no_null/1,
78
uuid_to_urn_string/1,
89
urn_string_to_uuid/1,
@@ -12,12 +13,24 @@
1213
is_x_header/1
1314
]).
1415

16+
-define(UTF8_LIMIT, 4096).
17+
1518
-spec is_valid_shortstr(term()) -> boolean().
1619
is_valid_shortstr(Bin) when ?IS_SHORTSTR_LEN(Bin) ->
1720
is_utf8_no_null(Bin);
1821
is_valid_shortstr(_) ->
1922
false.
2023

24+
-spec is_utf8_no_null_limited(term()) -> boolean().
25+
is_utf8_no_null_limited(Bin)
26+
when byte_size(Bin) =< ?UTF8_LIMIT ->
27+
is_utf8_no_null(Bin);
28+
is_utf8_no_null_limited(_Term) ->
29+
%% If longer than 4096 bytes, just assume it's not UTF-8.
30+
%% It _may_ still be valid UTF-8 but checking this
31+
%% on the hot path is going to be excessively slow.
32+
false.
33+
2134
-spec is_utf8_no_null(term()) -> boolean().
2235
is_utf8_no_null(Term) ->
2336
utf8_scan(Term, fun (C) -> C > 0 end).

deps/rabbitmq_event_exchange/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
PROJECT = rabbitmq_event_exchange
22
PROJECT_DESCRIPTION = Event Exchange Type
33

4+
define PROJECT_ENV
5+
[
6+
{protocol, amqp_0_9_1}
7+
]
8+
endef
9+
410
define PROJECT_APP_EXTRA_KEYS
511
{broker_version_requirements, []}
612
endef

deps/rabbitmq_event_exchange/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ the management plugin for stats.
1111

1212
## How it Works
1313

14-
It declares a topic exchange called `amq.rabbitmq.event` **in the default
15-
virtual host**. All events are published to this exchange with routing
14+
It declares a topic exchange called `amq.rabbitmq.event`, by default in the default
15+
virtual host (`/`). All events are published to this exchange with routing
1616
keys like 'exchange.created', 'binding.deleted' etc, so you can
1717
subscribe to only the events you're interested in.
1818

deps/rabbitmq_event_exchange/priv/schema/rabbitmq_event_exchange.schema

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
fun(Conf) ->
66
list_to_binary(cuttlefish:conf_get("event_exchange.vhost", Conf))
77
end}.
8+
9+
{mapping, "event_exchange.protocol", "rabbitmq_event_exchange.protocol", [
10+
{datatype, {enum, [amqp_0_9_1, amqp_1_0]}}
11+
]}.

deps/rabbitmq_event_exchange/src/rabbit_exchange_type_event.erl

Lines changed: 144 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
-include_lib("rabbit_common/include/rabbit.hrl").
1313
-include_lib("rabbit_common/include/rabbit_framing.hrl").
14+
-include_lib("amqp10_common/include/amqp10_framing.hrl").
15+
-include_lib("rabbit/include/mc.hrl").
1416
-include("rabbit_event_exchange.hrl").
1517

1618
-export([register/0, unregister/0]).
@@ -20,8 +22,11 @@
2022

2123
-export([fmt_proplist/1]). %% testing
2224

23-
-record(state, {vhost,
24-
has_any_bindings
25+
-define(APP_NAME, rabbitmq_event_exchange).
26+
27+
-record(state, {protocol :: amqp_0_9_1 | amqp_1_0,
28+
vhost :: rabbit_types:vhost(),
29+
has_any_bindings :: boolean()
2530
}).
2631

2732
-rabbit_boot_step({rabbit_event_exchange,
@@ -65,41 +70,35 @@ exchange(VHost) ->
6570
%%----------------------------------------------------------------------------
6671

6772
init([]) ->
73+
{ok, Protocol} = application:get_env(?APP_NAME, protocol),
6874
VHost = get_vhost(),
6975
X = rabbit_misc:r(VHost, exchange, ?EXCH_NAME),
7076
HasBindings = case rabbit_binding:list_for_source(X) of
71-
[] -> false;
72-
_ -> true
73-
end,
74-
{ok, #state{vhost = VHost,
77+
[] -> false;
78+
_ -> true
79+
end,
80+
{ok, #state{protocol = Protocol,
81+
vhost = VHost,
7582
has_any_bindings = HasBindings}}.
7683

7784
handle_call(_Request, State) -> {ok, not_understood, State}.
7885

79-
handle_event(_, #state{has_any_bindings = false} = State) ->
80-
{ok, State};
81-
handle_event(#event{type = Type,
82-
props = Props,
83-
timestamp = TS,
84-
reference = none}, #state{vhost = VHost} = State) ->
85-
_ = case key(Type) of
86-
ignore -> ok;
87-
Key ->
88-
Props2 = [{<<"timestamp_in_ms">>, TS} | Props],
89-
PBasic = #'P_basic'{delivery_mode = 2,
90-
headers = fmt_proplist(Props2),
91-
%% 0-9-1 says the timestamp is a
92-
%% "64 bit POSIX
93-
%% timestamp". That's second
94-
%% resolution, not millisecond.
95-
timestamp = erlang:convert_time_unit(
96-
TS, milli_seconds, seconds)},
97-
Content = rabbit_basic:build_content(PBasic, <<>>),
98-
XName = exchange(VHost),
99-
{ok, Msg} = mc_amqpl:message(XName, Key, Content),
100-
rabbit_queue_type:publish_at_most_once(XName, Msg)
101-
end,
102-
{ok, State};
86+
handle_event(#event{type = Type,
87+
props = Props,
88+
reference = none,
89+
timestamp = Timestamp},
90+
#state{protocol = Protocol,
91+
vhost = VHost,
92+
has_any_bindings = true} = State) ->
93+
case key(Type) of
94+
ignore ->
95+
{ok, State};
96+
Key ->
97+
XName = exchange(VHost),
98+
Mc = mc_init(Protocol, XName, Key, Props, Timestamp),
99+
_ = rabbit_queue_type:publish_at_most_once(XName, Mc),
100+
{ok, State}
101+
end;
103102
handle_event(_Event, State) ->
104103
{ok, State}.
105104

@@ -207,9 +206,110 @@ key(S) ->
207206
Tokens -> list_to_binary(string:join(Tokens, "."))
208207
end.
209208

209+
get_vhost() ->
210+
case application:get_env(?APP_NAME, vhost) of
211+
undefined ->
212+
{ok, V} = application:get_env(rabbit, default_vhost),
213+
V;
214+
{ok, V} ->
215+
V
216+
end.
217+
218+
mc_init(amqp_1_0, XName, Key, Props, Timestamp) ->
219+
Sections = [#'v1_0.message_annotations'{content = props_to_message_annotations(Props)},
220+
#'v1_0.properties'{creation_time = {timestamp, Timestamp}},
221+
#'v1_0.data'{content = <<>>}],
222+
Payload = iolist_to_binary([amqp10_framing:encode_bin(S) || S <- Sections]),
223+
Anns = #{?ANN_EXCHANGE => XName#resource.name,
224+
?ANN_ROUTING_KEYS => [Key]},
225+
mc:init(mc_amqp, Payload, Anns);
226+
mc_init(amqp_0_9_1, XName, Key, Props0, TimestampMillis) ->
227+
Props = [{<<"timestamp_in_ms">>, TimestampMillis} | Props0],
228+
Headers = fmt_proplist(Props),
229+
TimestampSecs = erlang:convert_time_unit(TimestampMillis, millisecond, second),
230+
PBasic = #'P_basic'{delivery_mode = 2,
231+
headers = Headers,
232+
timestamp = TimestampSecs},
233+
Content = rabbit_basic:build_content(PBasic, <<>>),
234+
{ok, Mc} = mc_amqpl:message(XName, Key, Content),
235+
Mc.
236+
237+
props_to_message_annotations(Props) ->
238+
KVList = lists:foldl(
239+
fun({K, #resource{virtual_host = Vhost, name = Name}}, Acc) ->
240+
Ann0 = {to_message_annotation_key(K), {utf8, Name}},
241+
Ann1 = {{symbol, <<"x-opt-vhost">>}, {utf8, Vhost}},
242+
[Ann0, Ann1 | Acc];
243+
({K, V}, Acc) ->
244+
Ann = {to_message_annotation_key(K),
245+
to_message_annotation_val(V)},
246+
[Ann | Acc]
247+
end, [], Props),
248+
lists:reverse(KVList).
249+
250+
to_message_annotation_key(Key) ->
251+
Key1 = to_binary(Key),
252+
Pattern = try persistent_term:get(cp_underscore)
253+
catch error:badarg ->
254+
Cp = binary:compile_pattern(<<"_">>),
255+
ok = persistent_term:put(cp_underscore, Cp),
256+
Cp
257+
end,
258+
Key2 = binary:replace(Key1, Pattern, <<"-">>, [global]),
259+
Key3 = case Key2 of
260+
<<"x-", _/binary>> ->
261+
Key2;
262+
_ ->
263+
<<"x-opt-", Key2/binary>>
264+
end,
265+
{symbol, Key3}.
266+
267+
to_message_annotation_val(V)
268+
when is_binary(V) ->
269+
case mc_util:is_utf8_no_null_limited(V) of
270+
true ->
271+
{utf8, V};
272+
false ->
273+
{binary, V}
274+
end;
275+
to_message_annotation_val(V)
276+
when is_integer(V) ->
277+
{long, V};
278+
to_message_annotation_val(V)
279+
when is_number(V) ->
280+
%% AMQP double and Erlang float are both 64-bit.
281+
{double, V};
282+
to_message_annotation_val(V)
283+
when is_boolean(V) ->
284+
{boolean, V};
285+
to_message_annotation_val(V)
286+
when is_pid(V) ->
287+
{utf8, to_pid(V)};
288+
to_message_annotation_val(V)
289+
when is_atom(V) ->
290+
{utf8, atom_to_binary(V, utf8)};
291+
to_message_annotation_val([{Key, _} | _] = Proplist)
292+
when is_atom(Key) orelse
293+
is_binary(Key) ->
294+
Map = lists:map(fun({K, V}) ->
295+
{{utf8, to_binary(K)},
296+
to_message_annotation_val(V)}
297+
end, Proplist),
298+
{map, Map};
299+
to_message_annotation_val([{Key, Type, _} | _] = Table)
300+
when is_binary(Key) andalso
301+
is_atom(Type) ->
302+
%% Looks like an AMQP 0.9.1 table
303+
mc_amqpl:from_091(table, Table);
304+
to_message_annotation_val(V)
305+
when is_list(V) ->
306+
{list, [to_message_annotation_val(Val) || Val <- V]};
307+
to_message_annotation_val(V) ->
308+
{utf8, fmt_other(V)}.
309+
210310
fmt_proplist(Props) ->
211311
lists:foldl(fun({K, V}, Acc) ->
212-
case fmt(a2b(K), V) of
312+
case fmt(to_binary(K), V) of
213313
L when is_list(L) -> lists:append(L, Acc);
214314
T -> [T | Acc]
215315
end
@@ -226,11 +326,8 @@ fmt(K, V) when is_number(V) -> {K, float, V};
226326
fmt(K, V) when is_binary(V) -> {K, longstr, V};
227327
fmt(K, [{_, _}|_] = Vs) -> {K, table, fmt_proplist(Vs)};
228328
fmt(K, Vs) when is_list(Vs) -> {K, array, [fmt(V) || V <- Vs]};
229-
fmt(K, V) when is_pid(V) -> {K, longstr,
230-
list_to_binary(rabbit_misc:pid_to_string(V))};
231-
fmt(K, V) -> {K, longstr,
232-
list_to_binary(
233-
rabbit_misc:format("~1000000000p", [V]))}.
329+
fmt(K, V) when is_pid(V) -> {K, longstr, to_pid(V)};
330+
fmt(K, V) -> {K, longstr, fmt_other(V)}.
234331

235332
%% Exactly the same as fmt/2, duplicated only for performance issues
236333
fmt(true) -> {bool, true};
@@ -241,20 +338,16 @@ fmt(V) when is_number(V) -> {float, V};
241338
fmt(V) when is_binary(V) -> {longstr, V};
242339
fmt([{_, _}|_] = Vs) -> {table, fmt_proplist(Vs)};
243340
fmt(Vs) when is_list(Vs) -> {array, [fmt(V) || V <- Vs]};
244-
fmt(V) when is_pid(V) -> {longstr,
245-
list_to_binary(rabbit_misc:pid_to_string(V))};
246-
fmt(V) -> {longstr,
247-
list_to_binary(
248-
rabbit_misc:format("~1000000000p", [V]))}.
341+
fmt(V) when is_pid(V) -> {longstr, to_pid(V)};
342+
fmt(V) -> {longstr, fmt_other(V)}.
249343

250-
a2b(A) when is_atom(A) -> atom_to_binary(A, utf8);
251-
a2b(B) when is_binary(B) -> B.
344+
fmt_other(V) ->
345+
list_to_binary(rabbit_misc:format("~1000000000p", [V])).
252346

253-
get_vhost() ->
254-
case application:get_env(rabbitmq_event_exchange, vhost) of
255-
undefined ->
256-
{ok, V} = application:get_env(rabbit, default_vhost),
257-
V;
258-
{ok, V} ->
259-
V
260-
end.
347+
to_binary(Val) when is_atom(Val) ->
348+
atom_to_binary(Val);
349+
to_binary(Val) when is_binary(Val) ->
350+
Val.
351+
352+
to_pid(Val) ->
353+
list_to_binary(rabbit_misc:pid_to_string(Val)).
Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
[
2-
{virtual_host1,
3-
"event_exchange.vhost = /",
4-
[
5-
{rabbitmq_event_exchange, [
6-
{vhost, <<"/">>}
7-
]}
8-
], [rabbitmq_event_exchange]
9-
},
2+
{virtual_host1,
3+
"event_exchange.vhost = /",
4+
[{rabbitmq_event_exchange, [
5+
{vhost, <<"/">>}
6+
]}],
7+
[rabbitmq_event_exchange]
8+
},
109

11-
{virtual_host2,
12-
"event_exchange.vhost = dev",
13-
[
14-
{rabbitmq_event_exchange, [
15-
{vhost, <<"dev">>}
16-
]}
17-
], [rabbitmq_event_exchange]
18-
}
10+
{virtual_host2,
11+
"event_exchange.vhost = dev",
12+
[{rabbitmq_event_exchange, [
13+
{vhost, <<"dev">>}
14+
]}
15+
],
16+
[rabbitmq_event_exchange]
17+
},
18+
19+
{protocol_amqp,
20+
"event_exchange.protocol = amqp_1_0",
21+
[{rabbitmq_event_exchange, [
22+
{protocol, amqp_1_0}
23+
]}],
24+
[rabbitmq_event_exchange]
25+
},
26+
27+
{protocol_amqpl,
28+
"event_exchange.protocol = amqp_0_9_1",
29+
[{rabbitmq_event_exchange, [
30+
{protocol, amqp_0_9_1}
31+
]}],
32+
[rabbitmq_event_exchange]
33+
}
1934
].

0 commit comments

Comments
 (0)