Support AMQP 1.0 natively

## What

Similar to Native MQTT in #5895, this commits implements Native AMQP 1.0.
By "native", we mean do not proxy via AMQP 0.9.1 anymore.

  ## Why

Native AMQP 1.0 comes with the following major benefits:
1. Similar to Native MQTT, this commit provides better throughput, latency,
   scalability, and resource usage for AMQP 1.0.
   See https://blog.rabbitmq.com/posts/2023/03/native-mqtt for native MQTT improvements.
   See further below for some benchmarks.
2. Since AMQP 1.0 is not limited anymore by the AMQP 0.9.1 protocol,
   this commit allows implementing more AMQP 1.0 features in the future.
   Some features are already implemented in this commit (see next section).
3. Simpler, better understandable, and more maintainable code.

Native AMQP 1.0 as implemented in this commit has the
following major benefits compared to AMQP 0.9.1:
4. Memory and disk alarms will only stop accepting incoming TRANSFER frames.
   New connections can still be created to consume from RabbitMQ to empty queues.
5. Due to 4. no need anymore for separate connections for publishers and
   consumers as we currently recommended for AMQP 0.9.1. which potentially
   halves the number of physical TCP connections.
6. When a single connection sends to multiple target queues, a single
   slow target queue won't block the entire connection.
   Publisher can still send data quickly to all other target queues.
7. A publisher can request whether it wants publisher confirmation on a per-message basis.
   In AMQP 0.9.1 publisher confirms are configured per channel only.
8. Consumers can change their "prefetch count" dynamically which isn't
   possible in our AMQP 0.9.1 implementation. See #10174
9. AMQP 1.0 is an extensible protocol

This commit also fixes dozens of bugs present in the AMQP 1.0 plugin in
RabbitMQ 3.x - most of which cannot be backported due to the complexity
and limitations of the old 3.x implementation.

This commit contains breaking changes and is therefore targeted for RabbitMQ 4.0.

 ## Implementation details

1. Breaking change: With Native AMQP, the behaviour of
```
Convert AMQP 0.9.1 message headers to application properties for an AMQP 1.0 consumer
amqp1_0.convert_amqp091_headers_to_app_props = false | true (default false)
Convert AMQP 1.0 Application Properties to AMQP 0.9.1 headers
amqp1_0.convert_app_props_to_amqp091_headers = false | true (default false)
```
will break because we always convert according to the message container conversions.
For example, AMQP 0.9.1 x-headers will go into message-annotations instead of application properties.
Also, `false` won’t be respected since we always convert the headers with message containers.

2. Remove rabbit_queue_collector

rabbit_queue_collector is responsible for synchronously deleting
exclusive queues. Since the AMQP 1.0 plugin never creates exclusive
queues, rabbit_queue_collector doesn't need to be started in the first
place. This will save 1 Erlang process per AMQP 1.0 connection.

3. 7 processes per connection + 1 process per session in this commit instead of
   7 processes per connection + 15 processes per session in 3.x
Supervision hierarchy got re-designed.

4. Use 1 writer process per AMQP 1.0 connection
AMQP 0.9.1 uses a separate rabbit_writer Erlang process per AMQP 0.9.1 channel.
Prior to this commit, AMQP 1.0 used a separate rabbit_amqp1_0_writer process per AMQP 1.0 session.
Advantage of single writer proc per session (prior to this commit):
* High parallelism for serialising packets if multiple sessions within
  a connection write heavily at the same time.

This commit uses a single writer process per AMQP 1.0 connection that is
shared across all AMQP 1.0 sessions.
Advantages of single writer proc per connection (this commit):
* Lower memory usage with hundreds of thousands of AMQP 1.0 sessions
* Less TCP and IP header overhead given that the single writer process
  can accumulate across all sessions bytes before flushing the socket.

In other words, this commit decides that a reader / writer process pair
per AMQP 1.0 connection is good enough for bi-directional TRANSFER flows.
Having a writer per session is too heavy.
We still ensure high throughput by having separate reader, writer, and
session processes.

5. Transform rabbit_amqp1_0_writer into gen_server
Why:
Prior to this commit, when clicking on the AMQP 1.0 writer process in
observer, the process crashed.
Instead of handling all these debug messages of the sys module, it's better
to implement a gen_server.
There is no advantage of using a special OTP process over gen_server
for the AMQP 1.0 writer.
gen_server also provides cleaner format status output.

How:
Message callbacks return a timeout of 0.
After all messages in the inbox are processed, the timeout message is
handled by flushing any pending bytes.

6. Remove stats timer from writer
AMQP 1.0 connections haven't emitted any stats previously.

7. When there are contiguous queue confirmations in the session process
mailbox, batch them. When the confirmations are sent to the publisher, a
single DISPOSITION frame is sent for contiguously confirmed delivery
IDs.
This approach should be good enough. However it's sub optimal in
scenarios where contiguous delivery IDs that need confirmations are rare,
for example:
* There are multiple links in the session with different sender
  settlement modes and sender publishes across these links interleaved.
* sender settlement mode is mixed and sender publishes interleaved settled
  and unsettled TRANSFERs.

8. Introduce credit API v2
Why:
The AMQP 0.9.1 credit extension which is to be removed in 4.0 was poorly
designed since basic.credit is a synchronous call into the queue process
blocking the entire AMQP 1.0 session process.

How:
Change the interactions between queue clients and queue server
implementations:
* Clients only request a credit reply if the FLOW's `echo` field is set
* Include all link flow control state held by the queue process into a
  new credit_reply queue event:
  * `available` after the queue sends any deliveries
  * `link-credit` after the queue sends any deliveries
  * `drain` which allows us to combine the old queue events
    send_credit_reply and send_drained into a single new queue event
    credit_reply.
* Include the consumer tag into the credit_reply queue event such that
  the AMQP 1.0 session process can process any credit replies
  asynchronously.

Link flow control state `delivery-count` also moves to the queue processes.

The new interactions are hidden behind feature flag credit_api_v2 to
allow for rolling upgrades from 3.13 to 4.0.

9. Use serial number arithmetic in quorum queues and session process.

10. Completely bypass the rabbit_limiter module for AMQP 1.0
flow control. The goal is to eventually remove the rabbit_limiter module
in 4.0 since AMQP 0.9.1 global QoS will be unsupported in 4.0. This
commit lifts the AMQP 1.0 link flow control logic out of rabbit_limiter
into rabbit_queue_consumers.

11. Fix credit bug for streams:
AMQP 1.0 settlements shouldn't top up link credit,
only FLOW frames should top up link credit.

12. Allow sender settle mode unsettled for streams
since AMQP 1.0 acknowledgements to streams are no-ops (currently).

13. Fix AMQP 1.0 client bugs
Auto renewing credits should not be related to settling TRANSFERs.
Remove field link_credit_unsettled as it was wrong and confusing.
Prior to this commit auto renewal did not work when the sender uses
sender settlement mode settled.

14. Fix AMQP 1.0 client bugs
The wrong outdated Link was passed to function auto_flow/2

15. Use osiris chunk iterator
Only hold messages of uncompressed sub batches in memory if consumer
doesn't have sufficient credits.
Compressed sub batches are skipped for non Stream protocol consumers.

16. Fix incoming link flow control
Always use confirms between AMQP 1.0 queue clients and queue servers.
As already done internally by rabbit_fifo_client and
rabbit_stream_queue, use confirms for classic queues as well.

17. Include link handle into correlation when publishing messages to target queues
such that session process can correlate confirms from target queues to
incoming links.

18. Only grant more credits to publishers if publisher hasn't sufficient credits
anymore and there are not too many unconfirmed messages on the link.

19. Completely ignore `block` and `unblock` queue actions and RabbitMQ credit flow
between classic queue process and session process.

20. Link flow control is independent between links.
A client can refer to a queue or to an exchange with multiple
dynamically added target queues. Multiple incoming links can also fan
in to the same queue. However the link topology looks like, this
commit ensures that each link is only granted more credits if that link
isn't overloaded.

21. A connection or a session can send to many different queues.
In AMQP 0.9.1, a single slow queue will lead to the entire channel, and
then entire connection being blocked.
This commit makes sure that a single slow queue from one link won't slow
down sending on other links.
For example, having link A sending to a local classic queue and
link B sending to 5 replica quorum queue, link B will naturally
grant credits slower than link A. So, despite the quorum queue being
slower in confirming messages, the same AMQP 1.0 connection and session
can still pump data very fast into the classic queue.

22. If cluster wide memory or disk alarm occurs.
Each session sends a FLOW with incoming-window to 0 to sending client.
If sending clients don’t obey, force disconnect the client.

If cluster wide memory alarm clears:
Each session resumes with a FLOW defaulting to initial incoming-window.

23. All operations apart of publishing TRANSFERS to RabbitMQ can continue during cluster wide alarms,
specifically, attaching consumers and consuming, i.e. emptying queues.
There is no need for separate AMQP 1.0 connections for publishers and consumers as recommended in our AMQP 0.9.1 implementation.

24. Flow control summary:
* If queue becomes bottleneck, that’s solved by slowing down individual sending links (AMQP 1.0 link flow control).
* If session becomes bottleneck (more unlikely), that’s solved by AMQP 1.0 session flow control.
* If connection becomes bottleneck, it naturally won’t read fast enough from the socket causing TCP backpressure being applied.
Nowhere will RabbitMQ internal credit based flow control (i.e. module credit_flow) be used on the incoming AMQP 1.0 message path.

25. Register AMQP sessions
Prefer local-only pg over our custom pg_local implementation as
pg is a better process group implementation than pg_local.
pg_local was identified as bottleneck in tests where many MQTT clients were disconnected at once.

26. Start a local-only pg when Rabbit boots:
> A scope can be kept local-only by using a scope name that is unique cluster-wide, e.g. the node name:
> pg:start_link(node()).
Register AMQP 1.0 connections and sessions with pg.

In future we should remove pg_local and instead use the new local-only
pg for all registered processes such as AMQP 0.9.1 connections and channels.

27. Requeue messages if link detached
Although the spec allows to settle delivery IDs on detached links, RabbitMQ does not respect the 'closed'
field of the DETACH frame and therefore handles every DETACH frame as closed. Since the link is closed,
we expect every outstanding delivery to be requeued.
In addition to consumer cancellation, detaching a link therefore causes in flight deliveries to be requeued.
Note that this behaviour is different from merely consumer cancellation in AMQP 0.9.1:
"After a consumer is cancelled there will be no future deliveries dispatched to it. Note that there can
still be "in flight" deliveries dispatched previously. Cancelling a consumer will neither discard nor requeue them."
[https://www.rabbitmq.com/consumers.html#unsubscribing]
An AMQP receiver can first drain, and then detach to prevent "in flight" deliveries

28. Init AMQP session with BEGIN frame
Similar to how there can't be an MQTT processor without a CONNECT
frame, there can't be an AMQP session without a BEGIN frame.
This allows having strict dialyzer types for session flow control
fields (i.e. not allowing 'undefined').

29. Move serial_number to AMQP 1.0 common lib
such that it can be used by both AMQP 1.0 server and client

30. Fix AMQP client to do serial number arithmetic.

31. AMQP client: Differentiate between delivery-id and transfer-id for better
understandability.

32. Fix link flow control in classic queues
This commit fixes
```
java -jar target/perf-test.jar -ad false -f persistent -u cq -c 3000 -C 1000000 -y 0
```
followed by
```
./omq -x 0 amqp -T /queue/cq -D 1000000 --amqp-consumer-credits 2
```
Prior to this commit, (and on RabbitMQ 3.x) the consuming would halt after around
8 - 10,000 messages.

The bug was that in flight messages from classic queue process to
session process were not taken into account when topping up credit to
the classic queue process.
Fixes #2597

The solution to this bug (and a much cleaner design anyway independent of
this bug) is that queues should hold all link flow control state including
the delivery-count.

Hence, when credit API v2 is used the delivery-count will be held by the
classic queue process, quorum queue process, and stream queue client
instead of managing the delivery-count in the session.

33. The double level crediting between (a) session process and
rabbit_fifo_client, and (b) rabbit_fifo_client and rabbit_fifo was
removed. Therefore, instead of managing 3 separate delivery-counts (i. session,
ii. rabbit_fifo_client, iii. rabbit_fifo), only 1 delivery-count is used
in rabbit_fifo. This is a big simplification.

34. This commit fixes quorum queues without bumping the machine version
nor introducing new rabbit_fifo commands.

Whether credit API v2 is used is solely determined at link attachment time
depending on whether feature flag credit_api_v2 is enabled.

Even when that feature flag will be enabled later on, this link will
keep using credit API v1 until detached (or the node is shut down).

Eventually, after feature flag credit_api_v2 has been enabled and a
subsequent rolling upgrade, all links will use credit API v2.

This approach is safe and simple.

The 2 alternatives to move delivery-count from the session process to the
queue processes would have been:

i. Explicit feature flag credit_api_v2 migration function
* Can use a gen_server:call and only finish migration once all delivery-counts were migrated.
Cons:
* Extra new message format just for migration is required.
* Risky as migration will fail if a target queue doesn’t reply.

ii. Session always includes DeliveryCountSnd when crediting to the queue:
Cons:
* 2 delivery counts will be hold simultaneously in session proc and queue proc;
could be solved by deleting the session proc’s delivery-count for credit-reply
* What happens if the receiver doesn’t provide credit for a very long time? Is that a problem?

35. Support stream filtering in AMQP 1.0 (by @acogoluegnes)
Use the x-stream-filter-value message annotation
to carry the filter value in a published message.
Use the rabbitmq:stream-filter and rabbitmq:stream-match-unfiltered
filters when creating a receiver that wants to filter
out messages from a stream.

36. Remove credit extension from AMQP 0.9.1 client

37. Support maintenance mode closing AMQP 1.0 connections.

38. Remove AMQP 0.9.1 client dependency from AMQP 1.0 implementation.

39. Move AMQP 1.0 plugin to the core. AMQP 1.0 is enabled by default.
    The old rabbitmq_amqp1_0 plugin will be kept as a no-op plugin to prevent deployment
    tools from failing that execute:
```
rabbitmq-plugins enable rabbitmq_amqp1_0
rabbitmq-plugins disable rabbitmq_amqp1_0
```

40. Breaking change: Remove CLI command `rabbitmqctl list_amqp10_connections`.
Instead, list both AMQP 0.9.1 and AMQP 1.0 connections in `list_connections`:
```
rabbitmqctl list_connections protocol
Listing connections ...
protocol
{1, 0}
{0,9,1}
```

 ## Benchmarks

 ### Throughput & Latency

Setup:
* Single node Ubuntu 22.04
* Erlang 26.1.1

Start RabbitMQ:
```
make run-broker PLUGINS="rabbitmq_management rabbitmq_amqp1_0" FULL=1 RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS="+S 3"
```

Predeclare durable classic queue cq1, durable quorum queue qq1, durable stream queue sq1.

Start client:
https://github.com/ssorj/quiver
https://hub.docker.com/r/ssorj/quiver/tags (digest 453a2aceda64)
```
docker run -it --rm --add-host host.docker.internal:host-gateway ssorj/quiver:latest
bash-5.1# quiver --version
quiver 0.4.0-SNAPSHOT
```

1. Classic queue
```
quiver //host.docker.internal//amq/queue/cq1 --durable --count 1m --duration 10m --body-size 12 --credit 1000
```

This commit:
```
Count ............................................. 1,000,000 messages
Duration ............................................... 73.8 seconds
Sender rate .......................................... 13,548 messages/s
Receiver rate ........................................ 13,547 messages/s
End-to-end rate ...................................... 13,547 messages/s

Latencies by percentile:

          0% ........ 0 ms       90.00% ........ 9 ms
         25% ........ 2 ms       99.00% ....... 14 ms
         50% ........ 4 ms       99.90% ....... 17 ms
        100% ....... 26 ms       99.99% ....... 24 ms
```

RabbitMQ 3.x (main branch as of 30 January 2024):
```
---------------------- Sender -----------------------  --------------------- Receiver ----------------------  --------
Time [s]      Count [m]  Rate [m/s]  CPU [%]  RSS [M]  Time [s]      Count [m]  Rate [m/s]  CPU [%]  RSS [M]  Lat [ms]
-----------------------------------------------------  -----------------------------------------------------  --------
     2.1        130,814      65,342        6     73.6       2.1          3,217       1,607        0      8.0       511
     4.1        163,580      16,367        2     74.1       4.1          3,217           0        0      8.0         0
     6.1        229,114      32,767        3     74.1       6.1          3,217           0        0      8.0         0
     8.1        261,880      16,367        2     74.1       8.1         67,874      32,296        8      8.2     7,662
    10.1        294,646      16,367        2     74.1      10.1         67,874           0        0      8.2         0
    12.1        360,180      32,734        3     74.1      12.1         67,874           0        0      8.2         0
    14.1        392,946      16,367        3     74.1      14.1         68,604         365        0      8.2    12,147
    16.1        458,480      32,734        3     74.1      16.1         68,604           0        0      8.2         0
    18.1        491,246      16,367        2     74.1      18.1         68,604           0        0      8.2         0
    20.1        556,780      32,767        4     74.1      20.1         68,604           0        0      8.2         0
    22.1        589,546      16,375        2     74.1      22.1         68,604           0        0      8.2         0
receiver timed out
    24.1        622,312      16,367        2     74.1      24.1         68,604           0        0      8.2         0
quiver:  error: PlanoProcessError: Command 'quiver-arrow receive //host.docker.internal//amq/queue/cq1 --impl qpid-proton-c --duration 10m --count 1m --rate 0 --body-size 12 --credit 1000 --transaction-size 0 --timeout 10 --durable --output /tmp/quiver-otujr23y' returned non-zero exit status 1.
Traceback (most recent call last):
  File "/usr/local/lib/quiver/python/quiver/pair.py", line 144, in run
    _plano.wait(receiver, check=True)
  File "/usr/local/lib/quiver/python/plano/main.py", line 1243, in wait
    raise PlanoProcessError(proc)
plano.main.PlanoProcessError: Command 'quiver-arrow receive //host.docker.internal//amq/queue/cq1 --impl qpid-proton-c --duration 10m --count 1m --rate 0 --body-size 12 --credit 1000 --transaction-size 0 --timeout 10 --durable --output /tmp/quiver-otujr23y' returned non-zero exit status 1.
```

2. Quorum queue:
```
quiver //host.docker.internal//amq/queue/qq1 --durable --count 1m --duration 10m --body-size 12 --credit 1000
```
This commit:
```
Count ............................................. 1,000,000 messages
Duration .............................................. 101.4 seconds
Sender rate ........................................... 9,867 messages/s
Receiver rate ......................................... 9,868 messages/s
End-to-end rate ....................................... 9,865 messages/s

Latencies by percentile:

          0% ....... 11 ms       90.00% ....... 23 ms
         25% ....... 15 ms       99.00% ....... 28 ms
         50% ....... 18 ms       99.90% ....... 33 ms
        100% ....... 49 ms       99.99% ....... 47 ms
```

RabbitMQ 3.x:
```
---------------------- Sender -----------------------  --------------------- Receiver ----------------------  --------
Time [s]      Count [m]  Rate [m/s]  CPU [%]  RSS [M]  Time [s]      Count [m]  Rate [m/s]  CPU [%]  RSS [M]  Lat [ms]
-----------------------------------------------------  -----------------------------------------------------  --------
     2.1        130,814      65,342        9     69.9       2.1         18,430       9,206        5      7.6     1,221
     4.1        163,580      16,375        5     70.2       4.1         18,867         218        0      7.6     2,168
     6.1        229,114      32,767        6     70.2       6.1         18,867           0        0      7.6         0
     8.1        294,648      32,734        7     70.2       8.1         18,867           0        0      7.6         0
    10.1        360,182      32,734        6     70.2      10.1         18,867           0        0      7.6         0
    12.1        425,716      32,767        6     70.2      12.1         18,867           0        0      7.6         0
receiver timed out
    14.1        458,482      16,367        5     70.2      14.1         18,867           0        0      7.6         0
quiver:  error: PlanoProcessError: Command 'quiver-arrow receive //host.docker.internal//amq/queue/qq1 --impl qpid-proton-c --duration 10m --count 1m --rate 0 --body-size 12 --credit 1000 --transaction-size 0 --timeout 10 --durable --output /tmp/quiver-b1gcup43' returned non-zero exit status 1.
Traceback (most recent call last):
  File "/usr/local/lib/quiver/python/quiver/pair.py", line 144, in run
    _plano.wait(receiver, check=True)
  File "/usr/local/lib/quiver/python/plano/main.py", line 1243, in wait
    raise PlanoProcessError(proc)
plano.main.PlanoProcessError: Command 'quiver-arrow receive //host.docker.internal//amq/queue/qq1 --impl qpid-proton-c --duration 10m --count 1m --rate 0 --body-size 12 --credit 1000 --transaction-size 0 --timeout 10 --durable --output /tmp/quiver-b1gcup43' returned non-zero exit status 1.
```

3. Stream:
```
quiver-arrow send //host.docker.internal//amq/queue/sq1 --durable --count 1m -d 10m --summary --verbose
```

This commit:
```
Count ............................................. 1,000,000 messages
Duration ................................................ 8.7 seconds
Message rate ........................................ 115,154 messages/s
```

RabbitMQ 3.x:
```
Count ............................................. 1,000,000 messages
Duration ............................................... 21.2 seconds
Message rate ......................................... 47,232 messages/s
```

 ### Memory usage

Start RabbitMQ:
```
ERL_MAX_PORTS=3000000 RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS="+P 3000000 +S 6" make run-broker PLUGINS="rabbitmq_amqp1_0" FULL=1 RABBITMQ_CONFIG_FILE="rabbitmq.conf"
```

```
/bin/cat rabbitmq.conf

tcp_listen_options.sndbuf  = 2048
tcp_listen_options.recbuf  = 2048
vm_memory_high_watermark.relative = 0.95
vm_memory_high_watermark_paging_ratio = 0.95
loopback_users = none
```

Create 50k connections with 2 sessions per connection, i.e. 100k session in total:

```go
package main

import (
	"context"
	"log"
	"time"

	"github.com/Azure/go-amqp"
)

func main() {
	for i := 0; i < 50000; i++ {
		conn, err := amqp.Dial(context.TODO(), "amqp://nuc", &amqp.ConnOptions{SASLType: amqp.SASLTypeAnonymous()})
		if err != nil {
			log.Fatal("dialing AMQP server:", err)
		}
		_, err = conn.NewSession(context.TODO(), nil)
		if err != nil {
			log.Fatal("creating AMQP session:", err)
		}
		_, err = conn.NewSession(context.TODO(), nil)
		if err != nil {
			log.Fatal("creating AMQP session:", err)
		}
	}
	log.Println("opened all connections")
	time.Sleep(5 * time.Hour)
}
```

This commit:
```
erlang:memory().
[{total,4586376480},
 {processes,4025898504},
 {processes_used,4025871040},
 {system,560477976},
 {atom,1048841},
 {atom_used,1042841},
 {binary,233228608},
 {code,21449982},
 {ets,108560464}]

erlang:system_info(process_count).
450289
```
7 procs per connection + 1 proc per session.
(7 + 2*1) * 50,000 = 450,000 procs

RabbitMQ 3.x:
```
erlang:memory().
[{total,15168232704},
 {processes,14044779256},
 {processes_used,14044755120},
 {system,1123453448},
 {atom,1057033},
 {atom_used,1052587},
 {binary,236381264},
 {code,21790238},
 {ets,391423744}]

erlang:system_info(process_count).
1850309
```
7 procs per connection + 15 per session
(7 + 2*15) * 50,000 = 1,850,000 procs

50k connections + 100k session require
with this commit: 4.5 GB
in RabbitMQ 3.x: 15 GB

 ## Future work

1. More efficient parser and serializer
2. TODO in mc_amqp: Do not store the parsed message on disk.
3. Implement both AMQP HTTP extension and AMQP management extension to allow AMQP
clients to create RabbitMQ objects (queues, exchanges, ...).
This commit is contained in:
David Ansari 2023-07-21 10:29:07 +00:00
parent 3513df355b
commit 8cb313d5a1
171 changed files with 12092 additions and 7371 deletions

View File

@ -176,8 +176,6 @@ RSYNC_FLAGS += -a $(RSYNC_V) \
--exclude '/cowboy/doc/' \ --exclude '/cowboy/doc/' \
--exclude '/cowboy/examples/' \ --exclude '/cowboy/examples/' \
--exclude '/rabbit/escript/' \ --exclude '/rabbit/escript/' \
--exclude '/rabbitmq_amqp1_0/test/swiftmq/build/'\
--exclude '/rabbitmq_amqp1_0/test/swiftmq/swiftmq*'\
--exclude '/rabbitmq_cli/escript/' \ --exclude '/rabbitmq_cli/escript/' \
--exclude '/rabbitmq_mqtt/test/build/' \ --exclude '/rabbitmq_mqtt/test/build/' \
--exclude '/rabbitmq_mqtt/test/test_client/'\ --exclude '/rabbitmq_mqtt/test/test_client/'\

View File

@ -100,7 +100,6 @@ dialyze(
) )
broker_for_integration_suites( broker_for_integration_suites(
extra_plugins = ["//deps/rabbitmq_amqp1_0:erlang_app"],
) )
TEST_DEPS = [ TEST_DEPS = [

View File

@ -30,7 +30,7 @@ PACKAGES_DIR ?= $(abspath PACKAGES)
BUILD_DEPS = rabbit_common elvis_mk BUILD_DEPS = rabbit_common elvis_mk
DEPS = amqp10_common credentials_obfuscation DEPS = amqp10_common credentials_obfuscation
TEST_DEPS = rabbit rabbitmq_amqp1_0 rabbitmq_ct_helpers TEST_DEPS = rabbit rabbitmq_ct_helpers
LOCAL_DEPS = ssl inets crypto public_key LOCAL_DEPS = ssl inets crypto public_key
DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-test.mk DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-test.mk
@ -51,20 +51,6 @@ include erlang.mk
HEX_TARBALL_FILES += rabbitmq-components.mk \ HEX_TARBALL_FILES += rabbitmq-components.mk \
git-revisions.txt git-revisions.txt
# --------------------------------------------------------------------
# Compiler flags.
# --------------------------------------------------------------------
# gen_fsm is deprecated starting from Erlang 20, but we want to support
# Erlang 19 as well.
ERTS_VER := $(shell erl -version 2>&1 | sed -E 's/.* version //')
ERLANG_20_ERTS_VER := 9.0
ifeq ($(call compare_version,$(ERTS_VER),$(ERLANG_20_ERTS_VER),>=),true)
ERLC_OPTS += -Dnowarn_deprecated_gen_fsm
endif
# Dialyze the tests. # Dialyze the tests.
DIALYZER_OPTS += --src -r test DIALYZER_OPTS += --src -r test

View File

@ -2,16 +2,16 @@
This is an [Erlang client for the AMQP 1.0](https://www.amqp.org/resources/specifications) protocol. This is an [Erlang client for the AMQP 1.0](https://www.amqp.org/resources/specifications) protocol.
It's primary purpose is to be used in RabbitMQ related projects but it is a Its primary purpose is to be used in RabbitMQ related projects but it is a
generic client that was tested with at least 4 implementations of AMQP 1.0. generic client that was tested with at least 3 implementations of AMQP 1.0.
If you are looking for an Erlang client for [AMQP 0-9-1](https://www.rabbitmq.com/tutorials/amqp-concepts.html) — a completely different If you are looking for an Erlang client for [AMQP 0-9-1](https://www.rabbitmq.com/tutorials/amqp-concepts.html) — a completely different
protocol despite the name — [consider this one](https://github.com/rabbitmq/rabbitmq-erlang-client). protocol despite the name — [consider this one](../amqp_client).
## Project Maturity and Status ## Project Maturity and Status
This client is used in the cross-protocol version of the RabbitMQ Shovel plugin. It is not 100% This client is used in the cross-protocol version of the RabbitMQ Shovel plugin. It is not 100%
feature complete but moderately mature and was tested against at least three AMQP 1.0 servers: feature complete but moderately mature and was tested against at least 3 AMQP 1.0 servers:
RabbitMQ, Azure ServiceBus, ActiveMQ. RabbitMQ, Azure ServiceBus, ActiveMQ.
This client library is not officially supported by VMware at this time. This client library is not officially supported by VMware at this time.
@ -80,8 +80,8 @@ after 2000 ->
exit(credited_timeout) exit(credited_timeout)
end. end.
%% create a new message using a delivery-tag, body and indicate %% Create a new message using a delivery-tag, body and indicate
%% it's settlement status (true meaning no disposition confirmation %% its settlement status (true meaning no disposition confirmation
%% will be sent by the receiver). %% will be sent by the receiver).
OutMsg = amqp10_msg:new(<<"my-tag">>, <<"my-body">>, true), OutMsg = amqp10_msg:new(<<"my-tag">>, <<"my-body">>, true),
ok = amqp10_client:send_msg(Sender, OutMsg), ok = amqp10_client:send_msg(Sender, OutMsg),
@ -112,7 +112,7 @@ after the `Open` frame has been successfully written to the socket rather than
waiting until the remote end returns with their `Open` frame. The client will waiting until the remote end returns with their `Open` frame. The client will
notify the caller of various internal/async events using `amqp10_event` notify the caller of various internal/async events using `amqp10_event`
messages. In the example above when the remote replies with their `Open` frame messages. In the example above when the remote replies with their `Open` frame
a message is sent of the following forma: a message is sent of the following form:
``` ```
{amqp10_event, {connection, ConnectionPid, opened}} {amqp10_event, {connection, ConnectionPid, opened}}

View File

@ -13,7 +13,6 @@ def all_beam_files(name = "all_beam_files"):
"src/amqp10_client_app.erl", "src/amqp10_client_app.erl",
"src/amqp10_client_connection.erl", "src/amqp10_client_connection.erl",
"src/amqp10_client_connection_sup.erl", "src/amqp10_client_connection_sup.erl",
"src/amqp10_client_connections_sup.erl",
"src/amqp10_client_frame_reader.erl", "src/amqp10_client_frame_reader.erl",
"src/amqp10_client_session.erl", "src/amqp10_client_session.erl",
"src/amqp10_client_sessions_sup.erl", "src/amqp10_client_sessions_sup.erl",
@ -42,7 +41,6 @@ def all_test_beam_files(name = "all_test_beam_files"):
"src/amqp10_client_app.erl", "src/amqp10_client_app.erl",
"src/amqp10_client_connection.erl", "src/amqp10_client_connection.erl",
"src/amqp10_client_connection_sup.erl", "src/amqp10_client_connection_sup.erl",
"src/amqp10_client_connections_sup.erl",
"src/amqp10_client_frame_reader.erl", "src/amqp10_client_frame_reader.erl",
"src/amqp10_client_session.erl", "src/amqp10_client_session.erl",
"src/amqp10_client_sessions_sup.erl", "src/amqp10_client_sessions_sup.erl",
@ -77,7 +75,6 @@ def all_srcs(name = "all_srcs"):
"src/amqp10_client_app.erl", "src/amqp10_client_app.erl",
"src/amqp10_client_connection.erl", "src/amqp10_client_connection.erl",
"src/amqp10_client_connection_sup.erl", "src/amqp10_client_connection_sup.erl",
"src/amqp10_client_connections_sup.erl",
"src/amqp10_client_frame_reader.erl", "src/amqp10_client_frame_reader.erl",
"src/amqp10_client_session.erl", "src/amqp10_client_session.erl",
"src/amqp10_client_sessions_sup.erl", "src/amqp10_client_sessions_sup.erl",

View File

@ -35,7 +35,7 @@
settle_msg/3, settle_msg/3,
flow_link_credit/3, flow_link_credit/3,
flow_link_credit/4, flow_link_credit/4,
echo/1, stop_receiver_link/1,
link_handle/1, link_handle/1,
get_msg/1, get_msg/1,
get_msg/2, get_msg/2,
@ -55,7 +55,7 @@
-type attach_role() :: amqp10_client_session:attach_role(). -type attach_role() :: amqp10_client_session:attach_role().
-type attach_args() :: amqp10_client_session:attach_args(). -type attach_args() :: amqp10_client_session:attach_args().
-type filter() :: amqp10_client_session:filter(). -type filter() :: amqp10_client_session:filter().
-type properties() :: amqp10_client_session:properties(). -type properties() :: amqp10_client_types:properties().
-type connection_config() :: amqp10_client_connection:connection_config(). -type connection_config() :: amqp10_client_connection:connection_config().
@ -109,10 +109,10 @@ open_connection(ConnectionConfig0) ->
notify_when_closed => NotifyWhenClosed notify_when_closed => NotifyWhenClosed
}, },
Sasl = maps:get(sasl, ConnectionConfig1), Sasl = maps:get(sasl, ConnectionConfig1),
ConnectionConfig2 = ConnectionConfig1#{sasl => amqp10_client_connection:encrypt_sasl(Sasl)}, ConnectionConfig = ConnectionConfig1#{sasl => amqp10_client_connection:encrypt_sasl(Sasl)},
amqp10_client_connection:open(ConnectionConfig2). amqp10_client_connection:open(ConnectionConfig).
%% @doc Opens a connection using a connection_config map %% @doc Closes a connection.
%% This is asynchronous and will notify completion to the caller using %% This is asynchronous and will notify completion to the caller using
%% an amqp10_event of the following format: %% an amqp10_event of the following format:
%% {amqp10_event, {connection, ConnectionPid, {closed, Why}}} %% {amqp10_event, {connection, ConnectionPid, {closed, Why}}}
@ -271,9 +271,8 @@ attach_receiver_link(Session, Name, Source, SettleMode, Durability, Filter) ->
%% This is asynchronous and will notify completion of the attach request to the %% This is asynchronous and will notify completion of the attach request to the
%% caller using an amqp10_event of the following format: %% caller using an amqp10_event of the following format:
%% {amqp10_event, {link, LinkRef, attached | {detached, Why}}} %% {amqp10_event, {link, LinkRef, attached | {detached, Why}}}
-spec attach_receiver_link(pid(), binary(), binary(), -spec attach_receiver_link(pid(), binary(), binary(), snd_settle_mode(),
snd_settle_mode(), terminus_durability(), filter(), terminus_durability(), filter(), properties()) ->
properties()) ->
{ok, link_ref()}. {ok, link_ref()}.
attach_receiver_link(Session, Name, Source, SettleMode, Durability, Filter, Properties) attach_receiver_link(Session, Name, Source, SettleMode, Durability, Filter, Properties)
when is_pid(Session) andalso when is_pid(Session) andalso
@ -307,43 +306,45 @@ detach_link(#link_ref{link_handle = Handle, session = Session}) ->
amqp10_client_session:detach(Session, Handle). amqp10_client_session:detach(Session, Handle).
%% @doc Grant credit to a sender. %% @doc Grant credit to a sender.
%% The amqp10_client will automatically grant more credit to the sender when %% The amqp10_client will automatically grant Credit to the sender when
%% the remaining link credit falls below the value of RenewWhenBelow. %% the remaining link credit falls below the value of RenewWhenBelow.
%% If RenewWhenBelow is 'never' the client will never grant new credit. Instead %% If RenewWhenBelow is 'never' the client will never grant more credit. Instead
%% the caller will be notified when the link_credit reaches 0 with an %% the caller will be notified when the link_credit reaches 0 with an
%% amqp10_event of the following format: %% amqp10_event of the following format:
%% {amqp10_event, {link, LinkRef, credit_exhausted}} %% {amqp10_event, {link, LinkRef, credit_exhausted}}
-spec flow_link_credit(link_ref(), Credit :: non_neg_integer(), -spec flow_link_credit(link_ref(), Credit :: non_neg_integer(),
RenewWhenBelow :: never | non_neg_integer()) -> ok. RenewWhenBelow :: never | pos_integer()) -> ok.
flow_link_credit(Ref, Credit, RenewWhenBelow) -> flow_link_credit(Ref, Credit, RenewWhenBelow) ->
flow_link_credit(Ref, Credit, RenewWhenBelow, false). flow_link_credit(Ref, Credit, RenewWhenBelow, false).
-spec flow_link_credit(link_ref(), Credit :: non_neg_integer(), -spec flow_link_credit(link_ref(), Credit :: non_neg_integer(),
RenewWhenBelow :: never | non_neg_integer(), RenewWhenBelow :: never | pos_integer(),
Drain :: boolean()) -> ok. Drain :: boolean()) -> ok.
flow_link_credit(#link_ref{role = receiver, session = Session, flow_link_credit(#link_ref{role = receiver, session = Session,
link_handle = Handle}, link_handle = Handle},
Credit, RenewWhenBelow, Drain) -> Credit, RenewWhenBelow, Drain)
when RenewWhenBelow =:= never orelse
is_integer(RenewWhenBelow) andalso
RenewWhenBelow > 0 andalso
RenewWhenBelow =< Credit ->
Flow = #'v1_0.flow'{link_credit = {uint, Credit}, Flow = #'v1_0.flow'{link_credit = {uint, Credit},
drain = Drain}, drain = Drain},
ok = amqp10_client_session:flow(Session, Handle, Flow, RenewWhenBelow). ok = amqp10_client_session:flow(Session, Handle, Flow, RenewWhenBelow).
%% @doc Request that the sender's flow state is echoed back %% @doc Stop a receiving link.
%% This may be used to determine when the Link has finally quiesced. %% See AMQP 1.0 spec §2.6.10.
%% see §2.6.10 of the spec stop_receiver_link(#link_ref{role = receiver,
echo(#link_ref{role = receiver, session = Session, session = Session,
link_handle = Handle}) -> link_handle = Handle}) ->
Flow = #'v1_0.flow'{link_credit = {uint, 0}, Flow = #'v1_0.flow'{link_credit = {uint, 0},
echo = true}, echo = true},
ok = amqp10_client_session:flow(Session, Handle, Flow, 0). ok = amqp10_client_session:flow(Session, Handle, Flow, never).
%%% messages %%% messages
%% @doc Send a message on a the link referred to be the 'LinkRef'. %% @doc Send a message on a the link referred to be the 'LinkRef'.
%% Returns ok for "async" transfers when messages are sent with settled=true
%% else it returns the delivery state from the disposition
-spec send_msg(link_ref(), amqp10_msg:amqp10_msg()) -> -spec send_msg(link_ref(), amqp10_msg:amqp10_msg()) ->
ok | {error, insufficient_credit | link_not_found | half_attached}. ok | amqp10_client_session:transfer_error().
send_msg(#link_ref{role = sender, session = Session, send_msg(#link_ref{role = sender, session = Session,
link_handle = Handle}, Msg0) -> link_handle = Handle}, Msg0) ->
Msg = amqp10_msg:set_handle(Handle, Msg0), Msg = amqp10_msg:set_handle(Handle, Msg0),

View File

@ -7,8 +7,6 @@
-define(AMQP_PROTOCOL_HEADER, <<"AMQP", 0, 1, 0, 0>>). -define(AMQP_PROTOCOL_HEADER, <<"AMQP", 0, 1, 0, 0>>).
-define(SASL_PROTOCOL_HEADER, <<"AMQP", 3, 1, 0, 0>>). -define(SASL_PROTOCOL_HEADER, <<"AMQP", 3, 1, 0, 0>>).
-define(MIN_MAX_FRAME_SIZE, 512).
-define(MAX_MAX_FRAME_SIZE, 1024 * 1024).
-define(FRAME_HEADER_SIZE, 8). -define(FRAME_HEADER_SIZE, 8).
-define(TIMEOUT, 5000). -define(TIMEOUT, 5000).

View File

@ -9,30 +9,12 @@
-behaviour(application). -behaviour(application).
%% Application callbacks %% application callbacks
-export([start/2, -export([start/2,
stop/1]). stop/1]).
-type start_type() :: (
normal |
{takeover, Node :: node()} |
{failover, Node :: node()}
).
-type state() :: term().
%%====================================================================
%% API
%%====================================================================
-spec start(StartType :: start_type(), StartArgs :: term()) ->
{ok, Pid :: pid()} | {ok, Pid :: pid(), State :: state()} | {error, Reason :: term()}.
start(_Type, _Args) -> start(_Type, _Args) ->
amqp10_client_sup:start_link(). amqp10_client_sup:start_link().
-spec stop(State :: state()) -> ok.
stop(_State) -> stop(_State) ->
ok. ok.
%%====================================================================
%% Internal functions
%%====================================================================

View File

@ -11,21 +11,13 @@
-include("amqp10_client.hrl"). -include("amqp10_client.hrl").
-include_lib("amqp10_common/include/amqp10_framing.hrl"). -include_lib("amqp10_common/include/amqp10_framing.hrl").
-include_lib("amqp10_common/include/amqp10_types.hrl").
-ifdef(nowarn_deprecated_gen_fsm). %% public API
-compile({nowarn_deprecated_function,
[{gen_fsm, reply, 2},
{gen_fsm, send_all_state_event, 2},
{gen_fsm, send_event, 2},
{gen_fsm, start_link, 3},
{gen_fsm, sync_send_all_state_event, 2}]}).
-endif.
%% Public API.
-export([open/1, -export([open/1,
close/2]). close/2]).
%% Private API. %% private API
-export([start_link/2, -export([start_link/2,
socket_ready/2, socket_ready/2,
protocol_header_received/5, protocol_header_received/5,
@ -34,13 +26,14 @@
encrypt_sasl/1, encrypt_sasl/1,
decrypt_sasl/1]). decrypt_sasl/1]).
%% gen_fsm callbacks. %% gen_statem callbacks
-export([init/1, -export([init/1,
callback_mode/0, callback_mode/0,
terminate/3, terminate/3,
code_change/4]). code_change/4]).
%% gen_fsm state callbacks. %% gen_statem state callbacks
%% see figure 2.23
-export([expecting_socket/3, -export([expecting_socket/3,
sasl_hdr_sent/3, sasl_hdr_sent/3,
sasl_hdr_rcvds/3, sasl_hdr_rcvds/3,
@ -71,8 +64,10 @@
notify => pid() | none, % the pid to send connection events to notify => pid() | none, % the pid to send connection events to
notify_when_opened => pid() | none, notify_when_opened => pid() | none,
notify_when_closed => pid() | none, notify_when_closed => pid() | none,
max_frame_size => non_neg_integer(), % TODO: constrain to large than 512 %% incoming maximum frame size set by our client application
outgoing_max_frame_size => non_neg_integer() | undefined, max_frame_size => pos_integer(), % TODO: constrain to large than 512
%% outgoing maximum frame size set by AMQP peer in OPEN performative
outgoing_max_frame_size => pos_integer() | undefined,
idle_time_out => milliseconds(), idle_time_out => milliseconds(),
% set to a negative value to allow a sender to "overshoot" the flow % set to a negative value to allow a sender to "overshoot" the flow
% control by this margin % control by this margin
@ -80,9 +75,7 @@
%% These credentials_obfuscation-wrapped values have the type of %% These credentials_obfuscation-wrapped values have the type of
%% decrypted_sasl/0 %% decrypted_sasl/0
sasl => encrypted_sasl() | decrypted_sasl(), sasl => encrypted_sasl() | decrypted_sasl(),
notify => pid(), properties => amqp10_client_types:properties()
notify_when_opened => pid() | none,
notify_when_closed => pid() | none
}. }.
-record(state, -record(state,
@ -167,13 +160,13 @@ protocol_header_received(Pid, Protocol, Maj, Min, Rev) ->
-spec begin_session(pid()) -> supervisor:startchild_ret(). -spec begin_session(pid()) -> supervisor:startchild_ret().
begin_session(Pid) -> begin_session(Pid) ->
gen_statem:call(Pid, begin_session, {dirty_timeout, ?TIMEOUT}). gen_statem:call(Pid, begin_session, ?TIMEOUT).
heartbeat(Pid) -> heartbeat(Pid) ->
gen_statem:cast(Pid, heartbeat). gen_statem:cast(Pid, heartbeat).
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------
%% gen_fsm callbacks. %% gen_statem callbacks.
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------
callback_mode() -> [state_functions]. callback_mode() -> [state_functions].
@ -259,7 +252,7 @@ hdr_sent({call, From}, begin_session,
State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]}, State1 = State#state{pending_session_reqs = [From | PendingSessionReqs]},
{keep_state, State1}. {keep_state, State1}.
open_sent(_EvtType, #'v1_0.open'{max_frame_size = MFSz, open_sent(_EvtType, #'v1_0.open'{max_frame_size = MaybeMaxFrameSize,
idle_time_out = Timeout}, idle_time_out = Timeout},
#state{pending_session_reqs = PendingSessionReqs, #state{pending_session_reqs = PendingSessionReqs,
config = Config} = State0) -> config = Config} = State0) ->
@ -271,8 +264,14 @@ open_sent(_EvtType, #'v1_0.open'{max_frame_size = MFSz,
heartbeat_timer = Tmr}; heartbeat_timer = Tmr};
_ -> State0 _ -> State0
end, end,
State1 = State#state{config = MaxFrameSize = case unpack(MaybeMaxFrameSize) of
Config#{outgoing_max_frame_size => unpack(MFSz)}}, undefined ->
%% default as per 2.7.1
?UINT_MAX;
Bytes when is_integer(Bytes) ->
Bytes
end,
State1 = State#state{config = Config#{outgoing_max_frame_size => MaxFrameSize}},
State2 = lists:foldr( State2 = lists:foldr(
fun(From, S0) -> fun(From, S0) ->
{Ret, S2} = handle_begin_session(From, S0), {Ret, S2} = handle_begin_session(From, S0),
@ -403,32 +402,32 @@ handle_begin_session({FromPid, _Ref},
end, end,
{Ret, State1}. {Ret, State1}.
send_open(#state{socket = Socket, config = Config}) -> send_open(#state{socket = Socket, config = Config0}) ->
{ok, Product} = application:get_key(description), {ok, Product} = application:get_key(description),
{ok, Version} = application:get_key(vsn), {ok, Version} = application:get_key(vsn),
Platform = "Erlang/OTP " ++ erlang:system_info(otp_release), Platform = "Erlang/OTP " ++ erlang:system_info(otp_release),
Props = {map, [{{symbol, <<"product">>}, Props0 = #{<<"product">> => {utf8, list_to_binary(Product)},
{utf8, list_to_binary(Product)}}, <<"version">> => {utf8, list_to_binary(Version)},
{{symbol, <<"version">>}, <<"platform">> => {utf8, list_to_binary(Platform)}},
{utf8, list_to_binary(Version)}}, Config = maps:update_with(properties,
{{symbol, <<"platform">>}, fun(Val) -> maps:merge(Props0, Val) end,
{utf8, list_to_binary(Platform)}} Props0,
]}, Config0),
Props = amqp10_client_types:make_properties(Config),
ContainerId = maps:get(container_id, Config, generate_container_id()), ContainerId = maps:get(container_id, Config, generate_container_id()),
IdleTimeOut = maps:get(idle_time_out, Config, 0), IdleTimeOut = maps:get(idle_time_out, Config, 0),
IncomingMaxFrameSize = maps:get(max_frame_size, Config),
Open0 = #'v1_0.open'{container_id = {utf8, ContainerId}, Open0 = #'v1_0.open'{container_id = {utf8, ContainerId},
channel_max = {ushort, 100}, channel_max = {ushort, 100},
idle_time_out = {uint, IdleTimeOut}, idle_time_out = {uint, IdleTimeOut},
properties = Props}, properties = Props,
Open1 = case Config of max_frame_size = {uint, IncomingMaxFrameSize}
#{max_frame_size := MFSz} -> },
Open0#'v1_0.open'{max_frame_size = {uint, MFSz}};
_ -> Open0
end,
Open = case Config of Open = case Config of
#{hostname := Hostname} -> #{hostname := Hostname} ->
Open1#'v1_0.open'{hostname = {utf8, Hostname}}; Open0#'v1_0.open'{hostname = {utf8, Hostname}};
_ -> Open1 _ ->
Open0
end, end,
Encoded = amqp10_framing:encode_bin(Open), Encoded = amqp10_framing:encode_bin(Open),
Frame = amqp10_binary_generator:build_frame(0, Encoded), Frame = amqp10_binary_generator:build_frame(0, Encoded),
@ -508,7 +507,7 @@ unpack(V) -> amqp10_client_types:unpack(V).
-spec generate_container_id() -> binary(). -spec generate_container_id() -> binary().
generate_container_id() -> generate_container_id() ->
Pre = list_to_binary(atom_to_list(node())), Pre = atom_to_binary(node()),
Id = bin_to_hex(crypto:strong_rand_bytes(8)), Id = bin_to_hex(crypto:strong_rand_bytes(8)),
<<Pre/binary, <<"_">>/binary, Id/binary>>. <<Pre/binary, <<"_">>/binary, Id/binary>>.
@ -552,4 +551,5 @@ decrypted_sasl_to_bin(none) -> <<"ANONYMOUS">>.
config_defaults() -> config_defaults() ->
#{sasl => none, #{sasl => none,
transfer_limit_margin => 0, transfer_limit_margin => 0,
max_frame_size => ?MAX_MAX_FRAME_SIZE}. %% 1 MB
max_frame_size => 1_048_576}.

View File

@ -8,35 +8,31 @@
-behaviour(supervisor). -behaviour(supervisor).
%% Private API. %% API
-export([start_link/1]). -export([start_link/1]).
%% Supervisor callbacks. %% Supervisor callbacks
-export([init/1]). -export([init/1]).
-define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args},
transient, 5000, Type, [Mod]}).
%% -------------------------------------------------------------------
%% Private API.
%% -------------------------------------------------------------------
-spec start_link(amqp10_client_connection:connection_config()) ->
{ok, pid()} | ignore | {error, any()}.
start_link(Config) -> start_link(Config) ->
supervisor:start_link(?MODULE, [Config]). supervisor:start_link(?MODULE, [Config]).
%% ------------------------------------------------------------------- init(Args0) ->
%% Supervisor callbacks. SupFlags = #{strategy => one_for_all,
%% ------------------------------------------------------------------- intensity => 0,
period => 1},
init(Args) -> Fun = start_link,
ReaderSpec = ?CHILD(reader, amqp10_client_frame_reader, Args = [self() | Args0],
worker, [self() | Args]), ConnectionSpec = #{id => connection,
ConnectionSpec = ?CHILD(connection, amqp10_client_connection, start => {amqp10_client_connection, Fun, Args},
worker, [self() | Args]), restart => transient},
SessionsSupSpec = ?CHILD(sessions, amqp10_client_sessions_sup, ReaderSpec = #{id => reader,
supervisor, []), start => {amqp10_client_frame_reader, Fun, Args},
{ok, {{one_for_all, 0, 1}, [ConnectionSpec, restart => transient},
ReaderSpec, SessionsSupSpec = #{id => sessions,
SessionsSupSpec]}}. start => {amqp10_client_sessions_sup, Fun, []},
restart => transient,
type => supervisor},
{ok, {SupFlags, [ConnectionSpec,
ReaderSpec,
SessionsSupSpec]}}.

View File

@ -1,38 +0,0 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(amqp10_client_connections_sup).
-behaviour(supervisor).
%% Private API.
-export([start_link/0,
stop_child/1]).
%% Supervisor callbacks.
-export([init/1]).
-define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args},
temporary, infinity, Type, [Mod]}).
%% -------------------------------------------------------------------
%% Private API.
%% -------------------------------------------------------------------
stop_child(Pid) ->
supervisor:terminate_child({local, ?MODULE}, Pid).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% -------------------------------------------------------------------
%% Supervisor callbacks.
%% -------------------------------------------------------------------
init([]) ->
Template = ?CHILD(connection_sup, amqp10_client_connection_sup,
supervisor, []),
{ok, {{simple_one_for_one, 0, 1}, [Template]}}.

File diff suppressed because it is too large Load Diff

View File

@ -8,29 +8,20 @@
-behaviour(supervisor). -behaviour(supervisor).
%% Private API. %% API
-export([start_link/0]). -export([start_link/0]).
%% Supervisor callbacks. %% Supervisor callbacks
-export([init/1]). -export([init/1]).
-define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args},
transient, 5000, Type, [Mod]}).
%% -------------------------------------------------------------------
%% Private API.
%% -------------------------------------------------------------------
-spec start_link() ->
{ok, pid()} | ignore | {error, any()}.
start_link() -> start_link() ->
supervisor:start_link(?MODULE, []). supervisor:start_link(?MODULE, []).
%% ------------------------------------------------------------------- init([]) ->
%% Supervisor callbacks. SupFlags = #{strategy => simple_one_for_one,
%% ------------------------------------------------------------------- intensity => 0,
period => 1},
init(Args) -> ChildSpec = #{id => session,
Template = ?CHILD(session, amqp10_client_session, worker, Args), start => {amqp10_client_session, start_link, []},
{ok, {{simple_one_for_one, 0, 1}, [Template]}}. restart => transient},
{ok, {SupFlags, [ChildSpec]}}.

View File

@ -8,27 +8,21 @@
-behaviour(supervisor). -behaviour(supervisor).
%% Private API. %% API
-export([start_link/0]). -export([start_link/0]).
%% Supervisor callbacks. %% Supervisor callbacks
-export([init/1]). -export([init/1]).
-define(CHILD(Id, Mod, Type, Args), {Id, {Mod, start_link, Args},
temporary, infinity, Type, [Mod]}).
%% -------------------------------------------------------------------
%% Private API.
%% -------------------------------------------------------------------
start_link() -> start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% -------------------------------------------------------------------
%% Supervisor callbacks.
%% -------------------------------------------------------------------
init([]) -> init([]) ->
Template = ?CHILD(connection_sup, amqp10_client_connection_sup, SupFlags = #{strategy => simple_one_for_one,
supervisor, []), intensity => 0,
{ok, {{simple_one_for_one, 0, 1}, [Template]}}. period => 1},
ChildSpec = #{id => connection_sup,
start => {amqp10_client_connection_sup, start_link, []},
restart => temporary,
type => supervisor},
{ok, {SupFlags, [ChildSpec]}}.

View File

@ -10,7 +10,8 @@
-export([unpack/1, -export([unpack/1,
utf8/1, utf8/1,
uint/1]). uint/1,
make_properties/1]).
-type amqp10_performative() :: #'v1_0.open'{} | #'v1_0.begin'{} | #'v1_0.attach'{} | -type amqp10_performative() :: #'v1_0.open'{} | #'v1_0.begin'{} | #'v1_0.attach'{} |
#'v1_0.flow'{} | #'v1_0.transfer'{} | #'v1_0.flow'{} | #'v1_0.transfer'{} |
@ -63,10 +64,13 @@
link_event_detail()}. link_event_detail()}.
-type amqp10_event() :: {amqp10_event, amqp10_event_detail()}. -type amqp10_event() :: {amqp10_event, amqp10_event_detail()}.
-type properties() :: #{binary() => tuple()}.
-export_type([amqp10_performative/0, channel/0, -export_type([amqp10_performative/0, channel/0,
source/0, target/0, amqp10_msg_record/0, source/0, target/0, amqp10_msg_record/0,
delivery_state/0, amqp_error/0, connection_error/0, delivery_state/0, amqp_error/0, connection_error/0,
amqp10_event_detail/0, amqp10_event/0]). amqp10_event_detail/0, amqp10_event/0,
properties/0]).
unpack(undefined) -> undefined; unpack(undefined) -> undefined;
@ -77,3 +81,12 @@ utf8(S) when is_list(S) -> {utf8, list_to_binary(S)};
utf8(B) when is_binary(B) -> {utf8, B}. utf8(B) when is_binary(B) -> {utf8, B}.
uint(N) -> {uint, N}. uint(N) -> {uint, N}.
make_properties(#{properties := Props})
when is_map(Props) andalso
map_size(Props) > 0 ->
{map, maps:fold(fun(K, V, L) ->
[{{symbol, K}, V} | L]
end, [], Props)};
make_properties(_) ->
undefined.

View File

@ -38,7 +38,7 @@
-include_lib("amqp10_common/include/amqp10_framing.hrl"). -include_lib("amqp10_common/include/amqp10_framing.hrl").
-type maybe(T) :: T | undefined. -type opt(T) :: T | undefined.
-type delivery_tag() :: binary(). -type delivery_tag() :: binary().
-type content_type() :: term(). % TODO: refine -type content_type() :: term(). % TODO: refine
@ -52,23 +52,23 @@
-type amqp10_header() :: #{durable => boolean(), % false -type amqp10_header() :: #{durable => boolean(), % false
priority => byte(), % 4 priority => byte(), % 4
ttl => maybe(non_neg_integer()), ttl => opt(non_neg_integer()),
first_acquirer => boolean(), % false first_acquirer => boolean(), % false
delivery_count => non_neg_integer()}. % 0 delivery_count => non_neg_integer()}. % 0
-type amqp10_properties() :: #{message_id => maybe(any()), -type amqp10_properties() :: #{message_id => opt(any()),
user_id => maybe(binary()), user_id => opt(binary()),
to => maybe(any()), to => opt(any()),
subject => maybe(binary()), subject => opt(binary()),
reply_to => maybe(any()), reply_to => opt(any()),
correlation_id => maybe(any()), correlation_id => opt(any()),
content_type => maybe(content_type()), content_type => opt(content_type()),
content_encoding => maybe(content_encoding()), content_encoding => opt(content_encoding()),
absolute_expiry_time => maybe(non_neg_integer()), absolute_expiry_time => opt(non_neg_integer()),
creation_time => maybe(non_neg_integer()), creation_time => opt(non_neg_integer()),
group_id => maybe(binary()), group_id => opt(binary()),
group_sequence => maybe(non_neg_integer()), group_sequence => opt(non_neg_integer()),
reply_to_group_id => maybe(binary())}. reply_to_group_id => opt(binary())}.
-type amqp10_body() :: [#'v1_0.data'{}] | -type amqp10_body() :: [#'v1_0.data'{}] |
[#'v1_0.amqp_sequence'{}] | [#'v1_0.amqp_sequence'{}] |
@ -78,13 +78,13 @@
-record(amqp10_msg, -record(amqp10_msg,
{transfer :: #'v1_0.transfer'{}, {transfer :: #'v1_0.transfer'{},
header :: maybe(#'v1_0.header'{}), header :: opt(#'v1_0.header'{}),
delivery_annotations :: maybe(#'v1_0.delivery_annotations'{}), delivery_annotations :: opt(#'v1_0.delivery_annotations'{}),
message_annotations :: maybe(#'v1_0.message_annotations'{}), message_annotations :: opt(#'v1_0.message_annotations'{}),
properties :: maybe(#'v1_0.properties'{}), properties :: opt(#'v1_0.properties'{}),
application_properties :: maybe(#'v1_0.application_properties'{}), application_properties :: opt(#'v1_0.application_properties'{}),
body :: amqp10_body() | unset, body :: amqp10_body() | unset,
footer :: maybe(#'v1_0.footer'{}) footer :: opt(#'v1_0.footer'{})
}). }).
-opaque amqp10_msg() :: #amqp10_msg{}. -opaque amqp10_msg() :: #amqp10_msg{}.
@ -142,7 +142,7 @@ settled(#amqp10_msg{transfer = #'v1_0.transfer'{settled = Settled}}) ->
% the last 1 octet is the version % the last 1 octet is the version
% See 2.8.11 in the spec % See 2.8.11 in the spec
-spec message_format(amqp10_msg()) -> -spec message_format(amqp10_msg()) ->
maybe({non_neg_integer(), non_neg_integer()}). opt({non_neg_integer(), non_neg_integer()}).
message_format(#amqp10_msg{transfer = message_format(#amqp10_msg{transfer =
#'v1_0.transfer'{message_format = undefined}}) -> #'v1_0.transfer'{message_format = undefined}}) ->
undefined; undefined;
@ -306,7 +306,7 @@ set_headers(Headers, #amqp10_msg{header = Current} = Msg) ->
H = maps:fold(fun(durable, V, Acc) -> H = maps:fold(fun(durable, V, Acc) ->
Acc#'v1_0.header'{durable = V}; Acc#'v1_0.header'{durable = V};
(priority, V, Acc) -> (priority, V, Acc) ->
Acc#'v1_0.header'{priority = {uint, V}}; Acc#'v1_0.header'{priority = {ubyte, V}};
(first_acquirer, V, Acc) -> (first_acquirer, V, Acc) ->
Acc#'v1_0.header'{first_acquirer = V}; Acc#'v1_0.header'{first_acquirer = V};
(ttl, V, Acc) -> (ttl, V, Acc) ->
@ -325,8 +325,8 @@ set_properties(Props, #amqp10_msg{properties = Current} = Msg) ->
P = maps:fold(fun(message_id, V, Acc) when is_binary(V) -> P = maps:fold(fun(message_id, V, Acc) when is_binary(V) ->
% message_id can be any type but we restrict it here % message_id can be any type but we restrict it here
Acc#'v1_0.properties'{message_id = utf8(V)}; Acc#'v1_0.properties'{message_id = utf8(V)};
(user_id, V, Acc) -> (user_id, V, Acc) when is_binary(V) ->
Acc#'v1_0.properties'{user_id = utf8(V)}; Acc#'v1_0.properties'{user_id = {binary, V}};
(to, V, Acc) -> (to, V, Acc) ->
Acc#'v1_0.properties'{to = utf8(V)}; Acc#'v1_0.properties'{to = utf8(V)};
(subject, V, Acc) -> (subject, V, Acc) ->

View File

@ -14,21 +14,10 @@
-include("src/amqp10_client.hrl"). -include("src/amqp10_client.hrl").
-compile(export_all). -compile([export_all, nowarn_export_all]).
-define(UNAUTHORIZED_USER, <<"test_user_no_perm">>). suite() ->
[{timetrap, {seconds, 120}}].
%% The latch constant defines how many processes are spawned in order
%% to run certain functionality in parallel. It follows the standard
%% countdown latch pattern.
-define(LATCH, 100).
%% The wait constant defines how long a consumer waits before it
%% unsubscribes
-define(WAIT, 200).
%% How to long wait for a process to die after an expected failure
-define(PROCESS_EXIT_TIMEOUT, 5000).
all() -> all() ->
[ [
@ -77,7 +66,8 @@ shared() ->
subscribe, subscribe,
subscribe_with_auto_flow, subscribe_with_auto_flow,
outgoing_heartbeat, outgoing_heartbeat,
roundtrip_large_messages roundtrip_large_messages,
transfer_id_vs_delivery_id
]. ].
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------
@ -112,17 +102,13 @@ stop_amqp10_client_app(Config) ->
init_per_group(rabbitmq, Config0) -> init_per_group(rabbitmq, Config0) ->
Config = rabbit_ct_helpers:set_config(Config0, Config = rabbit_ct_helpers:set_config(Config0,
{sasl, {plain, <<"guest">>, <<"guest">>}}), {sasl, {plain, <<"guest">>, <<"guest">>}}),
Config1 = rabbit_ct_helpers:merge_app_env(Config, rabbit_ct_helpers:run_steps(Config, rabbit_ct_broker_helpers:setup_steps());
[{rabbitmq_amqp1_0,
[{protocol_strict_mode, true}]}]),
rabbit_ct_helpers:run_steps(Config1, rabbit_ct_broker_helpers:setup_steps());
init_per_group(rabbitmq_strict, Config0) -> init_per_group(rabbitmq_strict, Config0) ->
Config = rabbit_ct_helpers:set_config(Config0, Config = rabbit_ct_helpers:set_config(Config0,
{sasl, {plain, <<"guest">>, <<"guest">>}}), {sasl, {plain, <<"guest">>, <<"guest">>}}),
Config1 = rabbit_ct_helpers:merge_app_env(Config, Config1 = rabbit_ct_helpers:merge_app_env(Config,
[{rabbitmq_amqp1_0, [{rabbit,
[{default_user, none}, [{amqp1_0_default_user, none}]}]),
{protocol_strict_mode, true}]}]),
rabbit_ct_helpers:run_steps(Config1, rabbit_ct_broker_helpers:setup_steps()); rabbit_ct_helpers:run_steps(Config1, rabbit_ct_broker_helpers:setup_steps());
init_per_group(activemq, Config0) -> init_per_group(activemq, Config0) ->
Config = rabbit_ct_helpers:set_config(Config0, {sasl, anon}), Config = rabbit_ct_helpers:set_config(Config0, {sasl, anon}),
@ -309,9 +295,7 @@ roundtrip_large_messages(Config) ->
Data1Mb = binary:copy(DataKb, 1024), Data1Mb = binary:copy(DataKb, 1024),
roundtrip(OpenConf, Data1Mb), roundtrip(OpenConf, Data1Mb),
roundtrip(OpenConf, binary:copy(Data1Mb, 8)), roundtrip(OpenConf, binary:copy(Data1Mb, 8)),
roundtrip(OpenConf, binary:copy(Data1Mb, 64)), ok = roundtrip(OpenConf, binary:copy(Data1Mb, 64)).
ok.
roundtrip(OpenConf) -> roundtrip(OpenConf) ->
roundtrip(OpenConf, <<"banana">>). roundtrip(OpenConf, <<"banana">>).
@ -319,39 +303,32 @@ roundtrip(OpenConf) ->
roundtrip(OpenConf, Body) -> roundtrip(OpenConf, Body) ->
{ok, Connection} = amqp10_client:open_connection(OpenConf), {ok, Connection} = amqp10_client:open_connection(OpenConf),
{ok, Session} = amqp10_client:begin_session(Connection), {ok, Session} = amqp10_client:begin_session(Connection),
{ok, Sender} = amqp10_client:attach_sender_link(Session, {ok, Sender} = amqp10_client:attach_sender_link(
<<"banana-sender">>, Session, <<"banana-sender">>, <<"test1">>, settled, unsettled_state),
<<"test1">>,
settled,
unsettled_state),
await_link(Sender, credited, link_credit_timeout), await_link(Sender, credited, link_credit_timeout),
Now = os:system_time(millisecond), Now = os:system_time(millisecond),
Props = #{creation_time => Now}, Props = #{creation_time => Now},
Msg0 = amqp10_msg:set_properties(Props, Msg0 = amqp10_msg:new(<<"my-tag">>, Body, true),
amqp10_msg:new(<<"my-tag">>, Body, true)), Msg1 = amqp10_msg:set_properties(Props, Msg0),
Msg1 = amqp10_msg:set_application_properties(#{"a_key" => "a_value"}, Msg0), Msg2 = amqp10_msg:set_application_properties(#{"a_key" => "a_value"}, Msg1),
Msg = amqp10_msg:set_message_annotations(#{<<"x_key">> => "x_value"}, Msg1), Msg3 = amqp10_msg:set_message_annotations(#{<<"x_key">> => "x_value"}, Msg2),
% RabbitMQ AMQP 1.0 does not yet support delivery annotations Msg = amqp10_msg:set_delivery_annotations(#{<<"y_key">> => "y_value"}, Msg3),
% Msg = amqp10_msg:set_delivery_annotations(#{<<"x_key">> => "x_value"}, Msg2),
ok = amqp10_client:send_msg(Sender, Msg), ok = amqp10_client:send_msg(Sender, Msg),
ok = amqp10_client:detach_link(Sender), ok = amqp10_client:detach_link(Sender),
await_link(Sender, {detached, normal}, link_detach_timeout), await_link(Sender, {detached, normal}, link_detach_timeout),
{error, link_not_found} = amqp10_client:detach_link(Sender), {error, link_not_found} = amqp10_client:detach_link(Sender),
{ok, Receiver} = amqp10_client:attach_receiver_link(Session, {ok, Receiver} = amqp10_client:attach_receiver_link(
<<"banana-receiver">>, Session, <<"banana-receiver">>, <<"test1">>, settled, unsettled_state),
<<"test1">>, {ok, OutMsg} = amqp10_client:get_msg(Receiver, 60_000 * 4),
settled,
unsettled_state),
{ok, OutMsg} = amqp10_client:get_msg(Receiver, 60000 * 5),
ok = amqp10_client:end_session(Session), ok = amqp10_client:end_session(Session),
ok = amqp10_client:close_connection(Connection), ok = amqp10_client:close_connection(Connection),
% ct:pal(?LOW_IMPORTANCE, "roundtrip message Out: ~tp~nIn: ~tp~n", [OutMsg, Msg]), % ct:pal(?LOW_IMPORTANCE, "roundtrip message Out: ~tp~nIn: ~tp~n", [OutMsg, Msg]),
#{creation_time := Now} = amqp10_msg:properties(OutMsg), #{creation_time := Now} = amqp10_msg:properties(OutMsg),
#{<<"a_key">> := <<"a_value">>} = amqp10_msg:application_properties(OutMsg), #{<<"a_key">> := <<"a_value">>} = amqp10_msg:application_properties(OutMsg),
#{<<"x_key">> := <<"x_value">>} = amqp10_msg:message_annotations(OutMsg), #{<<"x_key">> := <<"x_value">>} = amqp10_msg:message_annotations(OutMsg),
% #{<<"x_key">> := <<"x_value">>} = amqp10_msg:delivery_annotations(OutMsg), #{<<"y_key">> := <<"y_value">>} = amqp10_msg:delivery_annotations(OutMsg),
?assertEqual([Body], amqp10_msg:body(OutMsg)), ?assertEqual([Body], amqp10_msg:body(OutMsg)),
ok. ok.
@ -379,7 +356,7 @@ filtered_roundtrip(OpenConf, Body) ->
settled, settled,
unsettled_state), unsettled_state),
ok = amqp10_client:send_msg(Sender, Msg1), ok = amqp10_client:send_msg(Sender, Msg1),
{ok, OutMsg1} = amqp10_client:get_msg(DefaultReceiver, 60000 * 5), {ok, OutMsg1} = amqp10_client:get_msg(DefaultReceiver, 60_000 * 4),
?assertEqual(<<"msg-1-tag">>, amqp10_msg:delivery_tag(OutMsg1)), ?assertEqual(<<"msg-1-tag">>, amqp10_msg:delivery_tag(OutMsg1)),
timer:sleep(5 * 1000), timer:sleep(5 * 1000),
@ -398,16 +375,52 @@ filtered_roundtrip(OpenConf, Body) ->
unsettled_state, unsettled_state,
#{<<"apache.org:selector-filter:string">> => <<"amqp.annotation.x-opt-enqueuedtimeutc > ", Now2Binary/binary>>}), #{<<"apache.org:selector-filter:string">> => <<"amqp.annotation.x-opt-enqueuedtimeutc > ", Now2Binary/binary>>}),
{ok, OutMsg2} = amqp10_client:get_msg(DefaultReceiver, 60000 * 5), {ok, OutMsg2} = amqp10_client:get_msg(DefaultReceiver, 60_000 * 4),
?assertEqual(<<"msg-2-tag">>, amqp10_msg:delivery_tag(OutMsg2)), ?assertEqual(<<"msg-2-tag">>, amqp10_msg:delivery_tag(OutMsg2)),
{ok, OutMsgFiltered} = amqp10_client:get_msg(FilteredReceiver, 60000 * 5), {ok, OutMsgFiltered} = amqp10_client:get_msg(FilteredReceiver, 60_000 * 4),
?assertEqual(<<"msg-2-tag">>, amqp10_msg:delivery_tag(OutMsgFiltered)), ?assertEqual(<<"msg-2-tag">>, amqp10_msg:delivery_tag(OutMsgFiltered)),
ok = amqp10_client:end_session(Session), ok = amqp10_client:end_session(Session),
ok = amqp10_client:close_connection(Connection), ok = amqp10_client:close_connection(Connection),
ok. ok.
%% Assert that implementations respect the difference between transfer-id and delivery-id.
transfer_id_vs_delivery_id(Config) ->
Hostname = ?config(rmq_hostname, Config),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
OpenConf = #{address => Hostname, port => Port, sasl => anon},
{ok, Connection} = amqp10_client:open_connection(OpenConf),
{ok, Session} = amqp10_client:begin_session(Connection),
{ok, Sender} = amqp10_client:attach_sender_link(
Session, <<"banana-sender">>, <<"test1">>, settled, unsettled_state),
await_link(Sender, credited, link_credit_timeout),
P0 = binary:copy(<<0>>, 8_000_000),
P1 = <<P0/binary, 1>>,
P2 = <<P0/binary, 2>>,
Msg1 = amqp10_msg:new(<<"tag 1">>, P1, true),
Msg2 = amqp10_msg:new(<<"tag 2">>, P2, true),
ok = amqp10_client:send_msg(Sender, Msg1),
ok = amqp10_client:send_msg(Sender, Msg2),
ok = amqp10_client:detach_link(Sender),
await_link(Sender, {detached, normal}, link_detach_timeout),
{ok, Receiver} = amqp10_client:attach_receiver_link(
Session, <<"banana-receiver">>, <<"test1">>, settled, unsettled_state),
{ok, RcvMsg1} = amqp10_client:get_msg(Receiver, 60_000 * 4),
{ok, RcvMsg2} = amqp10_client:get_msg(Receiver, 60_000 * 4),
ok = amqp10_client:end_session(Session),
ok = amqp10_client:close_connection(Connection),
?assertEqual([P1], amqp10_msg:body(RcvMsg1)),
?assertEqual([P2], amqp10_msg:body(RcvMsg2)),
%% Despite many transfers, there were only 2 deliveries.
%% Therefore, delivery-id should have been increased by just 1.
?assertEqual(serial_number:add(amqp10_msg:delivery_id(RcvMsg1), 1),
amqp10_msg:delivery_id(RcvMsg2)).
% a message is sent before the link attach is guaranteed to % a message is sent before the link attach is guaranteed to
% have completed and link credit granted % have completed and link credit granted
% also queue a link detached immediately after transfer % also queue a link detached immediately after transfer
@ -676,11 +689,13 @@ incoming_heartbeat(Config) ->
idle_time_out => 1000, notify => self()}, idle_time_out => 1000, notify => self()},
{ok, Connection} = amqp10_client:open_connection(CConf), {ok, Connection} = amqp10_client:open_connection(CConf),
receive receive
{amqp10_event, {connection, Connection, {amqp10_event,
{closed, {resource_limit_exceeded, <<"remote idle-time-out">>}}}} -> {connection, Connection0,
{closed, {resource_limit_exceeded, <<"remote idle-time-out">>}}}}
when Connection0 =:= Connection ->
ok ok
after 5000 -> after 5000 ->
exit(incoming_heartbeat_assert) exit(incoming_heartbeat_assert)
end, end,
demonitor(MockRef). demonitor(MockRef).
@ -704,7 +719,8 @@ publish_messages(Sender, Data, Num) ->
receive_one(Receiver) -> receive_one(Receiver) ->
receive receive
{amqp10_msg, Receiver, Msg} -> {amqp10_msg, Receiver0, Msg}
when Receiver0 =:= Receiver ->
amqp10_client:accept_msg(Receiver, Msg) amqp10_client:accept_msg(Receiver, Msg)
after 2000 -> after 2000 ->
timeout timeout
@ -712,7 +728,8 @@ receive_one(Receiver) ->
await_disposition(DeliveryTag) -> await_disposition(DeliveryTag) ->
receive receive
{amqp10_disposition, {accepted, DeliveryTag}} -> ok {amqp10_disposition, {accepted, DeliveryTag0}}
when DeliveryTag0 =:= DeliveryTag -> ok
after 3000 -> after 3000 ->
flush(), flush(),
exit(dispostion_timeout) exit(dispostion_timeout)
@ -720,9 +737,12 @@ await_disposition(DeliveryTag) ->
await_link(Who, What, Err) -> await_link(Who, What, Err) ->
receive receive
{amqp10_event, {link, Who, What}} -> {amqp10_event, {link, Who0, What0}}
when Who0 =:= Who andalso
What0 =:= What ->
ok; ok;
{amqp10_event, {link, Who, {detached, Why}}} -> {amqp10_event, {link, Who0, {detached, Why}}}
when Who0 =:= Who ->
exit(Why) exit(Why)
after 5000 -> after 5000 ->
flush(), flush(),

View File

@ -116,6 +116,11 @@ rabbitmq_suite(
name = "binary_parser_SUITE", name = "binary_parser_SUITE",
) )
rabbitmq_suite(
name = "serial_number_SUITE",
size = "small",
)
assert_suites() assert_suites()
alias( alias(

View File

@ -13,6 +13,7 @@ def all_beam_files(name = "all_beam_files"):
"src/amqp10_binary_parser.erl", "src/amqp10_binary_parser.erl",
"src/amqp10_framing.erl", "src/amqp10_framing.erl",
"src/amqp10_framing0.erl", "src/amqp10_framing0.erl",
"src/serial_number.erl",
], ],
hdrs = [":public_and_private_hdrs"], hdrs = [":public_and_private_hdrs"],
app_name = "amqp10_common", app_name = "amqp10_common",
@ -34,6 +35,7 @@ def all_test_beam_files(name = "all_test_beam_files"):
"src/amqp10_binary_parser.erl", "src/amqp10_binary_parser.erl",
"src/amqp10_framing.erl", "src/amqp10_framing.erl",
"src/amqp10_framing0.erl", "src/amqp10_framing0.erl",
"src/serial_number.erl",
], ],
hdrs = [":public_and_private_hdrs"], hdrs = [":public_and_private_hdrs"],
app_name = "amqp10_common", app_name = "amqp10_common",
@ -62,11 +64,12 @@ def all_srcs(name = "all_srcs"):
"src/amqp10_binary_parser.erl", "src/amqp10_binary_parser.erl",
"src/amqp10_framing.erl", "src/amqp10_framing.erl",
"src/amqp10_framing0.erl", "src/amqp10_framing0.erl",
"src/serial_number.erl",
], ],
) )
filegroup( filegroup(
name = "public_hdrs", name = "public_hdrs",
srcs = ["include/amqp10_framing.hrl"], srcs = ["include/amqp10_framing.hrl", "include/amqp10_types.hrl"],
) )
filegroup( filegroup(
name = "private_hdrs", name = "private_hdrs",
@ -96,3 +99,11 @@ def test_suite_beam_files(name = "test_suite_beam_files"):
app_name = "amqp10_common", app_name = "amqp10_common",
erlc_opts = "//:test_erlc_opts", erlc_opts = "//:test_erlc_opts",
) )
erlang_bytecode(
name = "serial_number_SUITE_beam_files",
testonly = True,
srcs = ["test/serial_number_SUITE.erl"],
outs = ["test/serial_number_SUITE.beam"],
app_name = "amqp10_common",
erlc_opts = "//:test_erlc_opts",
)

View File

@ -0,0 +1,12 @@
-define(UINT_MAX, 16#ff_ff_ff_ff).
% [1.6.5]
-type uint() :: 0..?UINT_MAX.
% [2.8.4]
-type link_handle() :: uint().
% [2.8.8]
-type delivery_number() :: sequence_no().
% [2.8.9]
-type transfer_number() :: sequence_no().
% [2.8.10]
-type sequence_no() :: uint().

View File

@ -117,11 +117,8 @@ parse_compound(UnitSize, Bin) ->
parse_compound1(0, <<>>, List) -> parse_compound1(0, <<>>, List) ->
lists:reverse(List); lists:reverse(List);
parse_compound1(_Left, <<>>, List) -> parse_compound1(_Left, <<>>, _List) ->
case application:get_env(rabbitmq_amqp1_0, protocol_strict_mode) of throw(compound_datatype_miscount);
{ok, false} -> lists:reverse(List); %% ignore miscount
{ok, true} -> throw(compound_datatype_miscount)
end;
parse_compound1(Count, Bin, Acc) -> parse_compound1(Count, Bin, Acc) ->
{Value, Rest} = parse(Bin), {Value, Rest} = parse(Bin),
parse_compound1(Count - 1, Rest, [Value | Acc]). parse_compound1(Count - 1, Rest, [Value | Acc]).

118
deps/amqp10_common/src/serial_number.erl vendored Normal file
View File

@ -0,0 +1,118 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.
%% https://www.ietf.org/rfc/rfc1982.txt
-module(serial_number).
-include("amqp10_types.hrl").
-export([add/2,
compare/2,
ranges/1,
diff/2,
foldl/4]).
-ifdef(TEST).
-export([usort/1]).
-endif.
-type serial_number() :: sequence_no().
-export_type([serial_number/0]).
%% SERIAL_BITS = 32
%% 2 ^ SERIAL_BITS
-define(SERIAL_SPACE, 16#100000000).
%% 2 ^ (SERIAL_BITS - 1) - 1
-define(SERIAL_MAX_ADDEND, 16#7fffffff).
-spec add(serial_number(), non_neg_integer()) ->
serial_number().
add(S, N)
when N >= 0 andalso
N =< ?SERIAL_MAX_ADDEND ->
(S + N) rem ?SERIAL_SPACE;
add(S, N) ->
exit({undefined_serial_addition, S, N}).
%% 2 ^ (SERIAL_BITS - 1)
-define(COMPARE, 2_147_483_648).
-spec compare(serial_number(), serial_number()) ->
equal | less | greater.
compare(A, B) ->
if A =:= B ->
equal;
(A < B andalso B - A < ?COMPARE) orelse
(A > B andalso A - B > ?COMPARE) ->
less;
(A < B andalso B - A > ?COMPARE) orelse
(A > B andalso A - B < ?COMPARE) ->
greater;
true ->
exit({undefined_serial_comparison, A, B})
end.
-spec usort([serial_number()]) ->
[serial_number()].
usort(L) ->
lists:usort(fun(A, B) ->
case compare(A, B) of
greater -> false;
_ -> true
end
end, L).
%% Takes a list of serial numbers and returns tuples
%% {First, Last} representing contiguous serial numbers.
-spec ranges([serial_number()]) ->
[{First :: serial_number(), Last :: serial_number()}].
ranges([]) ->
[];
ranges(SerialNumbers) ->
[First | Rest] = usort(SerialNumbers),
ranges0(Rest, [{First, First}]).
ranges0([], Acc) ->
lists:reverse(Acc);
ranges0([H | Rest], [{First, Last} | AccRest] = Acc0) ->
case add(Last, 1) of
H ->
Acc = [{First, H} | AccRest],
ranges0(Rest, Acc);
_ ->
Acc = [{H, H} | Acc0],
ranges0(Rest, Acc)
end.
-define(SERIAL_DIFF_BOUND, 16#80000000).
-spec diff(serial_number(), serial_number()) -> integer().
diff(A, B) ->
Diff = A - B,
if Diff > (?SERIAL_DIFF_BOUND) ->
%% B is actually greater than A
- (?SERIAL_SPACE - Diff);
Diff < - (?SERIAL_DIFF_BOUND) ->
?SERIAL_SPACE + Diff;
Diff < ?SERIAL_DIFF_BOUND andalso Diff > -?SERIAL_DIFF_BOUND ->
Diff;
true ->
exit({undefined_serial_diff, A, B})
end.
-spec foldl(Fun, Acc0, First, Last) -> Acc1 when
Fun :: fun((serial_number(), AccIn) -> AccOut),
Acc0 :: term(),
Acc1 :: term(),
AccIn :: term(),
AccOut :: term(),
First :: serial_number(),
Last :: serial_number().
foldl(Fun, Acc0, Current, Last) ->
Acc = Fun(Current, Acc0),
case compare(Current, Last) of
less -> foldl(Fun, Acc, add(Current, 1), Last);
equal -> Acc
end.

View File

@ -0,0 +1,124 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.
%%
-module(serial_number_SUITE).
-include_lib("eunit/include/eunit.hrl").
-compile([export_all,
nowarn_export_all]).
-import(serial_number, [add/2,
compare/2,
usort/1,
ranges/1,
diff/2,
foldl/4]).
all() -> [test_add,
test_compare,
test_usort,
test_ranges,
test_diff,
test_foldl].
test_add(_Config) ->
?assertEqual(1, add(0, 1)),
%% "Addition of a value outside the range
%% [0 .. (2^(SERIAL_BITS - 1) - 1)] is undefined."
MaxAddend = round(math:pow(2, 32 - 1) - 1),
MinAddend = 0,
?assertEqual(MaxAddend, add(0, MaxAddend)),
?assertEqual(MinAddend, add(0, MinAddend)),
?assertEqual(0, add(16#ffffffff, 1)),
?assertEqual(1, add(16#ffffffff, 2)),
AddendTooLarge = MaxAddend + 1,
?assertExit({undefined_serial_addition, 0, AddendTooLarge},
add(0, AddendTooLarge)),
AddendTooSmall = MinAddend - 1,
?assertExit({undefined_serial_addition, 0, AddendTooSmall},
add(0, AddendTooSmall)).
test_compare(_Config) ->
?assertEqual(equal, compare(0, 0)),
?assertEqual(equal, compare(16#ffffffff, 16#ffffffff)),
?assertEqual(less, compare(0, 1)),
?assertEqual(greater, compare(1, 0)),
?assertEqual(less, compare(0, 2)),
?assertEqual(less, compare(0, round(math:pow(2, 32 - 1)) - 1)),
?assertExit({undefined_serial_comparison, 0, _},
compare(0, round(math:pow(2, 32 - 1)))),
?assertEqual(less, compare(16#ffffffff - 5, 30_000)),
?assertEqual(greater, compare(1, 0)),
?assertEqual(greater, compare(2147483647, 0)),
?assertExit({undefined_serial_comparison, 2147483648, 0},
compare(2147483648, 0)).
test_usort(_Config) ->
?assertEqual([],
usort([])),
?assertEqual([3],
usort([3])),
?assertEqual([0],
usort([0, 0])),
?assertEqual([4294967000, 4294967293, 4294967294, 4294967295, 0, 3, 4],
usort([3, 4294967295, 4294967295, 4294967293, 4294967000, 4294967294, 0, 4])).
test_ranges(_Config) ->
?assertEqual([],
ranges([])),
?assertEqual([{0, 0}],
ranges([0])),
?assertEqual([{0, 1}],
ranges([0, 1])),
?assertEqual([{0, 1}],
ranges([1, 0])),
?assertEqual([{0, 0}, {2, 2}],
ranges([0, 2])),
?assertEqual([{0, 0}, {2, 2}],
ranges([2, 0])),
%% 2 ^ 32 - 1 = 4294967295
?assertEqual([{4294967290, 4294967290}, {4294967295, 4294967295}],
ranges([4294967290, 4294967295])),
?assertEqual([{4294967290, 4294967290}, {4294967295, 4294967295}],
ranges([4294967295, 4294967290])),
?assertEqual([{4294967294, 4294967294}, {0, 0}],
ranges([4294967294, 0])),
?assertEqual([{4294967294, 4294967294}, {0, 0}],
ranges([0, 4294967294])),
?assertEqual([{4294967295, 0}],
ranges([4294967295, 0])),
?assertEqual([{4294967294, 1}, {3, 5}, {10, 10}, {18, 19}],
ranges([4294967294, 4294967295, 0, 1, 3, 4, 5, 10, 18, 19])),
?assertEqual([{4294967294, 1}, {3, 5}, {10, 10}, {18, 19}],
ranges([1, 10, 4294967294, 0, 3, 4, 5, 19, 18, 4294967295])).
test_diff(_Config) ->
?assertEqual(0, diff(0, 0)),
?assertEqual(0, diff(1, 1)),
?assertEqual(0, diff(16#ffffffff, 16#ffffffff)),
?assertEqual(1, diff(1, 0)),
?assertEqual(2, diff(1, 16#ffffffff)),
?assertEqual(6, diff(0, 16#fffffffa)),
?assertEqual(206, diff(200, 16#fffffffa)),
?assertEqual(-2, diff(16#ffffffff, 1)),
?assertExit({undefined_serial_diff, _, _},
diff(0, 16#80000000)),
?assertExit({undefined_serial_diff, _, _},
diff(16#ffffffff, 16#7fffffff)).
test_foldl(_Config) ->
?assertEqual(
[16#ffffffff - 1, 16#ffffffff, 0, 1],
foldl(fun(S, Acc) ->
Acc ++ [S]
end, [], 16#ffffffff - 1, 1)),
?assertEqual(
[0],
foldl(fun(S, Acc) ->
Acc ++ [S]
end, [], 0, 0)).

View File

@ -797,11 +797,6 @@ handle_method_from_server1(#'basic.nack'{} = BasicNack, none,
#state{confirm_handler = {CH, _Ref}} = State) -> #state{confirm_handler = {CH, _Ref}} = State) ->
CH ! BasicNack, CH ! BasicNack,
{noreply, update_confirm_set(BasicNack, State)}; {noreply, update_confirm_set(BasicNack, State)};
handle_method_from_server1(#'basic.credit_drained'{} = CreditDrained, none,
#state{consumer = Consumer} = State) ->
Consumer ! CreditDrained,
{noreply, State};
handle_method_from_server1(Method, none, State) -> handle_method_from_server1(Method, none, State) ->
{noreply, rpc_bottom_half(Method, State)}; {noreply, rpc_bottom_half(Method, State)};
handle_method_from_server1(Method, Content, State) -> handle_method_from_server1(Method, Content, State) ->

View File

@ -176,10 +176,7 @@ handle_info({'DOWN', _MRef, process, Pid, _Info},
_ -> {ok, State} %% unnamed consumer went down _ -> {ok, State} %% unnamed consumer went down
%% before receiving consume_ok %% before receiving consume_ok
end end
end; end.
handle_info(#'basic.credit_drained'{} = Method, State) ->
deliver_to_consumer_or_die(Method, Method, State),
{ok, State}.
%% @private %% @private
handle_call({register_default_consumer, Pid}, _From, handle_call({register_default_consumer, Pid}, _From,
@ -246,8 +243,7 @@ tag(#'basic.consume'{consumer_tag = Tag}) -> Tag;
tag(#'basic.consume_ok'{consumer_tag = Tag}) -> Tag; tag(#'basic.consume_ok'{consumer_tag = Tag}) -> Tag;
tag(#'basic.cancel'{consumer_tag = Tag}) -> Tag; tag(#'basic.cancel'{consumer_tag = Tag}) -> Tag;
tag(#'basic.cancel_ok'{consumer_tag = Tag}) -> Tag; tag(#'basic.cancel_ok'{consumer_tag = Tag}) -> Tag;
tag(#'basic.deliver'{consumer_tag = Tag}) -> Tag; tag(#'basic.deliver'{consumer_tag = Tag}) -> Tag.
tag(#'basic.credit_drained'{consumer_tag = Tag}) -> Tag.
add_to_monitor_dict(Pid, Monitors) -> add_to_monitor_dict(Pid, Monitors) ->
case maps:find(Pid, Monitors) of case maps:find(Pid, Monitors) of

View File

@ -9,8 +9,7 @@
-export([init_state/0, dest_prefixes/0, all_dest_prefixes/0]). -export([init_state/0, dest_prefixes/0, all_dest_prefixes/0]).
-export([ensure_endpoint/4, ensure_endpoint/5, ensure_binding/3]). -export([ensure_endpoint/4, ensure_endpoint/5, ensure_binding/3]).
-export([parse_endpoint/1, parse_endpoint/2]). -export([dest_temp_queue/1]).
-export([parse_routing/1, dest_temp_queue/1]).
-include("amqp_client.hrl"). -include("amqp_client.hrl").
-include("rabbit_routing_prefixes.hrl"). -include("rabbit_routing_prefixes.hrl").
@ -26,50 +25,6 @@ all_dest_prefixes() -> [?TEMP_QUEUE_PREFIX | dest_prefixes()].
%% -------------------------------------------------------------------------- %% --------------------------------------------------------------------------
parse_endpoint(Destination) ->
parse_endpoint(Destination, false).
parse_endpoint(undefined, AllowAnonymousQueue) ->
parse_endpoint("/queue", AllowAnonymousQueue);
parse_endpoint(Destination, AllowAnonymousQueue) when is_binary(Destination) ->
parse_endpoint(unicode:characters_to_list(Destination),
AllowAnonymousQueue);
parse_endpoint(Destination, AllowAnonymousQueue) when is_list(Destination) ->
case re:split(Destination, "/", [{return, list}]) of
[Name] ->
{ok, {queue, unescape(Name)}};
["", Type | Rest]
when Type =:= "exchange" orelse Type =:= "queue" orelse
Type =:= "topic" orelse Type =:= "temp-queue" ->
parse_endpoint0(atomise(Type), Rest, AllowAnonymousQueue);
["", "amq", "queue" | Rest] ->
parse_endpoint0(amqqueue, Rest, AllowAnonymousQueue);
["", "reply-queue" = Prefix | [_|_]] ->
parse_endpoint0(reply_queue,
[lists:nthtail(2 + length(Prefix), Destination)],
AllowAnonymousQueue);
_ ->
{error, {unknown_destination, Destination}}
end.
parse_endpoint0(exchange, ["" | _] = Rest, _) ->
{error, {invalid_destination, exchange, to_url(Rest)}};
parse_endpoint0(exchange, [Name], _) ->
{ok, {exchange, {unescape(Name), undefined}}};
parse_endpoint0(exchange, [Name, Pattern], _) ->
{ok, {exchange, {unescape(Name), unescape(Pattern)}}};
parse_endpoint0(queue, [], false) ->
{error, {invalid_destination, queue, []}};
parse_endpoint0(queue, [], true) ->
{ok, {queue, undefined}};
parse_endpoint0(Type, [[_|_]] = [Name], _) ->
{ok, {Type, unescape(Name)}};
parse_endpoint0(Type, Rest, _) ->
{error, {invalid_destination, Type, to_url(Rest)}}.
%% --------------------------------------------------------------------------
ensure_endpoint(Dir, Channel, Endpoint, State) -> ensure_endpoint(Dir, Channel, Endpoint, State) ->
ensure_endpoint(Dir, Channel, Endpoint, [], State). ensure_endpoint(Dir, Channel, Endpoint, [], State).
@ -140,16 +95,6 @@ ensure_binding(Queue, {Exchange, RoutingKey}, Channel) ->
%% -------------------------------------------------------------------------- %% --------------------------------------------------------------------------
parse_routing({exchange, {Name, undefined}}) ->
{Name, ""};
parse_routing({exchange, {Name, Pattern}}) ->
{Name, Pattern};
parse_routing({topic, Name}) ->
{"amq.topic", Name};
parse_routing({Type, Name})
when Type =:= queue orelse Type =:= reply_queue orelse Type =:= amqqueue ->
{"", Name}.
dest_temp_queue({temp_queue, Name}) -> Name; dest_temp_queue({temp_queue, Name}) -> Name;
dest_temp_queue(_) -> none. dest_temp_queue(_) -> none.
@ -206,17 +151,3 @@ queue_declare_method(#'queue.declare'{} = Method, Type, Params) ->
_ -> _ ->
Method2 Method2
end. end.
%% --------------------------------------------------------------------------
to_url([]) -> [];
to_url(Lol) -> "/" ++ string:join(Lol, "/").
atomise(Name) when is_list(Name) ->
list_to_atom(re:replace(Name, "-", "_", [{return,list}, global])).
unescape(Str) -> unescape(Str, []).
unescape("%2F" ++ Str, Acc) -> unescape(Str, [$/ | Acc]);
unescape([C | Str], Acc) -> unescape(Str, [C | Acc]);
unescape([], Acc) -> lists:reverse(Acc).

View File

@ -1342,9 +1342,9 @@ channel_writer_death(Config) ->
Ret = amqp_channel:call(Channel, QoS), Ret = amqp_channel:call(Channel, QoS),
throw({unexpected_success, Ret}) throw({unexpected_success, Ret})
catch catch
exit:{{function_clause, exit:{{{badrecord, <<>>},
[{rabbit_channel, check_user_id_header, _, _} | _]}, _} [{rabbit_channel, _, _, _} | _]}, _}
when ConnType =:= direct -> ok; when ConnType =:= direct -> ok;
exit:{{infrastructure_died, {unknown_properties_record, <<>>}}, _} exit:{{infrastructure_died, {unknown_properties_record, <<>>}}, _}
when ConnType =:= network -> ok when ConnType =:= network -> ok

View File

@ -322,9 +322,9 @@ route_destination_parsing(_Config) ->
ok. ok.
parse_dest(Destination, Params) -> parse_dest(Destination, Params) ->
rabbit_routing_util:parse_endpoint(Destination, Params). rabbit_routing_parser:parse_endpoint(Destination, Params).
parse_dest(Destination) -> parse_dest(Destination) ->
rabbit_routing_util:parse_endpoint(Destination). rabbit_routing_parser:parse_endpoint(Destination).
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------
%% Topic variable map %% Topic variable map

View File

@ -40,3 +40,6 @@ callgraph.dot*
PACKAGES/* PACKAGES/*
rabbit-rabbitmq-deps.mk rabbit-rabbitmq-deps.mk
[Bb]in/
[Oo]bj/

View File

@ -59,6 +59,8 @@ _APP_ENV = """[
{default_user_tags, [administrator]}, {default_user_tags, [administrator]},
{default_vhost, <<"/">>}, {default_vhost, <<"/">>},
{default_permissions, [<<".*">>, <<".*">>, <<".*">>]}, {default_permissions, [<<".*">>, <<".*">>, <<".*">>]},
{amqp1_0_default_user, <<"guest">>},
{amqp1_0_default_vhost, <<"/">>},
{loopback_users, [<<"guest">>]}, {loopback_users, [<<"guest">>]},
{password_hashing_module, rabbit_password_hashing_sha256}, {password_hashing_module, rabbit_password_hashing_sha256},
{server_properties, []}, {server_properties, []},
@ -234,6 +236,9 @@ rabbitmq_app(
xref( xref(
name = "xref", name = "xref",
additional_libs = [
"//deps/rabbitmq_cli:erlang_app", # keep
],
target = ":erlang_app", target = ":erlang_app",
) )
@ -245,8 +250,10 @@ plt(
], ],
for_target = ":erlang_app", for_target = ":erlang_app",
ignore_warnings = True, ignore_warnings = True,
libs = ["//deps/rabbitmq_cli:elixir"], # keep
plt = "//:base_plt", plt = "//:base_plt",
deps = [ deps = [
"//deps/rabbitmq_cli:erlang_app", # keep
"@looking_glass//:erlang_app", # keep "@looking_glass//:erlang_app", # keep
], ],
) )
@ -273,6 +280,7 @@ rabbitmq_home(
plugins = [ plugins = [
":test_erlang_app", ":test_erlang_app",
"//deps/rabbitmq_ct_client_helpers:erlang_app", "//deps/rabbitmq_ct_client_helpers:erlang_app",
"//deps/rabbitmq_amqp1_0:erlang_app",
"@inet_tcp_proxy_dist//:erlang_app", "@inet_tcp_proxy_dist//:erlang_app",
"@meck//:erlang_app", "@meck//:erlang_app",
], ],
@ -1236,6 +1244,52 @@ rabbitmq_integration_suite(
], ],
) )
rabbitmq_integration_suite(
name = "amqp_client_SUITE",
size = "large",
additional_beam = [
":test_event_recorder_beam",
],
shard_count = 3,
runtime_deps = [
"//deps/amqp10_client:erlang_app",
],
)
rabbitmq_integration_suite(
name = "amqp_proxy_protocol_SUITE",
size = "medium",
)
rabbitmq_integration_suite(
name = "amqp_system_SUITE",
flaky = True,
shard_count = 2,
tags = [
"dotnet",
],
test_env = {
"TMPDIR": "$TEST_TMPDIR",
},
)
rabbitmq_integration_suite(
name = "amqp_auth_SUITE",
additional_beam = [
":test_event_recorder_beam",
],
runtime_deps = [
"//deps/amqp10_client:erlang_app",
],
)
rabbitmq_integration_suite(
name = "amqp_credit_api_v2_SUITE",
runtime_deps = [
"//deps/amqp10_client:erlang_app",
],
)
assert_suites() assert_suites()
filegroup( filegroup(
@ -1332,6 +1386,7 @@ eunit(
":test_test_util_beam", ":test_test_util_beam",
":test_test_rabbit_event_handler_beam", ":test_test_rabbit_event_handler_beam",
":test_clustering_utils_beam", ":test_clustering_utils_beam",
":test_event_recorder_beam",
], ],
target = ":test_erlang_app", target = ":test_erlang_app",
test_env = { test_env = {

View File

@ -39,6 +39,8 @@ define PROJECT_ENV
{default_user_tags, [administrator]}, {default_user_tags, [administrator]},
{default_vhost, <<"/">>}, {default_vhost, <<"/">>},
{default_permissions, [<<".*">>, <<".*">>, <<".*">>]}, {default_permissions, [<<".*">>, <<".*">>, <<".*">>]},
{amqp1_0_default_user, <<"guest">>},
{amqp1_0_default_vhost, <<"/">>},
{loopback_users, [<<"guest">>]}, {loopback_users, [<<"guest">>]},
{password_hashing_module, rabbit_password_hashing_sha256}, {password_hashing_module, rabbit_password_hashing_sha256},
{server_properties, []}, {server_properties, []},
@ -133,8 +135,8 @@ endef
LOCAL_DEPS = sasl os_mon inets compiler public_key crypto ssl syntax_tools xmerl LOCAL_DEPS = sasl os_mon inets compiler public_key crypto ssl syntax_tools xmerl
BUILD_DEPS = rabbitmq_cli BUILD_DEPS = rabbitmq_cli
DEPS = ranch rabbit_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris amqp10_common syslog systemd seshat khepri khepri_mnesia_migration DEPS = ranch rabbit_common amqp10_common rabbitmq_prelaunch ra sysmon_handler stdout_formatter recon redbug observer_cli osiris syslog systemd seshat khepri khepri_mnesia_migration
TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers amqp_client meck proper TEST_DEPS = rabbitmq_ct_helpers rabbitmq_ct_client_helpers meck proper amqp_client amqp10_client rabbitmq_amqp1_0
PLT_APPS += mnesia PLT_APPS += mnesia

77
deps/rabbit/app.bzl vendored
View File

@ -24,7 +24,7 @@ def all_beam_files(name = "all_beam_files"):
app_name = "rabbit", app_name = "rabbit",
dest = "ebin", dest = "ebin",
erlc_opts = "//:erlc_opts", erlc_opts = "//:erlc_opts",
deps = ["//deps/rabbit_common:erlang_app"], deps = ["//deps/amqp10_common:erlang_app", "//deps/rabbit_common:erlang_app"],
) )
erlang_bytecode( erlang_bytecode(
name = "other_beam", name = "other_beam",
@ -46,6 +46,12 @@ def all_beam_files(name = "all_beam_files"):
"src/rabbit.erl", "src/rabbit.erl",
"src/rabbit_access_control.erl", "src/rabbit_access_control.erl",
"src/rabbit_alarm.erl", "src/rabbit_alarm.erl",
"src/rabbit_amqp1_0.erl",
"src/rabbit_amqp_reader.erl",
"src/rabbit_amqp_session.erl",
"src/rabbit_amqp_session_sup.erl",
"src/rabbit_amqp_util.erl",
"src/rabbit_amqp_writer.erl",
"src/rabbit_amqqueue.erl", "src/rabbit_amqqueue.erl",
"src/rabbit_amqqueue_control.erl", "src/rabbit_amqqueue_control.erl",
"src/rabbit_amqqueue_process.erl", "src/rabbit_amqqueue_process.erl",
@ -286,7 +292,7 @@ def all_test_beam_files(name = "all_test_beam_files"):
app_name = "rabbit", app_name = "rabbit",
dest = "test", dest = "test",
erlc_opts = "//:test_erlc_opts", erlc_opts = "//:test_erlc_opts",
deps = ["//deps/rabbit_common:erlang_app"], deps = ["//deps/amqp10_common:erlang_app", "//deps/rabbit_common:erlang_app"],
) )
erlang_bytecode( erlang_bytecode(
name = "test_other_beam", name = "test_other_beam",
@ -309,6 +315,12 @@ def all_test_beam_files(name = "all_test_beam_files"):
"src/rabbit.erl", "src/rabbit.erl",
"src/rabbit_access_control.erl", "src/rabbit_access_control.erl",
"src/rabbit_alarm.erl", "src/rabbit_alarm.erl",
"src/rabbit_amqp1_0.erl",
"src/rabbit_amqp_reader.erl",
"src/rabbit_amqp_session.erl",
"src/rabbit_amqp_session_sup.erl",
"src/rabbit_amqp_util.erl",
"src/rabbit_amqp_writer.erl",
"src/rabbit_amqqueue.erl", "src/rabbit_amqqueue.erl",
"src/rabbit_amqqueue_control.erl", "src/rabbit_amqqueue_control.erl",
"src/rabbit_amqqueue_process.erl", "src/rabbit_amqqueue_process.erl",
@ -541,6 +553,7 @@ def all_srcs(name = "all_srcs"):
"include/gm_specs.hrl", "include/gm_specs.hrl",
"include/internal_user.hrl", "include/internal_user.hrl",
"include/mc.hrl", "include/mc.hrl",
"include/rabbit_amqp.hrl",
"include/rabbit_global_counters.hrl", "include/rabbit_global_counters.hrl",
"include/vhost.hrl", "include/vhost.hrl",
"include/vhost_v2.hrl", "include/vhost_v2.hrl",
@ -586,6 +599,12 @@ def all_srcs(name = "all_srcs"):
"src/rabbit.erl", "src/rabbit.erl",
"src/rabbit_access_control.erl", "src/rabbit_access_control.erl",
"src/rabbit_alarm.erl", "src/rabbit_alarm.erl",
"src/rabbit_amqp1_0.erl",
"src/rabbit_amqp_reader.erl",
"src/rabbit_amqp_session.erl",
"src/rabbit_amqp_session_sup.erl",
"src/rabbit_amqp_util.erl",
"src/rabbit_amqp_writer.erl",
"src/rabbit_amqqueue.erl", "src/rabbit_amqqueue.erl",
"src/rabbit_amqqueue_control.erl", "src/rabbit_amqqueue_control.erl",
"src/rabbit_amqqueue_process.erl", "src/rabbit_amqqueue_process.erl",
@ -2147,3 +2166,57 @@ def test_suite_beam_files(name = "test_suite_beam_files"):
erlc_opts = "//:test_erlc_opts", erlc_opts = "//:test_erlc_opts",
deps = ["//deps/amqp_client:erlang_app"], deps = ["//deps/amqp_client:erlang_app"],
) )
erlang_bytecode(
name = "test_event_recorder_beam",
testonly = True,
srcs = ["test/event_recorder.erl"],
outs = ["test/event_recorder.beam"],
app_name = "rabbit",
erlc_opts = "//:test_erlc_opts",
deps = ["//deps/rabbit_common:erlang_app"],
)
erlang_bytecode(
name = "amqp_auth_SUITE_beam_files",
testonly = True,
srcs = ["test/amqp_auth_SUITE.erl"],
outs = ["test/amqp_auth_SUITE.beam"],
app_name = "rabbit",
erlc_opts = "//:test_erlc_opts",
deps = ["//deps/amqp10_common:erlang_app", "//deps/amqp_client:erlang_app"],
)
erlang_bytecode(
name = "amqp_client_SUITE_beam_files",
testonly = True,
srcs = ["test/amqp_client_SUITE.erl"],
outs = ["test/amqp_client_SUITE.beam"],
app_name = "rabbit",
erlc_opts = "//:test_erlc_opts",
deps = ["//deps/amqp10_common:erlang_app", "//deps/amqp_client:erlang_app"],
)
erlang_bytecode(
name = "amqp_credit_api_v2_SUITE_beam_files",
testonly = True,
srcs = ["test/amqp_credit_api_v2_SUITE.erl"],
outs = ["test/amqp_credit_api_v2_SUITE.beam"],
app_name = "rabbit",
erlc_opts = "//:test_erlc_opts",
deps = ["//deps/amqp_client:erlang_app"],
)
erlang_bytecode(
name = "amqp_proxy_protocol_SUITE_beam_files",
testonly = True,
srcs = ["test/amqp_proxy_protocol_SUITE.erl"],
outs = ["test/amqp_proxy_protocol_SUITE.beam"],
app_name = "rabbit",
erlc_opts = "//:test_erlc_opts",
)
erlang_bytecode(
name = "amqp_system_SUITE_beam_files",
testonly = True,
srcs = ["test/amqp_system_SUITE.erl"],
outs = ["test/amqp_system_SUITE.beam"],
app_name = "rabbit",
erlc_opts = "//:test_erlc_opts",
deps = ["//deps/rabbit_common:erlang_app"],
)

View File

@ -954,10 +954,6 @@
## ##
# amqp1_0.default_user = guest # amqp1_0.default_user = guest
## Enable protocol strict mode. See the README for more information.
##
# amqp1_0.protocol_strict_mode = false
## Logging settings. ## Logging settings.
## ##
## See https://rabbitmq.com/logging.html for details. ## See https://rabbitmq.com/logging.html for details.

74
deps/rabbit/include/rabbit_amqp.hrl vendored Normal file
View File

@ -0,0 +1,74 @@
%%-define(debug, true).
-ifdef(debug).
-define(DEBUG0(F), ?SAFE(rabbit_log:debug(F, []))).
-define(DEBUG(F, A), ?SAFE(rabbit_log:debug(F, A))).
-else.
-define(DEBUG0(F), ok).
-define(DEBUG(F, A), ok).
-endif.
-define(pprint(F), rabbit_log:debug("~p~n",
[amqp10_framing:pprint(F)])).
-define(SAFE(F),
((fun() ->
try F
catch __T:__E:__ST ->
rabbit_log:debug("~p:~p thrown debugging~n~p~n",
[__T, __E, __ST])
end
end)())).
%% General consts
%% [2.8.19]
-define(MIN_MAX_FRAME_1_0_SIZE, 512).
-define(SEND_ROLE, false).
-define(RECV_ROLE, true).
%% for rabbit_event user_authentication_success and user_authentication_failure
-define(AUTH_EVENT_KEYS,
[name,
host,
port,
peer_host,
peer_port,
protocol,
auth_mechanism,
ssl,
ssl_protocol,
ssl_key_exchange,
ssl_cipher,
ssl_hash,
peer_cert_issuer,
peer_cert_subject,
peer_cert_validity]).
-define(ITEMS,
[pid,
frame_max,
timeout,
vhost,
user,
node
] ++ ?AUTH_EVENT_KEYS).
-define(INFO_ITEMS,
[connection_state,
recv_oct,
recv_cnt,
send_oct,
send_cnt
] ++ ?ITEMS).
%% for rabbit_event connection_created
-define(CONNECTION_EVENT_KEYS,
[type,
client_properties,
connected_at,
channel_max
] ++ ?ITEMS).
-include_lib("amqp10_common/include/amqp10_framing.hrl").

View File

@ -1,5 +1,4 @@
-define(NUM_PROTOCOL_COUNTERS, 8). -define(NUM_PROTOCOL_COUNTERS, 8).
-define(NUM_PROTOCOL_QUEUE_TYPE_COUNTERS, 8).
%% Dead Letter counters: %% Dead Letter counters:
%% %%

View File

@ -2586,6 +2586,33 @@ end}.
end end
}. }.
% ===============================
% AMQP 1.0
% ===============================
%% Connections that skip SASL layer or use SASL mechanism ANONYMOUS will connect as this account.
%% Setting this to a username will allow clients to connect without authenticating.
%% For production environments, set this value to 'none'.
{mapping, "amqp1_0.default_user", "rabbit.amqp1_0_default_user",
[{datatype, [{enum, [none]}, string]}]}.
{mapping, "amqp1_0.default_vhost", "rabbit.amqp1_0_default_vhost",
[{datatype, string}]}.
{translation, "rabbit.amqp1_0_default_user",
fun(Conf) ->
case cuttlefish:conf_get("amqp1_0.default_user", Conf) of
none -> none;
User -> list_to_binary(User)
end
end}.
{translation , "rabbit.amqp1_0_default_vhost",
fun(Conf) ->
list_to_binary(cuttlefish:conf_get("amqp1_0.default_vhost", Conf))
end}.
% =============================== % ===============================
% Validators % Validators
% =============================== % ===============================

View File

@ -19,6 +19,7 @@
is_persistent/1, is_persistent/1,
ttl/1, ttl/1,
correlation_id/1, correlation_id/1,
user_id/1,
message_id/1, message_id/1,
timestamp/1, timestamp/1,
priority/1, priority/1,
@ -280,6 +281,15 @@ correlation_id(#?MODULE{protocol = Proto,
correlation_id(BasicMsg) -> correlation_id(BasicMsg) ->
mc_compat:correlation_id(BasicMsg). mc_compat:correlation_id(BasicMsg).
-spec user_id(state()) ->
{binary, rabbit_types:username()} |
undefined.
user_id(#?MODULE{protocol = Proto,
data = Data}) ->
Proto:property(?FUNCTION_NAME, Data);
user_id(BasicMsg) ->
mc_compat:user_id(BasicMsg).
-spec message_id(state()) -> -spec message_id(state()) ->
{uuid, binary()} | {uuid, binary()} |
{utf8, binary()} | {utf8, binary()} |

View File

@ -58,14 +58,32 @@
message_section/0 message_section/0
]). ]).
%% mc implementation %% TODO
%% Up to 3.13 the parsed AMQP 1.0 message is never stored on disk.
%% We want that to hold true for 4.0 as well to save disk space and disk I/O.
%%
%% As the essential annotations, durable, priority, ttl and delivery_count
%% is all we are interested in it isn't necessary to keep hold of the
%% incoming AMQP header inside the state
%%
%% Probably prepare(store, Msg) should serialize the message.
%% mc:prepare(store, Msg) should also be called from rabbit_stream_queue after converting to mc_amqp.
%%
%% When we received the message via AMQP 1.0, our mc_amqp:state() should ideally store a binary of each section.
%% This way, prepare(store, Msg) wouldn't need to serialize anything because there shouldn't be any changes
%% in the sections between receiving via AMQP 1.0 and storing the message in queues.
%%
%% Also, we don't need to parse each section.
%% For example, apart from validation we wouldnt need to parse application properties at all - unless requested by the headers exchange.
%% Ideally the parser could have a validate mode, that validated the section(s) but didnt build up an erlang term representation of the data.
%% Such a validation mode could be used for application properties. Message annotations might not need to be parsed either.
%% So, message annotations and application properties should be parsed lazily, only if needed.
%%
%% Upon sending the message to clients, when converting from AMQP 1.0, the serialized message needs to be parsed into sections.
init(Sections) when is_list(Sections) -> init(Sections) when is_list(Sections) ->
Msg = decode(Sections, #msg{}), Msg = decode(Sections, #msg{}),
init(Msg); init(Msg);
init(#msg{} = Msg) -> init(#msg{} = Msg) ->
%% TODO: as the essential annotations, durable, priority, ttl and delivery_count
%% is all we are interested in it isn't necessary to keep hold of the
%% incoming AMQP header inside the state
Anns = essential_properties(Msg), Anns = essential_properties(Msg),
{Msg, Anns}. {Msg, Anns}.
@ -95,6 +113,8 @@ property(correlation_id, #msg{properties = #'v1_0.properties'{correlation_id = C
Corr; Corr;
property(message_id, #msg{properties = #'v1_0.properties'{message_id = MsgId}}) -> property(message_id, #msg{properties = #'v1_0.properties'{message_id = MsgId}}) ->
MsgId; MsgId;
property(user_id, #msg{properties = #'v1_0.properties'{user_id = UserId}}) ->
UserId;
property(_Prop, #msg{}) -> property(_Prop, #msg{}) ->
undefined. undefined.
@ -134,7 +154,7 @@ get_property(timestamp, Msg) ->
end; end;
get_property(ttl, Msg) -> get_property(ttl, Msg) ->
case Msg of case Msg of
#msg{header = #'v1_0.header'{ttl = {_, Ttl}}} -> #msg{header = #'v1_0.header'{ttl = {uint, Ttl}}} ->
Ttl; Ttl;
_ -> _ ->
%% fallback in case the source protocol was AMQP 0.9.1 %% fallback in case the source protocol was AMQP 0.9.1
@ -158,6 +178,13 @@ get_property(priority, Msg) ->
_ -> _ ->
undefined undefined
end end
end;
get_property(subject, Msg) ->
case Msg of
#msg{properties = #'v1_0.properties'{subject = {utf8, Subject}}} ->
Subject;
_ ->
undefined
end. end.
convert_to(?MODULE, Msg, _Env) -> convert_to(?MODULE, Msg, _Env) ->
@ -170,10 +197,19 @@ convert_to(TargetProto, Msg, Env) ->
serialize(Sections) -> serialize(Sections) ->
encode_bin(Sections). encode_bin(Sections).
protocol_state(Msg, Anns) -> protocol_state(Msg0 = #msg{header = Header0}, Anns) ->
Redelivered = maps:get(redelivered, Anns, false),
FirstAcquirer = not Redelivered,
Header = case Header0 of
undefined ->
#'v1_0.header'{first_acquirer = FirstAcquirer};
#'v1_0.header'{} ->
Header0#'v1_0.header'{first_acquirer = FirstAcquirer}
end,
Msg = Msg0#msg{header = Header},
#{?ANN_EXCHANGE := Exchange, #{?ANN_EXCHANGE := Exchange,
?ANN_ROUTING_KEYS := [RKey | _]} = Anns, ?ANN_ROUTING_KEYS := [RKey | _]} = Anns,
%% any x-* annotations get added as message annotations %% any x-* annotations get added as message annotations
AnnsToAdd = maps:filter(fun (Key, _) -> mc_util:is_x_header(Key) end, Anns), AnnsToAdd = maps:filter(fun (Key, _) -> mc_util:is_x_header(Key) end, Anns),
@ -394,6 +430,10 @@ essential_properties(#msg{message_annotations = MA} = Msg) ->
Priority = get_property(priority, Msg), Priority = get_property(priority, Msg),
Timestamp = get_property(timestamp, Msg), Timestamp = get_property(timestamp, Msg),
Ttl = get_property(ttl, Msg), Ttl = get_property(ttl, Msg),
RoutingKeys = case get_property(subject, Msg) of
undefined -> undefined;
Subject -> [Subject]
end,
Deaths = case message_annotation(<<"x-death">>, Msg, undefined) of Deaths = case message_annotation(<<"x-death">>, Msg, undefined) of
{list, DeathMaps} -> {list, DeathMaps} ->
@ -418,8 +458,10 @@ essential_properties(#msg{message_annotations = MA} = Msg) ->
maps_put_truthy( maps_put_truthy(
ttl, Ttl, ttl, Ttl,
maps_put_truthy( maps_put_truthy(
deaths, Deaths, ?ANN_ROUTING_KEYS, RoutingKeys,
#{}))))), maps_put_truthy(
deaths, Deaths,
#{})))))),
case MA of case MA of
[] -> [] ->
Anns; Anns;

View File

@ -25,7 +25,8 @@
message/3, message/3,
message/4, message/4,
message/5, message/5,
from_basic_message/1 from_basic_message/1,
to_091/2
]). ]).
-import(rabbit_misc, -import(rabbit_misc,

View File

@ -14,6 +14,7 @@
is_persistent/1, is_persistent/1,
ttl/1, ttl/1,
correlation_id/1, correlation_id/1,
user_id/1,
message_id/1, message_id/1,
timestamp/1, timestamp/1,
priority/1, priority/1,
@ -106,6 +107,9 @@ timestamp(#basic_message{content = Content}) ->
priority(#basic_message{content = Content}) -> priority(#basic_message{content = Content}) ->
get_property(?FUNCTION_NAME, Content). get_property(?FUNCTION_NAME, Content).
user_id(#basic_message{content = Content}) ->
get_property(?FUNCTION_NAME, Content).
correlation_id(#basic_message{content = Content}) -> correlation_id(#basic_message{content = Content}) ->
case get_property(?FUNCTION_NAME, Content) of case get_property(?FUNCTION_NAME, Content) of
undefined -> undefined ->
@ -384,6 +388,13 @@ get_property(P, #content{properties = none} = Content) ->
get_property(durable, get_property(durable,
#content{properties = #'P_basic'{delivery_mode = Mode}}) -> #content{properties = #'P_basic'{delivery_mode = Mode}}) ->
Mode == 2; Mode == 2;
get_property(user_id,
#content{properties = #'P_basic'{user_id = UserId}}) ->
if UserId =:= undefined ->
undefined;
is_binary(UserId) ->
{binary, UserId}
end;
get_property(ttl, #content{properties = Props}) -> get_property(ttl, #content{properties = Props}) ->
{ok, MsgTTL} = rabbit_basic:parse_expiration(Props), {ok, MsgTTL} = rabbit_basic:parse_expiration(Props),
MsgTTL; MsgTTL;

View File

@ -36,7 +36,7 @@
%%--------------------------------------------------------------------------- %%---------------------------------------------------------------------------
%% Boot steps. %% Boot steps.
-export([maybe_insert_default_data/0, boot_delegate/0, recover/0]). -export([maybe_insert_default_data/0, boot_delegate/0, recover/0, pg_local/0]).
%% for tests %% for tests
-export([validate_msg_store_io_batch_size_and_credit_disc_bound/2]). -export([validate_msg_store_io_batch_size_and_credit_disc_bound/2]).
@ -267,6 +267,12 @@
{mfa, {logger, debug, ["'networking' boot step skipped and moved to end of startup", [], #{domain => ?RMQLOG_DOMAIN_GLOBAL}]}}, {mfa, {logger, debug, ["'networking' boot step skipped and moved to end of startup", [], #{domain => ?RMQLOG_DOMAIN_GLOBAL}]}},
{requires, notify_cluster}]}). {requires, notify_cluster}]}).
-rabbit_boot_step({pg_local,
[{description, "local-only pg scope"},
{mfa, {rabbit, pg_local, []}},
{requires, kernel_ready},
{enables, core_initialized}]}).
%%--------------------------------------------------------------------------- %%---------------------------------------------------------------------------
-include_lib("rabbit_common/include/rabbit_framing.hrl"). -include_lib("rabbit_common/include/rabbit_framing.hrl").
@ -752,7 +758,7 @@ status() ->
true -> true ->
[{virtual_host_count, rabbit_vhost:count()}, [{virtual_host_count, rabbit_vhost:count()},
{connection_count, {connection_count,
length(rabbit_networking:connections_local()) + length(rabbit_networking:local_connections()) +
length(rabbit_networking:local_non_amqp_connections())}, length(rabbit_networking:local_non_amqp_connections())},
{queue_count, total_queue_count()}]; {queue_count, total_queue_count()}];
false -> false ->
@ -1098,6 +1104,9 @@ recover() ->
ok = rabbit_vhost:recover(), ok = rabbit_vhost:recover(),
ok. ok.
pg_local() ->
rabbit_sup:start_child(pg, [node()]).
-spec maybe_insert_default_data() -> 'ok'. -spec maybe_insert_default_data() -> 'ok'.
maybe_insert_default_data() -> maybe_insert_default_data() ->
@ -1690,7 +1699,19 @@ persist_static_configuration() ->
classic_queue_store_v2_max_cache_size, classic_queue_store_v2_max_cache_size,
classic_queue_store_v2_check_crc32, classic_queue_store_v2_check_crc32,
incoming_message_interceptors incoming_message_interceptors
]). ]),
%% Disallow 0 as it means unlimited:
%% "If this field is zero or unset, there is no maximum
%% size imposed by the link endpoint." [AMQP 1.0 §2.7.3]
MaxMsgSize = case application:get_env(?MODULE, max_message_size) of
{ok, Size}
when is_integer(Size) andalso Size > 0 ->
erlang:min(Size, ?MAX_MSG_SIZE);
_ ->
?MAX_MSG_SIZE
end,
ok = persistent_term:put(max_message_size, MaxMsgSize).
persist_static_configuration(Params) -> persist_static_configuration(Params) ->
App = ?MODULE, App = ?MODULE,

View File

@ -10,7 +10,8 @@
-include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("rabbit_common/include/rabbit.hrl").
-export([check_user_pass_login/2, check_user_login/2, check_user_loopback/2, -export([check_user_pass_login/2, check_user_login/2, check_user_loopback/2,
check_vhost_access/4, check_resource_access/4, check_topic_access/4]). check_vhost_access/4, check_resource_access/4, check_topic_access/4,
check_user_id/2]).
-export([permission_cache_can_expire/1, update_state/2, expiry_timestamp/1]). -export([permission_cache_can_expire/1, update_state/2, expiry_timestamp/1]).
@ -222,6 +223,31 @@ check_access(Fun, Module, ErrStr, ErrArgs, ErrName) ->
rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs) rabbit_misc:protocol_error(ErrName, FullErrStr, FullErrArgs)
end. end.
-spec check_user_id(mc:state(), rabbit_types:user()) ->
ok | {refused, string(), [term()]}.
check_user_id(Message, ActualUser) ->
case mc:user_id(Message) of
undefined ->
ok;
{binary, ClaimedUserName} ->
check_user_id0(ClaimedUserName, ActualUser)
end.
check_user_id0(Username, #user{username = Username}) ->
ok;
check_user_id0(_, #user{authz_backends = [{rabbit_auth_backend_dummy, _}]}) ->
ok;
check_user_id0(ClaimedUserName, #user{username = ActualUserName,
tags = Tags}) ->
case lists:member(impersonator, Tags) of
true ->
ok;
false ->
{refused,
"user_id property set to '~ts' but authenticated user was '~ts'",
[ClaimedUserName, ActualUserName]}
end.
-spec update_state(User :: rabbit_types:user(), NewState :: term()) -> -spec update_state(User :: rabbit_types:user(), NewState :: term()) ->
{'ok', rabbit_types:auth_user()} | {'ok', rabbit_types:auth_user()} |
{'refused', string()} | {'refused', string()} |

View File

@ -50,7 +50,7 @@
-type resource_alarm() :: {resource_limit, resource_alarm_source(), node()}. -type resource_alarm() :: {resource_limit, resource_alarm_source(), node()}.
-type alarm() :: local_alarm() | resource_alarm(). -type alarm() :: local_alarm() | resource_alarm().
-type resource_alert() :: {WasAlarmSetForNode :: boolean(), -type resource_alert() :: {WasAlarmSetForNode :: boolean(),
IsThereAnyAlarmsWithSameSourceInTheCluster :: boolean(), IsThereAnyAlarmWithSameSourceInTheCluster :: boolean(),
NodeForWhichAlarmWasSetOrCleared :: node()}. NodeForWhichAlarmWasSetOrCleared :: node()}.
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------

View File

@ -6,11 +6,16 @@
%% %%
-module(rabbit_amqp1_0). -module(rabbit_amqp1_0).
-define(PROCESS_GROUP_NAME, rabbit_amqp10_connections).
-export([list_local/0,
register_connection/1]).
%% Below 2 functions are deprecated.
%% They could be called in 3.13 / 4.0 mixed version clusters by the old 3.13 CLI command
%% rabbitmqctl list_amqp10_connections
-export([emit_connection_info_local/3, -export([emit_connection_info_local/3,
emit_connection_info_all/4, emit_connection_info_all/4]).
list/0,
register_connection/1,
unregister_connection/1]).
emit_connection_info_all(Nodes, Items, Ref, AggregatorPid) -> emit_connection_info_all(Nodes, Items, Ref, AggregatorPid) ->
Pids = [spawn_link(Node, rabbit_amqp1_0, emit_connection_info_local, Pids = [spawn_link(Node, rabbit_amqp1_0, emit_connection_info_local,
@ -20,21 +25,19 @@ emit_connection_info_all(Nodes, Items, Ref, AggregatorPid) ->
ok. ok.
emit_connection_info_local(Items, Ref, AggregatorPid) -> emit_connection_info_local(Items, Ref, AggregatorPid) ->
ConnectionPids = list_local(),
rabbit_control_misc:emitting_map_with_exit_handler( rabbit_control_misc:emitting_map_with_exit_handler(
AggregatorPid, Ref, AggregatorPid,
Ref,
fun(Pid) -> fun(Pid) ->
rabbit_amqp1_0_reader:info(Pid, Items) rabbit_amqp_reader:info(Pid, Items)
end, end,
list()). ConnectionPids).
-spec list() -> [pid()]. -spec list_local() -> [pid()].
list() -> list_local() ->
pg_local:get_members(rabbit_amqp10_connections). pg:get_local_members(node(), ?PROCESS_GROUP_NAME).
-spec register_connection(pid()) -> ok. -spec register_connection(pid()) -> ok.
register_connection(Pid) -> register_connection(Pid) ->
pg_local:join(rabbit_amqp10_connections, Pid). ok = pg:join(node(), ?PROCESS_GROUP_NAME, Pid).
-spec unregister_connection(pid()) -> ok.
unregister_connection(Pid) ->
pg_local:leave(rabbit_amqp10_connections, Pid).

1042
deps/rabbit/src/rabbit_amqp_reader.erl vendored Normal file

File diff suppressed because it is too large Load Diff

2325
deps/rabbit/src/rabbit_amqp_session.erl vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(rabbit_amqp_session_sup).
-behaviour(supervisor).
-include_lib("rabbit_common/include/rabbit.hrl").
%% client API
-export([start_link/1,
start_session/2]).
%% supervisor callback
-export([init/1]).
-spec start_link(Reader :: pid()) ->
supervisor:startlink_ret().
start_link(ReaderPid) ->
supervisor:start_link(?MODULE, ReaderPid).
init(ReaderPid) ->
SupFlags = #{strategy => simple_one_for_one,
intensity => 0,
period => 1},
ChildSpec = #{id => amqp1_0_session,
start => {rabbit_amqp_session, start_link, [ReaderPid]},
restart => temporary,
shutdown => ?WORKER_WAIT,
type => worker},
{ok, {SupFlags, [ChildSpec]}}.
-spec start_session(pid(), list()) ->
supervisor:startchild_ret().
start_session(SessionSupPid, Args) ->
supervisor:start_child(SessionSupPid, Args).

19
deps/rabbit/src/rabbit_amqp_util.erl vendored Normal file
View File

@ -0,0 +1,19 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(rabbit_amqp_util).
-include("rabbit_amqp.hrl").
-export([protocol_error/3]).
-spec protocol_error(term(), io:format(), [term()]) ->
no_return().
protocol_error(Condition, Msg, Args) ->
Description = list_to_binary(lists:flatten(io_lib:format(Msg, Args))),
Reason = #'v1_0.error'{condition = Condition,
description = {utf8, Description}},
exit(Reason).

218
deps/rabbit/src/rabbit_amqp_writer.erl vendored Normal file
View File

@ -0,0 +1,218 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(rabbit_amqp_writer).
-behaviour(gen_server).
-include("rabbit_amqp.hrl").
%% client API
-export([start_link/3,
send_command/3,
send_command/4,
send_command_sync/3,
send_command_and_notify/6,
internal_send_command/3]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
format_status/1]).
-record(state, {
sock :: rabbit_net:socket(),
max_frame_size :: unlimited | pos_integer(),
reader :: rabbit_types:connection(),
pending :: iolist(),
%% This field is just an optimisation to minimize the cost of erlang:iolist_size/1
pending_size :: non_neg_integer()
}).
-define(HIBERNATE_AFTER, 6_000).
-define(CALL_TIMEOUT, 300_000).
-define(AMQP_SASL_FRAME_TYPE, 1).
%%%%%%%%%%%%%%%%%%
%%% client API %%%
%%%%%%%%%%%%%%%%%%
-spec start_link (rabbit_net:socket(), non_neg_integer(), pid()) ->
rabbit_types:ok(pid()).
start_link(Sock, MaxFrame, ReaderPid) ->
Args = {Sock, MaxFrame, ReaderPid},
Opts = [{hibernate_after, ?HIBERNATE_AFTER}],
gen_server:start_link(?MODULE, Args, Opts).
-spec send_command(pid(),
rabbit_types:channel_number(),
rabbit_framing:amqp_method_record()) -> ok.
send_command(Writer, ChannelNum, MethodRecord) ->
Request = {send_command, ChannelNum, MethodRecord},
gen_server:cast(Writer, Request).
-spec send_command(pid(),
rabbit_types:channel_number(),
rabbit_framing:amqp_method_record(),
rabbit_types:content()) -> ok.
send_command(Writer, ChannelNum, MethodRecord, Content) ->
Request = {send_command, ChannelNum, MethodRecord, Content},
gen_server:cast(Writer, Request).
-spec send_command_sync(pid(),
rabbit_types:channel_number(),
rabbit_framing:amqp_method_record()) -> ok.
send_command_sync(Writer, ChannelNum, MethodRecord) ->
Request = {send_command, ChannelNum, MethodRecord},
gen_server:call(Writer, Request, ?CALL_TIMEOUT).
-spec send_command_and_notify(pid(),
rabbit_types:channel_number(),
pid(),
pid(),
rabbit_framing:amqp_method_record(),
rabbit_types:content()) -> ok.
send_command_and_notify(Writer, ChannelNum, QueuePid, SessionPid, MethodRecord, Content) ->
Request = {send_command_and_notify, ChannelNum, QueuePid, SessionPid, MethodRecord, Content},
gen_server:cast(Writer, Request).
-spec internal_send_command(rabbit_net:socket(),
rabbit_framing:amqp_method_record(),
amqp10_framing | rabbit_amqp_sasl) -> ok.
internal_send_command(Sock, MethodRecord, Protocol) ->
Data = assemble_frame(0, MethodRecord, Protocol),
ok = tcp_send(Sock, Data).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% gen_server callbacks %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
init({Sock, MaxFrame, ReaderPid}) ->
State = #state{sock = Sock,
max_frame_size = MaxFrame,
reader = ReaderPid,
pending = [],
pending_size = 0},
{ok, State}.
handle_cast({send_command, ChannelNum, MethodRecord}, State0) ->
State = internal_send_command_async(ChannelNum, MethodRecord, State0),
no_reply(State);
handle_cast({send_command, ChannelNum, MethodRecord, Content}, State0) ->
State = internal_send_command_async(ChannelNum, MethodRecord, Content, State0),
no_reply(State);
handle_cast({send_command_and_notify, ChannelNum, QueuePid, SessionPid, MethodRecord, Content}, State0) ->
State = internal_send_command_async(ChannelNum, MethodRecord, Content, State0),
rabbit_amqqueue:notify_sent(QueuePid, SessionPid),
no_reply(State).
handle_call({send_command, ChannelNum, MethodRecord}, _From, State0) ->
State1 = internal_send_command_async(ChannelNum, MethodRecord, State0),
State = flush(State1),
{reply, ok, State}.
handle_info(timeout, State0) ->
State = flush(State0),
{noreply, State};
handle_info({'DOWN', _MRef, process, QueuePid, _Reason}, State) ->
rabbit_amqqueue:notify_sent_queue_down(QueuePid),
no_reply(State).
format_status(Status) ->
maps:update_with(
state,
fun(#state{sock = Sock,
max_frame_size = MaxFrame,
reader = Reader,
pending = Pending,
pending_size = PendingSize}) ->
#{socket => Sock,
max_frame_size => MaxFrame,
reader => Reader,
%% Below 2 fields should always have the same value.
pending => iolist_size(Pending),
pending_size => PendingSize}
end,
Status).
%%%%%%%%%%%%%%%
%%% Helpers %%%
%%%%%%%%%%%%%%%
no_reply(State) ->
{noreply, State, 0}.
internal_send_command_async(Channel, MethodRecord,
State = #state{pending = Pending,
pending_size = PendingSize}) ->
Frame = assemble_frame(Channel, MethodRecord),
maybe_flush(State#state{pending = [Frame | Pending],
pending_size = PendingSize + iolist_size(Frame)}).
internal_send_command_async(Channel, MethodRecord, Content,
State = #state{max_frame_size = MaxFrame,
pending = Pending,
pending_size = PendingSize}) ->
Frames = assemble_frames(Channel, MethodRecord, Content, MaxFrame),
maybe_flush(State#state{pending = [Frames | Pending],
pending_size = PendingSize + iolist_size(Frames)}).
%% Note: a transfer record can be followed by a number of other
%% records to make a complete frame but unlike 0-9-1 we may have many
%% content records. However, that's already been handled for us, we're
%% just sending a chunk, so from this perspective it's just a binary.
%%TODO respect MaxFrame
assemble_frames(Channel, Performative, Content, _MaxFrame) ->
?DEBUG("~s Channel ~tp <-~n~tp~n followed by ~tp bytes of content~n",
[?MODULE, Channel, amqp10_framing:pprint(Performative),
iolist_size(Content)]),
PerfBin = amqp10_framing:encode_bin(Performative),
amqp10_binary_generator:build_frame(Channel, [PerfBin, Content]).
assemble_frame(Channel, Performative) ->
assemble_frame(Channel, Performative, amqp10_framing).
assemble_frame(Channel, Performative, amqp10_framing) ->
?DEBUG("~s Channel ~tp <-~n~tp~n",
[?MODULE, Channel, amqp10_framing:pprint(Performative)]),
PerfBin = amqp10_framing:encode_bin(Performative),
amqp10_binary_generator:build_frame(Channel, PerfBin);
assemble_frame(Channel, Performative, rabbit_amqp_sasl) ->
?DEBUG("~s Channel ~tp <-~n~tp~n",
[?MODULE, Channel, amqp10_framing:pprint(Performative)]),
PerfBin = amqp10_framing:encode_bin(Performative),
amqp10_binary_generator:build_frame(Channel, ?AMQP_SASL_FRAME_TYPE, PerfBin).
tcp_send(Sock, Data) ->
rabbit_misc:throw_on_error(
inet_error,
fun() -> rabbit_net:send(Sock, Data) end).
%% Flush when more than 2.5 * 1460 bytes (TCP over Ethernet MSS) = 3650 bytes of data
%% has accumulated. The idea is to get the TCP data sections full (i.e. fill 1460 bytes)
%% as often as possible to reduce the overhead of TCP/IP headers.
-define(FLUSH_THRESHOLD, 3650).
maybe_flush(State = #state{pending_size = PendingSize}) ->
case PendingSize > ?FLUSH_THRESHOLD of
true -> flush(State);
false -> State
end.
flush(State = #state{pending = []}) ->
State;
flush(State = #state{sock = Sock,
pending = Pending}) ->
case rabbit_net:send(Sock, lists:reverse(Pending)) of
ok ->
State#state{pending = [],
pending_size = 0};
{error, Reason} ->
exit({writer, send_failed, Reason})
end.

View File

@ -33,7 +33,7 @@
-export([consumers/1, consumers_all/1, emit_consumers_all/4, consumer_info_keys/0]). -export([consumers/1, consumers_all/1, emit_consumers_all/4, consumer_info_keys/0]).
-export([basic_get/5, basic_consume/12, basic_cancel/5, notify_decorators/1]). -export([basic_get/5, basic_consume/12, basic_cancel/5, notify_decorators/1]).
-export([notify_sent/2, notify_sent_queue_down/1, resume/2]). -export([notify_sent/2, notify_sent_queue_down/1, resume/2]).
-export([notify_down_all/2, notify_down_all/3, activate_limit_all/2, credit/5]). -export([notify_down_all/2, notify_down_all/3, activate_limit_all/2]).
-export([on_node_up/1, on_node_down/1]). -export([on_node_up/1, on_node_down/1]).
-export([update/2, store_queue/1, update_decorators/2, policy_changed/2]). -export([update/2, store_queue/1, update_decorators/2, policy_changed/2]).
-export([update_mirroring/1, sync_mirrors/1, cancel_sync_mirrors/1]). -export([update_mirroring/1, sync_mirrors/1, cancel_sync_mirrors/1]).
@ -92,7 +92,7 @@
-define(IS_QUORUM(QPid), is_tuple(QPid)). -define(IS_QUORUM(QPid), is_tuple(QPid)).
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
-export_type([name/0, qmsg/0, absent_reason/0]). -export_type([name/0, qmsg/0, msg_id/0, absent_reason/0]).
-type name() :: rabbit_types:r('queue'). -type name() :: rabbit_types:r('queue').
@ -101,7 +101,7 @@
-type qfun(A) :: fun ((amqqueue:amqqueue()) -> A | no_return()). -type qfun(A) :: fun ((amqqueue:amqqueue()) -> A | no_return()).
-type qmsg() :: {name(), pid() | {atom(), pid()}, msg_id(), -type qmsg() :: {name(), pid() | {atom(), pid()}, msg_id(),
boolean(), mc:state()}. boolean(), mc:state()}.
-type msg_id() :: non_neg_integer(). -type msg_id() :: undefined | non_neg_integer() | {Priority :: non_neg_integer(), undefined | non_neg_integer()}.
-type ok_or_errors() :: -type ok_or_errors() ::
'ok' | {'error', [{'error' | 'exit' | 'throw', any()}]}. 'ok' | {'error', [{'error' | 'exit' | 'throw', any()}]}.
-type absent_reason() :: 'nodedown' | 'crashed' | stopped | timeout. -type absent_reason() :: 'nodedown' | 'crashed' | stopped | timeout.
@ -789,11 +789,13 @@ check_exclusive_access(Q, _ReaderPid, _MatchType) ->
[rabbit_misc:rs(QueueName)]). [rabbit_misc:rs(QueueName)]).
-spec with_exclusive_access_or_die(name(), pid(), qfun(A)) -> -spec with_exclusive_access_or_die(name(), pid(), qfun(A)) ->
A | rabbit_types:channel_exit(). A | rabbit_types:channel_exit().
with_exclusive_access_or_die(Name, ReaderPid, F) -> with_exclusive_access_or_die(Name, ReaderPid, F) ->
with_or_die(Name, with_or_die(Name,
fun (Q) -> check_exclusive_access(Q, ReaderPid), F(Q) end). fun (Q) ->
check_exclusive_access(Q, ReaderPid),
F(Q)
end).
assert_args_equivalence(Q, NewArgs) -> assert_args_equivalence(Q, NewArgs) ->
ExistingArgs = amqqueue:get_arguments(Q), ExistingArgs = amqqueue:get_arguments(Q),
@ -1731,15 +1733,6 @@ deactivate_limit_all(QRefs, ChPid) ->
delegate:invoke_no_result(QPids, {gen_server2, cast, delegate:invoke_no_result(QPids, {gen_server2, cast,
[{deactivate_limit, ChPid}]}). [{deactivate_limit, ChPid}]}).
-spec credit(amqqueue:amqqueue(),
rabbit_types:ctag(),
non_neg_integer(),
boolean(),
rabbit_queue_type:state()) ->
{ok, rabbit_queue_type:state(), rabbit_queue_type:actions()}.
credit(Q, CTag, Credit, Drain, QStates) ->
rabbit_queue_type:credit(Q, CTag, Credit, Drain, QStates).
-spec basic_get(amqqueue:amqqueue(), boolean(), pid(), rabbit_types:ctag(), -spec basic_get(amqqueue:amqqueue(), boolean(), pid(), rabbit_types:ctag(),
rabbit_queue_type:state()) -> rabbit_queue_type:state()) ->
{'ok', non_neg_integer(), qmsg(), rabbit_queue_type:state()} | {'ok', non_neg_integer(), qmsg(), rabbit_queue_type:state()} |
@ -1766,7 +1759,7 @@ basic_consume(Q, NoAck, ChPid, LimiterPid,
channel_pid => ChPid, channel_pid => ChPid,
limiter_pid => LimiterPid, limiter_pid => LimiterPid,
limiter_active => LimiterActive, limiter_active => LimiterActive,
prefetch_count => ConsumerPrefetchCount, mode => {simple_prefetch, ConsumerPrefetchCount},
consumer_tag => ConsumerTag, consumer_tag => ConsumerTag,
exclusive_consume => ExclusiveConsume, exclusive_consume => ExclusiveConsume,
args => Args, args => Args,

View File

@ -370,6 +370,13 @@ code_change(_OldVsn, State, _Extra) ->
maybe_notify_decorators(false, State) -> State; maybe_notify_decorators(false, State) -> State;
maybe_notify_decorators(true, State) -> notify_decorators(State), State. maybe_notify_decorators(true, State) -> notify_decorators(State), State.
notify_decorators_if_became_empty(WasEmpty, State) ->
case (not WasEmpty) andalso is_empty(State) of
true -> notify_decorators(State);
false -> ok
end,
State.
notify_decorators(Event, State) -> notify_decorators(Event, State) ->
_ = decorator_callback(qname(State), Event, []), _ = decorator_callback(qname(State), Event, []),
ok. ok.
@ -570,14 +577,6 @@ assert_invariant(State = #q{consumers = Consumers, single_active_consumer_on = f
is_empty(#q{backing_queue = BQ, backing_queue_state = BQS}) -> BQ:is_empty(BQS). is_empty(#q{backing_queue = BQ, backing_queue_state = BQS}) -> BQ:is_empty(BQS).
maybe_send_drained(WasEmpty, #q{q = Q} = State) ->
case (not WasEmpty) andalso is_empty(State) of
true -> notify_decorators(State),
rabbit_queue_consumers:send_drained(amqqueue:get_name(Q));
false -> ok
end,
State.
confirm_messages([], MTC, _QName) -> confirm_messages([], MTC, _QName) ->
MTC; MTC;
confirm_messages(MsgIds, MTC, QName) -> confirm_messages(MsgIds, MTC, QName) ->
@ -852,7 +851,7 @@ requeue_and_run(AckTags, State = #q{backing_queue = BQ,
WasEmpty = BQ:is_empty(BQS), WasEmpty = BQ:is_empty(BQS),
{_MsgIds, BQS1} = BQ:requeue(AckTags, BQS), {_MsgIds, BQS1} = BQ:requeue(AckTags, BQS),
{_Dropped, State1} = maybe_drop_head(State#q{backing_queue_state = BQS1}), {_Dropped, State1} = maybe_drop_head(State#q{backing_queue_state = BQS1}),
run_message_queue(maybe_send_drained(WasEmpty, drop_expired_msgs(State1))). run_message_queue(notify_decorators_if_became_empty(WasEmpty, drop_expired_msgs(State1))).
fetch(AckRequired, State = #q{backing_queue = BQ, fetch(AckRequired, State = #q{backing_queue = BQ,
backing_queue_state = BQS}) -> backing_queue_state = BQS}) ->
@ -861,7 +860,7 @@ fetch(AckRequired, State = #q{backing_queue = BQ,
%% we will send expired messages at times. %% we will send expired messages at times.
{Result, BQS1} = BQ:fetch(AckRequired, BQS), {Result, BQS1} = BQ:fetch(AckRequired, BQS),
State1 = drop_expired_msgs(State#q{backing_queue_state = BQS1}), State1 = drop_expired_msgs(State#q{backing_queue_state = BQS1}),
{Result, maybe_send_drained(Result =:= empty, State1)}. {Result, notify_decorators_if_became_empty(Result =:= empty, State1)}.
ack(AckTags, ChPid, State) -> ack(AckTags, ChPid, State) ->
subtract_acks(ChPid, AckTags, State, subtract_acks(ChPid, AckTags, State,
@ -992,11 +991,6 @@ calculate_msg_expiry(Msg, TTL) ->
os:system_time(microsecond) + T * 1000 os:system_time(microsecond) + T * 1000
end. end.
%% Logically this function should invoke maybe_send_drained/2.
%% However, that is expensive. Since some frequent callers of
%% drop_expired_msgs/1, in particular deliver_or_enqueue/3, cannot
%% possibly cause the queue to become empty, we push the
%% responsibility to the callers. So be cautious when adding new ones.
drop_expired_msgs(State) -> drop_expired_msgs(State) ->
case is_empty(State) of case is_empty(State) of
true -> State; true -> State;
@ -1343,9 +1337,8 @@ handle_call({basic_get, ChPid, NoAck, LimiterPid}, _From,
end; end;
handle_call({basic_consume, NoAck, ChPid, LimiterPid, LimiterActive, handle_call({basic_consume, NoAck, ChPid, LimiterPid, LimiterActive,
PrefetchCount, ConsumerTag, ExclusiveConsume, Args, OkMsg, ActingUser}, ModeOrPrefetch, ConsumerTag, ExclusiveConsume, Args, OkMsg, ActingUser},
_From, State = #q{q = Q, _From, State = #q{consumers = Consumers,
consumers = Consumers,
active_consumer = Holder, active_consumer = Holder,
single_active_consumer_on = SingleActiveConsumerOn}) -> single_active_consumer_on = SingleActiveConsumerOn}) ->
ConsumerRegistration = case SingleActiveConsumerOn of ConsumerRegistration = case SingleActiveConsumerOn of
@ -1355,33 +1348,28 @@ handle_call({basic_consume, NoAck, ChPid, LimiterPid, LimiterActive,
{error, reply({error, exclusive_consume_unavailable}, State)}; {error, reply({error, exclusive_consume_unavailable}, State)};
false -> false ->
Consumers1 = rabbit_queue_consumers:add( Consumers1 = rabbit_queue_consumers:add(
amqqueue:get_name(Q),
ChPid, ConsumerTag, NoAck, ChPid, ConsumerTag, NoAck,
LimiterPid, LimiterActive, LimiterPid, LimiterActive, ModeOrPrefetch,
PrefetchCount, Args, is_empty(State), Args, ActingUser, Consumers),
ActingUser, Consumers), case Holder of
none ->
case Holder of NewConsumer = rabbit_queue_consumers:get(ChPid, ConsumerTag, Consumers1),
none -> {state, State#q{consumers = Consumers1,
NewConsumer = rabbit_queue_consumers:get(ChPid, ConsumerTag, Consumers1), has_had_consumers = true,
{state, State#q{consumers = Consumers1, active_consumer = NewConsumer}};
has_had_consumers = true, _ ->
active_consumer = NewConsumer}}; {state, State#q{consumers = Consumers1,
_ -> has_had_consumers = true}}
{state, State#q{consumers = Consumers1, end
has_had_consumers = true}}
end
end; end;
false -> false ->
case check_exclusive_access(Holder, ExclusiveConsume, State) of case check_exclusive_access(Holder, ExclusiveConsume, State) of
in_use -> {error, reply({error, exclusive_consume_unavailable}, State)}; in_use -> {error, reply({error, exclusive_consume_unavailable}, State)};
ok -> ok ->
Consumers1 = rabbit_queue_consumers:add( Consumers1 = rabbit_queue_consumers:add(
amqqueue:get_name(Q),
ChPid, ConsumerTag, NoAck, ChPid, ConsumerTag, NoAck,
LimiterPid, LimiterActive, LimiterPid, LimiterActive, ModeOrPrefetch,
PrefetchCount, Args, is_empty(State), Args, ActingUser, Consumers),
ActingUser, Consumers),
ExclusiveConsumer = ExclusiveConsumer =
if ExclusiveConsume -> {ChPid, ConsumerTag}; if ExclusiveConsume -> {ChPid, ConsumerTag};
true -> Holder true -> Holder
@ -1408,7 +1396,8 @@ handle_call({basic_consume, NoAck, ChPid, LimiterPid, LimiterActive,
{false, _} -> {false, _} ->
{true, up} {true, up}
end, end,
rabbit_core_metrics:consumer_created( PrefetchCount = rabbit_queue_consumers:parse_prefetch_count(ModeOrPrefetch),
rabbit_core_metrics:consumer_created(
ChPid, ConsumerTag, ExclusiveConsume, AckRequired, QName, ChPid, ConsumerTag, ExclusiveConsume, AckRequired, QName,
PrefetchCount, ConsumerIsActive, ActivityStatus, Args), PrefetchCount, ConsumerIsActive, ActivityStatus, Args),
emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume, emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume,
@ -1436,7 +1425,9 @@ handle_call({basic_cancel, ChPid, ConsumerTag, OkMsg, ActingUser}, _From,
emit_consumer_deleted(ChPid, ConsumerTag, qname(State1), ActingUser), emit_consumer_deleted(ChPid, ConsumerTag, qname(State1), ActingUser),
notify_decorators(State1), notify_decorators(State1),
case should_auto_delete(State1) of case should_auto_delete(State1) of
false -> reply(ok, ensure_expiry_timer(State1)); false ->
State2 = run_message_queue(Holder =/= Holder1, State1),
reply(ok, ensure_expiry_timer(State2));
true -> true ->
log_auto_delete( log_auto_delete(
io_lib:format( io_lib:format(
@ -1467,7 +1458,7 @@ handle_call(purge, _From, State = #q{backing_queue = BQ,
backing_queue_state = BQS}) -> backing_queue_state = BQS}) ->
{Count, BQS1} = BQ:purge(BQS), {Count, BQS1} = BQ:purge(BQS),
State1 = State#q{backing_queue_state = BQS1}, State1 = State#q{backing_queue_state = BQS1},
reply({ok, Count}, maybe_send_drained(Count =:= 0, State1)); reply({ok, Count}, notify_decorators_if_became_empty(Count =:= 0, State1));
handle_call({requeue, AckTags, ChPid}, From, State) -> handle_call({requeue, AckTags, ChPid}, From, State) ->
gen_server2:reply(From, ok), gen_server2:reply(From, ok),
@ -1638,21 +1629,57 @@ handle_cast(update_mirroring, State = #q{q = Q,
noreply(update_mirroring(Policy, State1)) noreply(update_mirroring(Policy, State1))
end; end;
handle_cast({credit, ChPid, CTag, Credit, Drain}, handle_cast({credit, SessionPid, CTag, Credit, Drain},
State = #q{consumers = Consumers, #q{q = Q,
backing_queue = BQ, backing_queue = BQ,
backing_queue_state = BQS, backing_queue_state = BQS0} = State) ->
q = Q}) -> %% Credit API v1.
Len = BQ:len(BQS), %% Delete this function clause when feature flag credit_api_v2 becomes required.
rabbit_classic_queue:send_credit_reply(ChPid, amqqueue:get_name(Q), Len), %% Behave like non-native AMQP 1.0: Send send_credit_reply before deliveries.
noreply( rabbit_classic_queue:send_credit_reply_credit_api_v1(
case rabbit_queue_consumers:credit(amqqueue:get_name(Q), SessionPid, amqqueue:get_name(Q), BQ:len(BQS0)),
Len == 0, Credit, Drain, ChPid, CTag, handle_cast({credit, SessionPid, CTag, credit_api_v1, Credit, Drain, false}, State);
Consumers) of handle_cast({credit, SessionPid, CTag, DeliveryCountRcv, Credit, Drain, Echo},
unchanged -> State; #q{consumers = Consumers0,
{unblocked, Consumers1} -> State1 = State#q{consumers = Consumers1}, q = Q} = State0) ->
run_message_queue(true, State1) QName = amqqueue:get_name(Q),
end); State = #q{backing_queue_state = PostBQS,
backing_queue = BQ} = case rabbit_queue_consumers:process_credit(
DeliveryCountRcv, Credit, SessionPid, CTag, Consumers0) of
unchanged ->
State0;
{unblocked, Consumers1} ->
State1 = State0#q{consumers = Consumers1},
run_message_queue(true, State1)
end,
case rabbit_queue_consumers:get_link_state(SessionPid, CTag) of
{credit_api_v1, PostCred}
when Drain andalso
is_integer(PostCred) andalso PostCred > 0 ->
%% credit API v1
rabbit_queue_consumers:drained(credit_api_v1, SessionPid, CTag),
rabbit_classic_queue:send_drained_credit_api_v1(SessionPid, QName, CTag, PostCred);
{PostDeliveryCountSnd, PostCred}
when is_integer(PostDeliveryCountSnd) andalso
Drain andalso
is_integer(PostCred) andalso PostCred > 0 ->
%% credit API v2
AdvancedDeliveryCount = serial_number:add(PostDeliveryCountSnd, PostCred),
rabbit_queue_consumers:drained(AdvancedDeliveryCount, SessionPid, CTag),
Avail = BQ:len(PostBQS),
rabbit_classic_queue:send_credit_reply(
SessionPid, QName, CTag, AdvancedDeliveryCount, 0, Avail, Drain);
{PostDeliveryCountSnd, PostCred}
when is_integer(PostDeliveryCountSnd) andalso
Echo ->
%% credit API v2
Avail = BQ:len(PostBQS),
rabbit_classic_queue:send_credit_reply(
SessionPid, QName, CTag, PostDeliveryCountSnd, PostCred, Avail, Drain);
_ ->
ok
end,
noreply(State);
% Note: https://www.pivotaltracker.com/story/show/166962656 % Note: https://www.pivotaltracker.com/story/show/166962656
% This event is necessary for the stats timer to be initialized with % This event is necessary for the stats timer to be initialized with
@ -1731,7 +1758,7 @@ handle_info({maybe_expire, _Vsn}, State) ->
handle_info({drop_expired, Vsn}, State = #q{args_policy_version = Vsn}) -> handle_info({drop_expired, Vsn}, State = #q{args_policy_version = Vsn}) ->
WasEmpty = is_empty(State), WasEmpty = is_empty(State),
State1 = drop_expired_msgs(State#q{ttl_timer_ref = undefined}), State1 = drop_expired_msgs(State#q{ttl_timer_ref = undefined}),
noreply(maybe_send_drained(WasEmpty, State1)); noreply(notify_decorators_if_became_empty(WasEmpty, State1));
handle_info({drop_expired, _Vsn}, State) -> handle_info({drop_expired, _Vsn}, State) ->
noreply(State); noreply(State);

View File

@ -411,7 +411,7 @@ make_decision(AllPartitions) ->
partition_value(Partition) -> partition_value(Partition) ->
Connections = [Res || Node <- Partition, Connections = [Res || Node <- Partition,
Res <- [rpc:call(Node, rabbit_networking, Res <- [rpc:call(Node, rabbit_networking,
connections_local, [])], local_connections, [])],
is_list(Res)], is_list(Res)],
{length(lists:append(Connections)), length(Partition)}. {length(lists:append(Connections)), length(Partition)}.

View File

@ -63,7 +63,7 @@
-export([get_vhost/1, get_user/1]). -export([get_vhost/1, get_user/1]).
%% For testing %% For testing
-export([build_topic_variable_map/3]). -export([build_topic_variable_map/3]).
-export([list_queue_states/1, get_max_message_size/0]). -export([list_queue_states/1]).
%% Mgmt HTTP API refactor %% Mgmt HTTP API refactor
-export([handle_method/6]). -export([handle_method/6]).
@ -87,13 +87,9 @@
%% same as reader's name, see #v1.name %% same as reader's name, see #v1.name
%% in rabbit_reader %% in rabbit_reader
conn_name, conn_name,
%% channel's originating source e.g. rabbit_reader | rabbit_direct | undefined
%% or any other channel creating/spawning entity
source,
%% same as #v1.user in the reader, used in %% same as #v1.user in the reader, used in
%% authorisation checks %% authorisation checks
user, user,
%% same as #v1.user in the reader
virtual_host, virtual_host,
%% when queue.bind's queue field is empty, %% when queue.bind's queue field is empty,
%% this name will be used instead %% this name will be used instead
@ -107,15 +103,10 @@
capabilities, capabilities,
trace_state :: rabbit_trace:state(), trace_state :: rabbit_trace:state(),
consumer_prefetch, consumer_prefetch,
%% Message content size limit
max_message_size,
consumer_timeout, consumer_timeout,
authz_context, authz_context,
%% defines how ofter gc will be executed %% defines how ofter gc will be executed
writer_gc_threshold, writer_gc_threshold
%% true with AMQP 1.0 to include the publishing sequence
%% in the return callback, false otherwise
extended_return_callback
}). }).
-record(pending_ack, { -record(pending_ack, {
@ -513,10 +504,8 @@ init([Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, VHost,
end, end,
%% Process dictionary is used here because permission cache already uses it. MK. %% Process dictionary is used here because permission cache already uses it. MK.
put(permission_cache_can_expire, rabbit_access_control:permission_cache_can_expire(User)), put(permission_cache_can_expire, rabbit_access_control:permission_cache_can_expire(User)),
MaxMessageSize = get_max_message_size(),
ConsumerTimeout = get_consumer_timeout(), ConsumerTimeout = get_consumer_timeout(),
OptionalVariables = extract_variable_map_from_amqp_params(AmqpParams), OptionalVariables = extract_variable_map_from_amqp_params(AmqpParams),
UseExtendedReturnCallback = use_extended_return_callback(AmqpParams),
{ok, GCThreshold} = application:get_env(rabbit, writer_gc_threshold), {ok, GCThreshold} = application:get_env(rabbit, writer_gc_threshold),
State = #ch{cfg = #conf{state = starting, State = #ch{cfg = #conf{state = starting,
protocol = Protocol, protocol = Protocol,
@ -532,17 +521,14 @@ init([Channel, ReaderPid, WriterPid, ConnPid, ConnName, Protocol, User, VHost,
capabilities = Capabilities, capabilities = Capabilities,
trace_state = rabbit_trace:init(VHost), trace_state = rabbit_trace:init(VHost),
consumer_prefetch = Prefetch, consumer_prefetch = Prefetch,
max_message_size = MaxMessageSize,
consumer_timeout = ConsumerTimeout, consumer_timeout = ConsumerTimeout,
authz_context = OptionalVariables, authz_context = OptionalVariables,
writer_gc_threshold = GCThreshold, writer_gc_threshold = GCThreshold
extended_return_callback = UseExtendedReturnCallback
}, },
limiter = Limiter, limiter = Limiter,
tx = none, tx = none,
next_tag = 1, next_tag = 1,
unacked_message_q = ?QUEUE:new(), unacked_message_q = ?QUEUE:new(),
queue_monitors = pmon:new(),
consumer_mapping = #{}, consumer_mapping = #{},
queue_consumers = #{}, queue_consumers = #{},
confirm_enabled = false, confirm_enabled = false,
@ -755,8 +741,7 @@ handle_info(emit_stats, State) ->
{noreply, send_confirms_and_nacks(State1), hibernate}; {noreply, send_confirms_and_nacks(State1), hibernate};
handle_info({{'DOWN', QName}, _MRef, process, QPid, Reason}, handle_info({{'DOWN', QName}, _MRef, process, QPid, Reason},
#ch{queue_states = QStates0, #ch{queue_states = QStates0} = State0) ->
queue_monitors = _QMons} = State0) ->
credit_flow:peer_down(QPid), credit_flow:peer_down(QPid),
case rabbit_queue_type:handle_down(QPid, QName, Reason, QStates0) of case rabbit_queue_type:handle_down(QPid, QName, Reason, QStates0) of
{ok, QState1, Actions} -> {ok, QState1, Actions} ->
@ -812,17 +797,17 @@ terminate(_Reason,
State = #ch{cfg = #conf{user = #user{username = Username}}, State = #ch{cfg = #conf{user = #user{username = Username}},
consumer_mapping = CM, consumer_mapping = CM,
queue_states = QueueCtxs}) -> queue_states = QueueCtxs}) ->
_ = rabbit_queue_type:close(QueueCtxs), rabbit_queue_type:close(QueueCtxs),
{_Res, _State1} = notify_queues(State), {_Res, _State1} = notify_queues(State),
pg_local:leave(rabbit_channels, self()), pg_local:leave(rabbit_channels, self()),
rabbit_event:if_enabled(State, #ch.stats_timer, rabbit_event:if_enabled(State, #ch.stats_timer,
fun() -> emit_stats(State) end), fun() -> emit_stats(State) end),
[delete_stats(Tag) || {Tag, _} <- get()], [delete_stats(Tag) || {Tag, _} <- get()],
maybe_decrease_global_publishers(State), maybe_decrease_global_publishers(State),
_ = maps:map( maps:foreach(
fun (_, _) -> fun (_, _) ->
rabbit_global_counters:consumer_deleted(amqp091) rabbit_global_counters:consumer_deleted(amqp091)
end, CM), end, CM),
rabbit_core_metrics:channel_closed(self()), rabbit_core_metrics:channel_closed(self()),
rabbit_event:notify(channel_closed, [{pid, self()}, rabbit_event:notify(channel_closed, [{pid, self()},
{user_who_performed_action, Username}, {user_who_performed_action, Username},
@ -839,16 +824,6 @@ code_change(_OldVsn, State, _Extra) ->
format_message_queue(Opt, MQ) -> rabbit_misc:format_message_queue(Opt, MQ). format_message_queue(Opt, MQ) -> rabbit_misc:format_message_queue(Opt, MQ).
-spec get_max_message_size() -> non_neg_integer().
get_max_message_size() ->
case application:get_env(rabbit, max_message_size) of
{ok, MS} when is_integer(MS) ->
erlang:min(MS, ?MAX_MSG_SIZE);
_ ->
?MAX_MSG_SIZE
end.
get_consumer_timeout() -> get_consumer_timeout() ->
case application:get_env(rabbit, consumer_timeout) of case application:get_env(rabbit, consumer_timeout) of
{ok, MS} when is_integer(MS) -> {ok, MS} when is_integer(MS) ->
@ -954,30 +929,19 @@ check_write_permitted_on_topic(Resource, User, RoutingKey, AuthzContext) ->
check_read_permitted_on_topic(Resource, User, RoutingKey, AuthzContext) -> check_read_permitted_on_topic(Resource, User, RoutingKey, AuthzContext) ->
check_topic_authorisation(Resource, User, RoutingKey, AuthzContext, read). check_topic_authorisation(Resource, User, RoutingKey, AuthzContext, read).
check_user_id_header(#'P_basic'{user_id = undefined}, _) -> check_user_id_header(Msg, User) ->
ok; case rabbit_access_control:check_user_id(Msg, User) of
check_user_id_header(#'P_basic'{user_id = Username}, ok ->
#ch{cfg = #conf{user = #user{username = Username}}}) -> ok;
ok; {refused, Reason, Args} ->
check_user_id_header( rabbit_misc:precondition_failed(Reason, Args)
#'P_basic'{}, #ch{cfg = #conf{user = #user{authz_backends =
[{rabbit_auth_backend_dummy, _}]}}}) ->
ok;
check_user_id_header(#'P_basic'{user_id = Claimed},
#ch{cfg = #conf{user = #user{username = Actual,
tags = Tags}}}) ->
case lists:member(impersonator, Tags) of
true -> ok;
false -> rabbit_misc:precondition_failed(
"user_id property set to '~ts' but authenticated user was "
"'~ts'", [Claimed, Actual])
end. end.
check_expiration_header(Props) -> check_expiration_header(Props) ->
case rabbit_basic:parse_expiration(Props) of case rabbit_basic:parse_expiration(Props) of
{ok, _} -> ok; {ok, _} -> ok;
{error, E} -> rabbit_misc:precondition_failed("invalid expiration '~ts': ~tp", {error, E} -> rabbit_misc:precondition_failed("invalid expiration '~ts': ~tp",
[Props#'P_basic'.expiration, E]) [Props#'P_basic'.expiration, E])
end. end.
check_internal_exchange(#exchange{name = Name, internal = true}) -> check_internal_exchange(#exchange{name = Name, internal = true}) ->
@ -1028,28 +992,21 @@ extract_variable_map_from_amqp_params([Value]) ->
extract_variable_map_from_amqp_params(_) -> extract_variable_map_from_amqp_params(_) ->
#{}. #{}.
%% Use tuple representation of amqp_params to avoid a dependency on amqp_client. check_msg_size(Content, GCThreshold) ->
%% Used for AMQP 1.0 MaxMessageSize = persistent_term:get(max_message_size),
use_extended_return_callback({amqp_params_direct,_,_,_,_,
{amqp_adapter_info,_,_,_,_,_,{'AMQP',"1.0"},_},
_}) ->
true;
use_extended_return_callback(_) ->
false.
check_msg_size(Content, MaxMessageSize, GCThreshold) ->
Size = rabbit_basic:maybe_gc_large_msg(Content, GCThreshold), Size = rabbit_basic:maybe_gc_large_msg(Content, GCThreshold),
case Size of case Size =< MaxMessageSize of
S when S > MaxMessageSize -> true ->
ErrorMessage = case MaxMessageSize of ok;
?MAX_MSG_SIZE -> false ->
"message size ~B is larger than max size ~B"; Fmt = case MaxMessageSize of
_ -> ?MAX_MSG_SIZE ->
"message size ~B is larger than configured max size ~B" "message size ~B is larger than max size ~B";
end, _ ->
rabbit_misc:precondition_failed(ErrorMessage, "message size ~B is larger than configured max size ~B"
[Size, MaxMessageSize]); end,
_ -> ok rabbit_misc:precondition_failed(
Fmt, [Size, MaxMessageSize])
end. end.
check_vhost_queue_limit(#resource{name = QueueName}, VHost) -> check_vhost_queue_limit(#resource{name = QueueName}, VHost) ->
@ -1226,22 +1183,21 @@ handle_method(#'basic.publish'{immediate = true}, _Content, _State) ->
handle_method(#'basic.publish'{exchange = ExchangeNameBin, handle_method(#'basic.publish'{exchange = ExchangeNameBin,
routing_key = RoutingKey, routing_key = RoutingKey,
mandatory = Mandatory}, mandatory = Mandatory},
Content, State = #ch{cfg = #conf{channel = ChannelNum, Content, State0 = #ch{cfg = #conf{channel = ChannelNum,
conn_name = ConnName, conn_name = ConnName,
virtual_host = VHostPath, virtual_host = VHostPath,
user = #user{username = Username} = User, user = #user{username = Username} = User,
trace_state = TraceState, trace_state = TraceState,
max_message_size = MaxMessageSize, authz_context = AuthzContext,
authz_context = AuthzContext, writer_gc_threshold = GCThreshold
writer_gc_threshold = GCThreshold },
},
tx = Tx, tx = Tx,
confirm_enabled = ConfirmEnabled, confirm_enabled = ConfirmEnabled,
delivery_flow = Flow delivery_flow = Flow
}) -> }) ->
State0 = maybe_increase_global_publishers(State), State1 = maybe_increase_global_publishers(State0),
rabbit_global_counters:messages_received(amqp091, 1), rabbit_global_counters:messages_received(amqp091, 1),
check_msg_size(Content, MaxMessageSize, GCThreshold), check_msg_size(Content, GCThreshold),
ExchangeName = rabbit_misc:r(VHostPath, exchange, ExchangeNameBin), ExchangeName = rabbit_misc:r(VHostPath, exchange, ExchangeNameBin),
check_write_permitted(ExchangeName, User, AuthzContext), check_write_permitted(ExchangeName, User, AuthzContext),
Exchange = rabbit_exchange:lookup_or_die(ExchangeName), Exchange = rabbit_exchange:lookup_or_die(ExchangeName),
@ -1251,19 +1207,19 @@ handle_method(#'basic.publish'{exchange = ExchangeNameBin,
%% certain to want to look at delivery-mode and priority. %% certain to want to look at delivery-mode and priority.
DecodedContent = #content {properties = Props} = DecodedContent = #content {properties = Props} =
maybe_set_fast_reply_to( maybe_set_fast_reply_to(
rabbit_binary_parser:ensure_content_decoded(Content), State), rabbit_binary_parser:ensure_content_decoded(Content), State1),
check_user_id_header(Props, State),
check_expiration_header(Props), check_expiration_header(Props),
DoConfirm = Tx =/= none orelse ConfirmEnabled, DoConfirm = Tx =/= none orelse ConfirmEnabled,
{DeliveryOptions, State1} = {DeliveryOptions, State} =
case DoConfirm of case DoConfirm of
false -> false ->
{maps_put_truthy(flow, Flow, #{mandatory => Mandatory}), State0}; {maps_put_truthy(flow, Flow, #{mandatory => Mandatory}), State1};
true -> true ->
rabbit_global_counters:messages_received_confirm(amqp091, 1), rabbit_global_counters:messages_received_confirm(amqp091, 1),
SeqNo = State0#ch.publish_seqno, SeqNo = State1#ch.publish_seqno,
Opts = maps_put_truthy(flow, Flow, #{correlation => SeqNo, mandatory => Mandatory}), Opts = maps_put_truthy(flow, Flow, #{correlation => SeqNo,
{Opts, State0#ch{publish_seqno = SeqNo + 1}} mandatory => Mandatory}),
{Opts, State1#ch{publish_seqno = SeqNo + 1}}
end, end,
case mc_amqpl:message(ExchangeName, case mc_amqpl:message(ExchangeName,
@ -1273,6 +1229,7 @@ handle_method(#'basic.publish'{exchange = ExchangeNameBin,
rabbit_misc:precondition_failed("invalid message: ~tp", [Reason]); rabbit_misc:precondition_failed("invalid message: ~tp", [Reason]);
{ok, Message0} -> {ok, Message0} ->
Message = rabbit_message_interceptor:intercept(Message0), Message = rabbit_message_interceptor:intercept(Message0),
check_user_id_header(Message, User),
QNames = rabbit_exchange:route(Exchange, Message, #{return_binding_keys => true}), QNames = rabbit_exchange:route(Exchange, Message, #{return_binding_keys => true}),
[rabbit_channel:deliver_reply(RK, Message) || [rabbit_channel:deliver_reply(RK, Message) ||
{virtual_reply_queue, RK} <- QNames], {virtual_reply_queue, RK} <- QNames],
@ -1283,10 +1240,10 @@ handle_method(#'basic.publish'{exchange = ExchangeNameBin,
Delivery = {Message, DeliveryOptions, Queues}, Delivery = {Message, DeliveryOptions, Queues},
{noreply, case Tx of {noreply, case Tx of
none -> none ->
deliver_to_queues(ExchangeName, Delivery, State1); deliver_to_queues(ExchangeName, Delivery, State);
{Msgs, Acks} -> {Msgs, Acks} ->
Msgs1 = ?QUEUE:in(Delivery, Msgs), Msgs1 = ?QUEUE:in(Delivery, Msgs),
State1#ch{tx = {Msgs1, Acks}} State#ch{tx = {Msgs1, Acks}}
end} end}
end; end;
@ -1729,19 +1686,6 @@ handle_method(#'channel.flow'{active = true}, _, State) ->
handle_method(#'channel.flow'{active = false}, _, _State) -> handle_method(#'channel.flow'{active = false}, _, _State) ->
rabbit_misc:protocol_error(not_implemented, "active=false", []); rabbit_misc:protocol_error(not_implemented, "active=false", []);
handle_method(#'basic.credit'{consumer_tag = CTag,
credit = Credit,
drain = Drain},
_, State = #ch{consumer_mapping = Consumers,
queue_states = QStates0}) ->
case maps:find(CTag, Consumers) of
{ok, {Q, _CParams}} ->
{ok, QStates, Actions} = rabbit_queue_type:credit(Q, CTag, Credit, Drain, QStates0),
{noreply, handle_queue_actions(Actions, State#ch{queue_states = QStates})};
error -> rabbit_misc:precondition_failed(
"unknown consumer tag '~ts'", [CTag])
end;
handle_method(_MethodRecord, _Content, _State) -> handle_method(_MethodRecord, _Content, _State) ->
rabbit_misc:protocol_error( rabbit_misc:protocol_error(
command_invalid, "unimplemented method", []). command_invalid, "unimplemented method", []).
@ -2146,10 +2090,10 @@ deliver_to_queues(XName,
{ok, QueueStates, Actions} -> {ok, QueueStates, Actions} ->
rabbit_global_counters:messages_routed(amqp091, length(Qs)), rabbit_global_counters:messages_routed(amqp091, length(Qs)),
QueueNames = rabbit_amqqueue:queue_names(Qs), QueueNames = rabbit_amqqueue:queue_names(Qs),
MsgSeqNo = maps:get(correlation, Options, undefined),
%% NB: the order here is important since basic.returns must be %% NB: the order here is important since basic.returns must be
%% sent before confirms. %% sent before confirms.
ok = process_routing_mandatory(Mandatory, RoutedToQueues, MsgSeqNo, Message, XName, State0), ok = process_routing_mandatory(Mandatory, RoutedToQueues, Message, XName, State0),
MsgSeqNo = maps:get(correlation, Options, undefined),
State1 = process_routing_confirm(MsgSeqNo, QueueNames, XName, State0), State1 = process_routing_confirm(MsgSeqNo, QueueNames, XName, State0),
%% Actions must be processed after registering confirms as actions may %% Actions must be processed after registering confirms as actions may
%% contain rejections of publishes %% contain rejections of publishes
@ -2178,32 +2122,23 @@ deliver_to_queues(XName,
process_routing_mandatory(_Mandatory = true, process_routing_mandatory(_Mandatory = true,
_RoutedToQs = [], _RoutedToQs = [],
MsgSeqNo,
Msg, Msg,
XName, XName,
State = #ch{cfg = #conf{extended_return_callback = ExtRetCallback}}) -> State) ->
rabbit_global_counters:messages_unroutable_returned(amqp091, 1), rabbit_global_counters:messages_unroutable_returned(amqp091, 1),
?INCR_STATS(exchange_stats, XName, 1, return_unroutable, State), ?INCR_STATS(exchange_stats, XName, 1, return_unroutable, State),
Content0 = mc:protocol_state(Msg), Content = mc:protocol_state(Msg),
Content = case ExtRetCallback of
true ->
%% providing the publishing sequence for AMQP 1.0
{MsgSeqNo, Content0};
false ->
Content0
end,
[RoutingKey | _] = mc:routing_keys(Msg), [RoutingKey | _] = mc:routing_keys(Msg),
ok = basic_return(Content, RoutingKey, XName#resource.name, State, no_route); ok = basic_return(Content, RoutingKey, XName#resource.name, State, no_route);
process_routing_mandatory(_Mandatory = false, process_routing_mandatory(_Mandatory = false,
_RoutedToQs = [], _RoutedToQs = [],
_MsgSeqNo,
_Msg, _Msg,
XName, XName,
State) -> State) ->
rabbit_global_counters:messages_unroutable_dropped(amqp091, 1), rabbit_global_counters:messages_unroutable_dropped(amqp091, 1),
?INCR_STATS(exchange_stats, XName, 1, drop_unroutable, State), ?INCR_STATS(exchange_stats, XName, 1, drop_unroutable, State),
ok; ok;
process_routing_mandatory(_, _, _, _, _, _) -> process_routing_mandatory(_, _, _, _, _) ->
ok. ok.
process_routing_confirm(undefined, _, _, State) -> process_routing_confirm(undefined, _, _, State) ->
@ -2797,12 +2732,11 @@ handle_consumer_timed_out(Timeout,#pending_ack{delivery_tag = DeliveryTag, tag =
[Channel, Timeout], none), [Channel, Timeout], none),
handle_exception(Ex, State). handle_exception(Ex, State).
handle_queue_actions(Actions, #ch{cfg = #conf{writer_pid = WriterPid}} = State0) -> handle_queue_actions(Actions, State) ->
lists:foldl( lists:foldl(
fun fun({settled, QRef, MsgSeqNos}, S0) ->
({settled, QRef, MsgSeqNos}, S0) ->
confirm(MsgSeqNos, QRef, S0); confirm(MsgSeqNos, QRef, S0);
({rejected, _QRef, MsgSeqNos}, S0) -> ({rejected, _QRef, MsgSeqNos}, S0) ->
{U, Rej} = {U, Rej} =
lists:foldr( lists:foldr(
fun(SeqNo, {U1, Acc}) -> fun(SeqNo, {U1, Acc}) ->
@ -2815,26 +2749,17 @@ handle_queue_actions(Actions, #ch{cfg = #conf{writer_pid = WriterPid}} = State0)
end, {S0#ch.unconfirmed, []}, MsgSeqNos), end, {S0#ch.unconfirmed, []}, MsgSeqNos),
S = S0#ch{unconfirmed = U}, S = S0#ch{unconfirmed = U},
record_rejects(Rej, S); record_rejects(Rej, S);
({deliver, CTag, AckRequired, Msgs}, S0) -> ({deliver, CTag, AckRequired, Msgs}, S0) ->
handle_deliver(CTag, AckRequired, Msgs, S0); handle_deliver(CTag, AckRequired, Msgs, S0);
({queue_down, QRef}, S0) -> ({queue_down, QRef}, S0) ->
handle_consuming_queue_down_or_eol(QRef, S0); handle_consuming_queue_down_or_eol(QRef, S0);
({block, QName}, S0) -> ({block, QName}, S0) ->
credit_flow:block(QName), credit_flow:block(QName),
S0; S0;
({unblock, QName}, S0) -> ({unblock, QName}, S0) ->
credit_flow:unblock(QName), credit_flow:unblock(QName),
S0;
({send_credit_reply, Avail}, S0) ->
ok = rabbit_writer:send_command(WriterPid,
#'basic.credit_ok'{available = Avail}),
S0;
({send_drained, {CTag, Credit}}, S0) ->
ok = rabbit_writer:send_command(WriterPid,
#'basic.credit_drained'{consumer_tag = CTag,
credit_drained = Credit}),
S0 S0
end, State0, Actions). end, State, Actions).
handle_eol(QName, State0) -> handle_eol(QName, State0) ->
State1 = handle_consuming_queue_down_or_eol(QName, State0), State1 = handle_consuming_queue_down_or_eol(QName, State0),

View File

@ -41,7 +41,8 @@
handle_event/3, handle_event/3,
deliver/3, deliver/3,
settle/5, settle/5,
credit/5, credit_v1/5,
credit/7,
dequeue/5, dequeue/5,
info/2, info/2,
state_info/1, state_info/1,
@ -58,8 +59,9 @@
-export([confirm_to_sender/3, -export([confirm_to_sender/3,
send_rejection/3, send_rejection/3,
deliver_to_consumer/5, deliver_to_consumer/5,
send_drained/3, send_credit_reply_credit_api_v1/3,
send_credit_reply/3]). send_drained_credit_api_v1/4,
send_credit_reply/7]).
-spec is_enabled() -> boolean(). -spec is_enabled() -> boolean().
is_enabled() -> true. is_enabled() -> true.
@ -237,16 +239,17 @@ consume(Q, Spec, State0) when ?amqqueue_is_classic(Q) ->
channel_pid := ChPid, channel_pid := ChPid,
limiter_pid := LimiterPid, limiter_pid := LimiterPid,
limiter_active := LimiterActive, limiter_active := LimiterActive,
prefetch_count := ConsumerPrefetchCount, mode := Mode,
consumer_tag := ConsumerTag, consumer_tag := ConsumerTag,
exclusive_consume := ExclusiveConsume, exclusive_consume := ExclusiveConsume,
args := Args, args := Args0,
ok_msg := OkMsg, ok_msg := OkMsg,
acting_user := ActingUser} = Spec, acting_user := ActingUser} = Spec,
{ModeOrPrefetch, Args} = consume_backwards_compat(Mode, Args0),
case delegate:invoke(QPid, case delegate:invoke(QPid,
{gen_server2, call, {gen_server2, call,
[{basic_consume, NoAck, ChPid, LimiterPid, [{basic_consume, NoAck, ChPid, LimiterPid,
LimiterActive, ConsumerPrefetchCount, ConsumerTag, LimiterActive, ModeOrPrefetch, ConsumerTag,
ExclusiveConsume, Args, OkMsg, ActingUser}, ExclusiveConsume, Args, OkMsg, ActingUser},
infinity]}) of infinity]}) of
ok -> ok ->
@ -257,6 +260,22 @@ consume(Q, Spec, State0) when ?amqqueue_is_classic(Q) ->
Err Err
end. end.
%% Delete this function when feature flag credit_api_v2 becomes required.
consume_backwards_compat({simple_prefetch, PrefetchCount} = Mode, Args) ->
case rabbit_feature_flags:is_enabled(credit_api_v2) of
true -> {Mode, Args};
false -> {PrefetchCount, Args}
end;
consume_backwards_compat({credited, InitialDeliveryCount} = Mode, Args)
when is_integer(InitialDeliveryCount) ->
%% credit API v2
{Mode, Args};
consume_backwards_compat({credited, credit_api_v1}, Args) ->
%% credit API v1
{_PrefetchCount = 0,
[{<<"x-credit">>, table, [{<<"credit">>, long, 0},
{<<"drain">>, bool, false}]} | Args]}.
cancel(Q, ConsumerTag, OkMsg, ActingUser, State) -> cancel(Q, ConsumerTag, OkMsg, ActingUser, State) ->
QPid = amqqueue:get_pid(Q), QPid = amqqueue:get_pid(Q),
case delegate:invoke(QPid, {gen_server2, call, case delegate:invoke(QPid, {gen_server2, call,
@ -282,11 +301,14 @@ settle(_QName, Op, _CTag, MsgIds, State) ->
[{reject, Op == requeue, MsgIds, ChPid}]}), [{reject, Op == requeue, MsgIds, ChPid}]}),
{State, []}. {State, []}.
credit(_QName, CTag, Credit, Drain, State) -> credit_v1(_QName, Ctag, LinkCreditSnd, Drain, #?STATE{pid = QPid} = State) ->
ChPid = self(), Request = {credit, self(), Ctag, LinkCreditSnd, Drain},
delegate:invoke_no_result(State#?STATE.pid, delegate:invoke_no_result(QPid, {gen_server2, cast, [Request]}),
{gen_server2, cast, {State, []}.
[{credit, ChPid, CTag, Credit, Drain}]}),
credit(_QName, Ctag, DeliveryCountRcv, LinkCreditRcv, Drain, Echo, #?STATE{pid = QPid} = State) ->
Request = {credit, self(), Ctag, DeliveryCountRcv, LinkCreditRcv, Drain, Echo},
delegate:invoke_no_result(QPid, {gen_server2, cast, [Request]}),
{State, []}. {State, []}.
handle_event(QName, {confirm, MsgSeqNos, Pid}, #?STATE{unconfirmed = U0} = State) -> handle_event(QName, {confirm, MsgSeqNos, Pid}, #?STATE{unconfirmed = U0} = State) ->
@ -352,9 +374,13 @@ handle_event(QName, {down, Pid, Info}, #?STATE{monitored = Monitored,
{ok, State#?STATE{unconfirmed = U}, {ok, State#?STATE{unconfirmed = U},
[{rejected, QName, MsgIds} | Actions0]} [{rejected, QName, MsgIds} | Actions0]}
end; end;
handle_event(_QName, {send_drained, _} = Action, State) -> handle_event(_QName, Action, State)
when element(1, Action) =:= credit_reply ->
{ok, State, [Action]}; {ok, State, [Action]};
handle_event(_QName, {send_credit_reply, _} = Action, State) -> handle_event(_QName, {send_drained, {Ctag, Credit}}, State) ->
%% This function clause should be deleted when feature flag
%% credit_api_v2 becomes required.
Action = {credit_reply_v1, Ctag, Credit, _Available = 0, _Drain = true},
{ok, State, [Action]}. {ok, State, [Action]}.
settlement_action(_Type, _QRef, [], Acc) -> settlement_action(_Type, _QRef, [], Acc) ->
@ -610,26 +636,30 @@ ensure_monitor(Pid, QName, State = #?STATE{monitored = Monitored}) ->
%% part of channel <-> queue api %% part of channel <-> queue api
confirm_to_sender(Pid, QName, MsgSeqNos) -> confirm_to_sender(Pid, QName, MsgSeqNos) ->
Msg = {confirm, MsgSeqNos, self()}, Evt = {confirm, MsgSeqNos, self()},
gen_server:cast(Pid, {queue_event, QName, Msg}). send_queue_event(Pid, QName, Evt).
send_rejection(Pid, QName, MsgSeqNo) -> send_rejection(Pid, QName, MsgSeqNo) ->
Msg = {reject_publish, MsgSeqNo, self()}, Evt = {reject_publish, MsgSeqNo, self()},
gen_server:cast(Pid, {queue_event, QName, Msg}). send_queue_event(Pid, QName, Evt).
deliver_to_consumer(Pid, QName, CTag, AckRequired, Message) -> deliver_to_consumer(Pid, QName, CTag, AckRequired, Message) ->
Deliver = {deliver, CTag, AckRequired, [Message]}, Evt = {deliver, CTag, AckRequired, [Message]},
Evt = {queue_event, QName, Deliver}, send_queue_event(Pid, QName, Evt).
gen_server:cast(Pid, Evt).
send_drained(Pid, QName, CTagCredits) when is_list(CTagCredits) -> %% Delete this function when feature flag credit_api_v2 becomes required.
lists:foreach(fun(CTagCredit) -> send_credit_reply_credit_api_v1(Pid, QName, Available) ->
send_drained(Pid, QName, CTagCredit) Evt = {send_credit_reply, Available},
end, CTagCredits); send_queue_event(Pid, QName, Evt).
send_drained(Pid, QName, CTagCredit) when is_tuple(CTagCredit) ->
gen_server:cast(Pid, {queue_event, QName,
{send_drained, CTagCredit}}).
send_credit_reply(Pid, QName, Len) when is_integer(Len) -> %% Delete this function when feature flag credit_api_v2 becomes required.
gen_server:cast(Pid, {queue_event, QName, send_drained_credit_api_v1(Pid, QName, Ctag, Credit) ->
{send_credit_reply, Len}}). Evt = {send_drained, {Ctag, Credit}},
send_queue_event(Pid, QName, Evt).
send_credit_reply(Pid, QName, Ctag, DeliveryCount, Credit, Available, Drain) ->
Evt = {credit_reply, Ctag, DeliveryCount, Credit, Available, Drain},
send_queue_event(Pid, QName, Evt).
send_queue_event(Pid, QName, Event) ->
gen_server:cast(Pid, {queue_event, QName, Event}).

View File

@ -45,7 +45,7 @@ insert(SeqNo, QNames, #resource{kind = exchange} = XName,
when is_integer(SeqNo) when is_integer(SeqNo)
andalso is_list(QNames) andalso is_list(QNames)
andalso not is_map_key(SeqNo, U0) -> andalso not is_map_key(SeqNo, U0) ->
U = U0#{SeqNo => {XName, maps:from_list([{Q, ok} || Q <- QNames])}}, U = U0#{SeqNo => {XName, maps:from_keys(QNames, ok)}},
S = case S0 of S = case S0 of
undefined -> SeqNo; undefined -> SeqNo;
_ -> S0 _ -> S0
@ -58,20 +58,18 @@ insert(SeqNo, QNames, #resource{kind = exchange} = XName,
confirm(SeqNos, QName, #?MODULE{smallest = Smallest0, confirm(SeqNos, QName, #?MODULE{smallest = Smallest0,
unconfirmed = U0} = State) unconfirmed = U0} = State)
when is_list(SeqNos) -> when is_list(SeqNos) ->
{Confirmed, U} = lists:foldr( {Confirmed, ConfirmedSmallest, U} =
fun (SeqNo, Acc) -> lists:foldl(
confirm_one(SeqNo, QName, Acc) fun (SeqNo, Acc) ->
end, {[], U0}, SeqNos), confirm_one(SeqNo, QName, Smallest0, Acc)
%% check if smallest is in Confirmed end, {[], false, U0}, SeqNos),
%% TODO: this can be optimised by checking in the preceeding foldr Smallest = case ConfirmedSmallest of
Smallest = true ->
case lists:any(fun ({S, _}) -> S == Smallest0 end, Confirmed) of %% work out new smallest
true -> next_smallest(Smallest0, U);
%% work out new smallest false ->
next_smallest(Smallest0, U); Smallest0
false -> end,
Smallest0
end,
{Confirmed, State#?MODULE{smallest = Smallest, {Confirmed, State#?MODULE{smallest = Smallest,
unconfirmed = U}}. unconfirmed = U}}.
@ -124,17 +122,21 @@ is_empty(State) ->
%% INTERNAL %% INTERNAL
confirm_one(SeqNo, QName, {Acc, U0}) -> confirm_one(SeqNo, QName, Smallest, {Acc, ConfirmedSmallest0, U0}) ->
case maps:take(SeqNo, U0) of case maps:take(SeqNo, U0) of
{{XName, QS}, U1} {{XName, QS}, U1}
when is_map_key(QName, QS) when is_map_key(QName, QS)
andalso map_size(QS) == 1 -> andalso map_size(QS) == 1 ->
%% last queue confirm %% last queue confirm
{[{SeqNo, XName} | Acc], U1}; ConfirmedSmallest = case SeqNo of
Smallest -> true;
_ -> ConfirmedSmallest0
end,
{[{SeqNo, XName} | Acc], ConfirmedSmallest, U1};
{{XName, QS}, U1} -> {{XName, QS}, U1} ->
{Acc, U1#{SeqNo => {XName, maps:remove(QName, QS)}}}; {Acc, ConfirmedSmallest0, U1#{SeqNo => {XName, maps:remove(QName, QS)}}};
error -> error ->
{Acc, U0} {Acc, ConfirmedSmallest0, U0}
end. end.
next_smallest(_S, U) when map_size(U) == 0 -> next_smallest(_S, U) when map_size(U) == 0 ->

View File

@ -18,7 +18,7 @@
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/0]). -export([start_link/1]).
-export([ -export([
start_channel_sup_sup/1, start_channel_sup_sup/1,
start_queue_collector/2 start_queue_collector/2
@ -30,10 +30,10 @@
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
-spec start_link() -> rabbit_types:ok_pid_or_error(). -spec start_link(supervisor:sup_flags()) ->
supervisor:startlink_ret().
start_link() -> start_link(SupFlags) ->
supervisor:start_link(?MODULE, []). supervisor:start_link(?MODULE, SupFlags).
-spec start_channel_sup_sup(pid()) -> rabbit_types:ok_pid_or_error(). -spec start_channel_sup_sup(pid()) -> rabbit_types:ok_pid_or_error().
@ -62,10 +62,6 @@ start_queue_collector(SupPid, Identity) ->
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
init([]) -> init(SupFlags) ->
?LG_PROCESS_TYPE(connection_helper_sup), ?LG_PROCESS_TYPE(connection_helper_sup),
SupFlags = #{strategy => one_for_one,
intensity => 10,
period => 10,
auto_shutdown => any_significant},
{ok, {SupFlags, []}}. {ok, {SupFlags, []}}.

View File

@ -19,7 +19,10 @@
-behaviour(supervisor). -behaviour(supervisor).
-behaviour(ranch_protocol). -behaviour(ranch_protocol).
-export([start_link/3, reader/1]). -export([start_link/3,
reader/1,
start_connection_helper_sup/2
]).
-export([init/1]). -export([init/1]).
@ -27,40 +30,17 @@
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
-spec start_link(any(), module(), any()) -> -spec start_link(ranch:ref(), module(), any()) ->
{'ok', pid(), pid()}. {'ok', pid(), pid()}.
start_link(Ref, _Transport, _Opts) -> start_link(Ref, _Transport, _Opts) ->
{ok, SupPid} = supervisor:start_link(?MODULE, []), {ok, SupPid} = supervisor:start_link(?MODULE, []),
%% We need to get channels in the hierarchy here so they get shut
%% down after the reader, so the reader gets a chance to terminate
%% them cleanly. But for 1.0 readers we can't start the real
%% ch_sup_sup (because we don't know if we will be 0-9-1 or 1.0) -
%% so we add another supervisor into the hierarchy.
%%
%% This supervisor also acts as an intermediary for heartbeaters and
%% the queue collector process, since these must not be siblings of the
%% reader due to the potential for deadlock if they are added/restarted
%% whilst the supervision tree is shutting down.
{ok, HelperSup} =
supervisor:start_child(
SupPid,
#{
id => helper_sup,
start => {rabbit_connection_helper_sup, start_link, []},
restart => transient,
significant => true,
shutdown => infinity,
type => supervisor,
modules => [rabbit_connection_helper_sup]
}
),
{ok, ReaderPid} = {ok, ReaderPid} =
supervisor:start_child( supervisor:start_child(
SupPid, SupPid,
#{ #{
id => reader, id => reader,
start => {rabbit_reader, start_link, [HelperSup, Ref]}, start => {rabbit_reader, start_link, [Ref]},
restart => transient, restart => transient,
significant => true, significant => true,
shutdown => ?WORKER_WAIT, shutdown => ?WORKER_WAIT,
@ -75,6 +55,20 @@ start_link(Ref, _Transport, _Opts) ->
reader(Pid) -> reader(Pid) ->
hd(rabbit_misc:find_child(Pid, reader)). hd(rabbit_misc:find_child(Pid, reader)).
-spec start_connection_helper_sup(pid(), supervisor:sup_flags()) ->
supervisor:startchild_ret().
start_connection_helper_sup(ConnectionSupPid, ConnectionHelperSupFlags) ->
supervisor:start_child(
ConnectionSupPid,
#{
id => helper_sup,
start => {rabbit_connection_helper_sup, start_link, [ConnectionHelperSupFlags]},
restart => transient,
significant => true,
shutdown => infinity,
type => supervisor
}).
%%-------------------------------------------------------------------------- %%--------------------------------------------------------------------------
init([]) -> init([]) ->

View File

@ -123,6 +123,8 @@
-rabbit_feature_flag( -rabbit_feature_flag(
{message_containers, {message_containers,
#{desc => "Message containers.", #{desc => "Message containers.",
%%TODO Once lower version node in mixed versions is bumped to 3.13,
%% make 'required' for upgrading AMQP 1.0 from 3.13 to 4.0
stability => stable, stability => stable,
depends_on => [feature_flags_v2] depends_on => [feature_flags_v2]
}}). }}).
@ -156,3 +158,9 @@
stability => stable, stability => stable,
depends_on => [stream_queue] depends_on => [stream_queue]
}}). }}).
-rabbit_feature_flag(
{credit_api_v2,
#{desc => "Credit API v2 between queue clients and queue processes",
stability => stable
}}).

View File

@ -74,6 +74,8 @@
chunk_disk_msgs/3]). chunk_disk_msgs/3]).
-endif. -endif.
-import(serial_number, [add/2, diff/2]).
%% command records representing all the protocol actions that are supported %% command records representing all the protocol actions that are supported
-record(enqueue, {pid :: option(pid()), -record(enqueue, {pid :: option(pid()),
seq :: option(msg_seqno()), seq :: option(msg_seqno()),
@ -95,7 +97,7 @@
msg_ids :: [msg_id()]}). msg_ids :: [msg_id()]}).
-record(credit, {consumer_id :: consumer_id(), -record(credit, {consumer_id :: consumer_id(),
credit :: non_neg_integer(), credit :: non_neg_integer(),
delivery_count :: non_neg_integer(), delivery_count :: rabbit_queue_type:delivery_count(),
drain :: boolean()}). drain :: boolean()}).
-record(purge, {}). -record(purge, {}).
-record(purge_nodes, {nodes :: [node()]}). -record(purge_nodes, {nodes :: [node()]}).
@ -130,7 +132,6 @@
delivery/0, delivery/0,
command/0, command/0,
credit_mode/0, credit_mode/0,
consumer_tag/0,
consumer_meta/0, consumer_meta/0,
consumer_id/0, consumer_id/0,
client_msg/0, client_msg/0,
@ -184,8 +185,8 @@ update_config(Conf, State) ->
% msg_ids are scoped per consumer % msg_ids are scoped per consumer
% ra_indexes holds all raft indexes for enqueues currently on queue % ra_indexes holds all raft indexes for enqueues currently on queue
-spec apply(ra_machine:command_meta_data(), command(), state()) -> -spec apply(ra_machine:command_meta_data(), command(), state()) ->
{state(), Reply :: term(), ra_machine:effects()} | {state(), ra_machine:reply(), ra_machine:effects() | ra_machine:effect()} |
{state(), Reply :: term()}. {state(), ra_machine:reply()}.
apply(Meta, #enqueue{pid = From, seq = Seq, apply(Meta, #enqueue{pid = From, seq = Seq,
msg = RawMsg}, State00) -> msg = RawMsg}, State00) ->
apply_enqueue(Meta, From, Seq, RawMsg, State00); apply_enqueue(Meta, From, Seq, RawMsg, State00);
@ -276,59 +277,92 @@ apply(#{index := Idx} = Meta,
_ -> _ ->
{State00, ok, []} {State00, ok, []}
end; end;
apply(Meta, #credit{credit = NewCredit, delivery_count = RemoteDelCnt, apply(Meta, #credit{credit = LinkCreditRcv, delivery_count = DeliveryCountRcv,
drain = Drain, consumer_id = ConsumerId}, drain = Drain, consumer_id = ConsumerId = {CTag, CPid}},
#?MODULE{consumers = Cons0, #?MODULE{consumers = Cons0,
service_queue = ServiceQueue0, service_queue = ServiceQueue0,
waiting_consumers = Waiting0} = State0) -> waiting_consumers = Waiting0} = State0) ->
case Cons0 of case Cons0 of
#{ConsumerId := #consumer{delivery_count = DelCnt} = Con0} -> #{ConsumerId := #consumer{delivery_count = DeliveryCountSnd,
%% this can go below 0 when credit is reduced cfg = Cfg} = Con0} ->
C = max(0, RemoteDelCnt + NewCredit - DelCnt), LinkCreditSnd = link_credit_snd(DeliveryCountRcv, LinkCreditRcv, DeliveryCountSnd, Cfg),
%% grant the credit %% grant the credit
Con1 = Con0#consumer{credit = C}, Con1 = Con0#consumer{credit = LinkCreditSnd},
ServiceQueue = maybe_queue_consumer(ConsumerId, Con1, ServiceQueue = maybe_queue_consumer(ConsumerId, Con1, ServiceQueue0),
ServiceQueue0), State1 = State0#?MODULE{service_queue = ServiceQueue,
Cons = maps:put(ConsumerId, Con1, Cons0), consumers = maps:update(ConsumerId, Con1, Cons0)},
{State1, ok, Effects} = {State2, ok, Effects} = checkout(Meta, State0, State1, []),
checkout(Meta, State0,
State0#?MODULE{service_queue = ServiceQueue, #?MODULE{consumers = Cons1 = #{ConsumerId := Con2}} = State2,
consumers = Cons}, []), #consumer{credit = PostCred,
Response = {send_credit_reply, messages_ready(State1)}, delivery_count = PostDeliveryCount} = Con2,
%% by this point all checkouts for the updated credit value Available = messages_ready(State2),
%% should be processed so we can evaluate the drain case credit_api_v2(Cfg) of
case Drain of
false ->
%% just return the result of the checkout
{State1, Response, Effects};
true -> true ->
Con = #consumer{credit = PostCred} = {Credit, DeliveryCount, State} =
maps:get(ConsumerId, State1#?MODULE.consumers), case Drain andalso PostCred > 0 of
%% add the outstanding credit to the delivery count true ->
DeliveryCount = Con#consumer.delivery_count + PostCred, AdvancedDeliveryCount = add(PostDeliveryCount, PostCred),
Consumers = maps:put(ConsumerId, ZeroCredit = 0,
Con#consumer{delivery_count = DeliveryCount, Con = Con2#consumer{delivery_count = AdvancedDeliveryCount,
credit = 0}, credit = ZeroCredit},
State1#?MODULE.consumers), Cons = maps:update(ConsumerId, Con, Cons1),
Drained = Con#consumer.credit, State3 = State2#?MODULE{consumers = Cons},
{CTag, _} = ConsumerId, {ZeroCredit, AdvancedDeliveryCount, State3};
{State1#?MODULE{consumers = Consumers}, false ->
%% returning a multi response with two client actions {PostCred, PostDeliveryCount, State2}
%% for the channel to execute end,
{multi, [Response, {send_drained, {CTag, Drained}}]}, %% We must send to queue client delivery effects before credit_reply such
Effects} %% that session process can send to AMQP 1.0 client TRANSFERs before FLOW.
{State, ok, Effects ++ [{send_msg, CPid,
{credit_reply, CTag, DeliveryCount, Credit, Available, Drain},
?DELIVERY_SEND_MSG_OPTS}]};
false ->
%% We must always send a send_credit_reply because basic.credit is synchronous.
%% Additionally, we keep the bug of credit API v1 that we send to queue client the
%% send_drained reply before the delivery effects (resulting in the wrong behaviour
%% that the session process sends to AMQP 1.0 client the FLOW before the TRANSFERs).
%% We have to keep this bug because old rabbit_fifo_client implementations expect
%% a send_drained Ra reply (they can't handle such a Ra effect).
CreditReply = {send_credit_reply, Available},
case Drain of
true ->
AdvancedDeliveryCount = PostDeliveryCount + PostCred,
Con = Con2#consumer{delivery_count = AdvancedDeliveryCount,
credit = 0},
Cons = maps:update(ConsumerId, Con, Cons1),
State = State2#?MODULE{consumers = Cons},
Reply = {multi, [CreditReply, {send_drained, {CTag, PostCred}}]},
{State, Reply, Effects};
false ->
{State2, CreditReply, Effects}
end
end; end;
_ when Waiting0 /= [] -> _ when Waiting0 /= [] ->
%% there are waiting consuemrs %%TODO next time when we bump the machine version:
%% 1. Do not put consumer at head of waiting_consumers if NewCredit == 0
%% to reduce likelihood of activating a 0 credit consumer.
%% 2. Support Drain == true, i.e. advance delivery-count, consuming all link-credit since there
%% are no messages available for an inactive consumer and send credit_reply with Drain=true.
case lists:keytake(ConsumerId, 1, Waiting0) of case lists:keytake(ConsumerId, 1, Waiting0) of
{value, {_, Con0 = #consumer{delivery_count = DelCnt}}, Waiting} -> {value, {_, Con0 = #consumer{delivery_count = DeliveryCountSnd,
%% the consumer is a waiting one cfg = Cfg}}, Waiting} ->
LinkCreditSnd = link_credit_snd(DeliveryCountRcv, LinkCreditRcv, DeliveryCountSnd, Cfg),
%% grant the credit %% grant the credit
C = max(0, RemoteDelCnt + NewCredit - DelCnt), Con = Con0#consumer{credit = LinkCreditSnd},
Con = Con0#consumer{credit = C},
State = State0#?MODULE{waiting_consumers = State = State0#?MODULE{waiting_consumers =
[{ConsumerId, Con} | Waiting]}, [{ConsumerId, Con} | Waiting]},
{State, {send_credit_reply, messages_ready(State)}}; %% No messages are available for inactive consumers.
Available = 0,
case credit_api_v2(Cfg) of
true ->
{State, ok,
{send_msg, CPid,
{credit_reply, CTag, DeliveryCountSnd, LinkCreditSnd, Available, false},
?DELIVERY_SEND_MSG_OPTS}};
false ->
{State, {send_credit_reply, Available}}
end;
false -> false ->
{State0, ok} {State0, ok}
end; end;
@ -1240,12 +1274,12 @@ query_consumers(#?MODULE{consumers = Consumers,
FromConsumers = FromConsumers =
maps:fold(fun (_, #consumer{status = cancelled}, Acc) -> maps:fold(fun (_, #consumer{status = cancelled}, Acc) ->
Acc; Acc;
({Tag, Pid}, (Key = {Tag, Pid},
#consumer{cfg = #consumer_cfg{meta = Meta}} = Consumer, #consumer{cfg = #consumer_cfg{meta = Meta}} = Consumer,
Acc) -> Acc) ->
{Active, ActivityStatus} = {Active, ActivityStatus} =
ActiveActivityStatusFun({Tag, Pid}, Consumer), ActiveActivityStatusFun(Key, Consumer),
maps:put({Tag, Pid}, maps:put(Key,
{Pid, Tag, {Pid, Tag,
maps:get(ack, Meta, undefined), maps:get(ack, Meta, undefined),
maps:get(prefetch, Meta, undefined), maps:get(prefetch, Meta, undefined),
@ -1258,12 +1292,12 @@ query_consumers(#?MODULE{consumers = Consumers,
FromWaitingConsumers = FromWaitingConsumers =
lists:foldl(fun ({_, #consumer{status = cancelled}}, Acc) -> lists:foldl(fun ({_, #consumer{status = cancelled}}, Acc) ->
Acc; Acc;
({{Tag, Pid}, (Key = {{Tag, Pid},
#consumer{cfg = #consumer_cfg{meta = Meta}} = Consumer}, #consumer{cfg = #consumer_cfg{meta = Meta}} = Consumer},
Acc) -> Acc) ->
{Active, ActivityStatus} = {Active, ActivityStatus} =
ActiveActivityStatusFun({Tag, Pid}, Consumer), ActiveActivityStatusFun(Key, Consumer),
maps:put({Tag, Pid}, maps:put(Key,
{Pid, Tag, {Pid, Tag,
maps:get(ack, Meta, undefined), maps:get(ack, Meta, undefined),
maps:get(prefetch, Meta, undefined), maps:get(prefetch, Meta, undefined),
@ -2032,7 +2066,7 @@ get_next_msg(#?MODULE{returns = Returns0,
delivery_effect({CTag, CPid}, [{MsgId, ?MSG(Idx, Header)}], delivery_effect({CTag, CPid}, [{MsgId, ?MSG(Idx, Header)}],
#?MODULE{msg_cache = {Idx, RawMsg}}) -> #?MODULE{msg_cache = {Idx, RawMsg}}) ->
{send_msg, CPid, {delivery, CTag, [{MsgId, {Header, RawMsg}}]}, {send_msg, CPid, {delivery, CTag, [{MsgId, {Header, RawMsg}}]},
[local, ra_event]}; ?DELIVERY_SEND_MSG_OPTS};
delivery_effect({CTag, CPid}, Msgs, _State) -> delivery_effect({CTag, CPid}, Msgs, _State) ->
RaftIdxs = lists:foldr(fun ({_, ?MSG(I, _)}, Acc) -> RaftIdxs = lists:foldr(fun ({_, ?MSG(I, _)}, Acc) ->
[I | Acc] [I | Acc]
@ -2043,7 +2077,7 @@ delivery_effect({CTag, CPid}, Msgs, _State) ->
fun (Cmd, {MsgId, ?MSG(_Idx, Header)}) -> fun (Cmd, {MsgId, ?MSG(_Idx, Header)}) ->
{MsgId, {Header, get_msg(Cmd)}} {MsgId, {Header, get_msg(Cmd)}}
end, Log, Msgs), end, Log, Msgs),
[{send_msg, CPid, {delivery, CTag, DelMsgs}, [local, ra_event]}] [{send_msg, CPid, {delivery, CTag, DelMsgs}, ?DELIVERY_SEND_MSG_OPTS}]
end, end,
{local, node(CPid)}}. {local, node(CPid)}}.
@ -2078,21 +2112,25 @@ checkout_one(#{system_time := Ts} = Meta, ExpiredMsg0, InitState0, Effects0) ->
%% recurse without consumer on queue %% recurse without consumer on queue
checkout_one(Meta, ExpiredMsg, checkout_one(Meta, ExpiredMsg,
InitState#?MODULE{service_queue = SQ1}, Effects1); InitState#?MODULE{service_queue = SQ1}, Effects1);
#consumer{status = cancelled} -> #consumer{status = S}
checkout_one(Meta, ExpiredMsg, when S =:= cancelled orelse
InitState#?MODULE{service_queue = SQ1}, Effects1); S =:= suspected_down ->
#consumer{status = suspected_down} ->
checkout_one(Meta, ExpiredMsg, checkout_one(Meta, ExpiredMsg,
InitState#?MODULE{service_queue = SQ1}, Effects1); InitState#?MODULE{service_queue = SQ1}, Effects1);
#consumer{checked_out = Checked0, #consumer{checked_out = Checked0,
next_msg_id = Next, next_msg_id = Next,
credit = Credit, credit = Credit,
delivery_count = DelCnt} = Con0 -> delivery_count = DelCnt0,
cfg = Cfg} = Con0 ->
Checked = maps:put(Next, ConsumerMsg, Checked0), Checked = maps:put(Next, ConsumerMsg, Checked0),
DelCnt = case credit_api_v2(Cfg) of
true -> add(DelCnt0, 1);
false -> DelCnt0 + 1
end,
Con = Con0#consumer{checked_out = Checked, Con = Con0#consumer{checked_out = Checked,
next_msg_id = Next + 1, next_msg_id = Next + 1,
credit = Credit - 1, credit = Credit - 1,
delivery_count = DelCnt + 1}, delivery_count = DelCnt},
Size = get_header(size, get_msg_header(ConsumerMsg)), Size = get_header(size, get_msg_header(ConsumerMsg)),
State = update_or_remove_sub( State = update_or_remove_sub(
Meta, ConsumerId, Con, Meta, ConsumerId, Con,
@ -2186,11 +2224,11 @@ update_or_remove_sub(_Meta, ConsumerId,
#?MODULE{consumers = Cons, #?MODULE{consumers = Cons,
service_queue = ServiceQueue} = State) -> service_queue = ServiceQueue} = State) ->
State#?MODULE{consumers = maps:put(ConsumerId, Con, Cons), State#?MODULE{consumers = maps:put(ConsumerId, Con, Cons),
service_queue = uniq_queue_in(ConsumerId, Con, ServiceQueue)}. service_queue = maybe_queue_consumer(ConsumerId, Con, ServiceQueue)}.
uniq_queue_in(Key, #consumer{credit = Credit, maybe_queue_consumer(Key, #consumer{credit = Credit,
status = up, status = up,
cfg = #consumer_cfg{priority = P}}, ServiceQueue) cfg = #consumer_cfg{priority = P}}, ServiceQueue)
when Credit > 0 -> when Credit > 0 ->
% TODO: queue:member could surely be quite expensive, however the practical % TODO: queue:member could surely be quite expensive, however the practical
% number of unique consumers may not be large enough for it to matter % number of unique consumers may not be large enough for it to matter
@ -2200,7 +2238,7 @@ uniq_queue_in(Key, #consumer{credit = Credit,
false -> false ->
priority_queue:in(Key, P, ServiceQueue) priority_queue:in(Key, P, ServiceQueue)
end; end;
uniq_queue_in(_Key, _Consumer, ServiceQueue) -> maybe_queue_consumer(_Key, _Consumer, ServiceQueue) ->
ServiceQueue. ServiceQueue.
update_consumer(Meta, {Tag, Pid} = ConsumerId, ConsumerMeta, update_consumer(Meta, {Tag, Pid} = ConsumerId, ConsumerMeta,
@ -2218,7 +2256,8 @@ update_consumer(Meta, {Tag, Pid} = ConsumerId, ConsumerMeta,
meta = ConsumerMeta, meta = ConsumerMeta,
priority = Priority, priority = Priority,
credit_mode = Mode}, credit_mode = Mode},
credit = Credit} credit = Credit,
delivery_count = initial_delivery_count(ConsumerMeta)}
end, end,
{Consumer, update_or_remove_sub(Meta, ConsumerId, Consumer, State0)}; {Consumer, update_or_remove_sub(Meta, ConsumerId, Consumer, State0)};
update_consumer(Meta, {Tag, Pid} = ConsumerId, ConsumerMeta, update_consumer(Meta, {Tag, Pid} = ConsumerId, ConsumerMeta,
@ -2252,8 +2291,8 @@ update_consumer(Meta, {Tag, Pid} = ConsumerId, ConsumerMeta,
meta = ConsumerMeta, meta = ConsumerMeta,
priority = Priority, priority = Priority,
credit_mode = Mode}, credit_mode = Mode},
credit = Credit}, credit = Credit,
delivery_count = initial_delivery_count(ConsumerMeta)},
{Consumer, {Consumer,
State0#?MODULE{waiting_consumers = State0#?MODULE{waiting_consumers =
Waiting ++ [{ConsumerId, Consumer}]}} Waiting ++ [{ConsumerId, Consumer}]}}
@ -2277,16 +2316,6 @@ credit_mode(#{machine_version := Vsn}, Credit, simple_prefetch)
credit_mode(_, _, Mode) -> credit_mode(_, _, Mode) ->
Mode. Mode.
maybe_queue_consumer(ConsumerId, #consumer{credit = Credit} = Con,
ServiceQueue0) ->
case Credit > 0 of
true ->
% consumer needs service - check if already on service queue
uniq_queue_in(ConsumerId, Con, ServiceQueue0);
false ->
ServiceQueue0
end.
%% creates a dehydrated version of the current state to be cached and %% creates a dehydrated version of the current state to be cached and
%% potentially used to for a snaphot at a later point %% potentially used to for a snaphot at a later point
dehydrate_state(#?MODULE{cfg = #cfg{}, dehydrate_state(#?MODULE{cfg = #cfg{},
@ -2363,8 +2392,8 @@ make_return(ConsumerId, MsgIds) ->
make_discard(ConsumerId, MsgIds) -> make_discard(ConsumerId, MsgIds) ->
#discard{consumer_id = ConsumerId, msg_ids = MsgIds}. #discard{consumer_id = ConsumerId, msg_ids = MsgIds}.
-spec make_credit(consumer_id(), non_neg_integer(), non_neg_integer(), -spec make_credit(consumer_id(), rabbit_queue_type:credit(),
boolean()) -> protocol(). non_neg_integer(), boolean()) -> protocol().
make_credit(ConsumerId, Credit, DeliveryCount, Drain) -> make_credit(ConsumerId, Credit, DeliveryCount, Drain) ->
#credit{consumer_id = ConsumerId, #credit{consumer_id = ConsumerId,
credit = Credit, credit = Credit,
@ -2563,3 +2592,26 @@ get_msg(#enqueue{msg = M}) ->
M; M;
get_msg(#requeue{msg = M}) -> get_msg(#requeue{msg = M}) ->
M. M.
-spec initial_delivery_count(consumer_meta()) ->
rabbit_queue_type:delivery_count().
initial_delivery_count(#{initial_delivery_count := Count}) ->
%% credit API v2
Count;
initial_delivery_count(_) ->
%% credit API v1
0.
-spec credit_api_v2(#consumer_cfg{}) ->
boolean().
credit_api_v2(#consumer_cfg{meta = ConsumerMeta}) ->
maps:is_key(initial_delivery_count, ConsumerMeta).
%% AMQP 1.0 §2.6.7
link_credit_snd(DeliveryCountRcv, LinkCreditRcv, DeliveryCountSnd, ConsumerCfg) ->
C = case credit_api_v2(ConsumerCfg) of
true -> diff(add(DeliveryCountRcv, LinkCreditRcv), DeliveryCountSnd);
false -> DeliveryCountRcv + LinkCreditRcv - DeliveryCountSnd
end,
%% C can be negative when receiver decreases credits while messages are in flight.
max(0, C).

View File

@ -17,6 +17,8 @@
is_list(H) orelse is_list(H) orelse
(is_map(H) andalso is_map_key(size, H))). (is_map(H) andalso is_map_key(size, H))).
-define(DELIVERY_SEND_MSG_OPTS, [local, ra_event]).
-type optimised_tuple(A, B) :: nonempty_improper_list(A, B). -type optimised_tuple(A, B) :: nonempty_improper_list(A, B).
-type option(T) :: undefined | T. -type option(T) :: undefined | T.
@ -56,14 +58,10 @@
-type delivery_msg() :: {msg_id(), {msg_header(), raw_msg()}}. -type delivery_msg() :: {msg_id(), {msg_header(), raw_msg()}}.
%% A tuple consisting of the message id, and the headered message. %% A tuple consisting of the message id, and the headered message.
-type consumer_tag() :: binary(). -type delivery() :: {delivery, rabbit_types:ctag(), [delivery_msg()]}.
%% An arbitrary binary tag used to distinguish between different consumers
%% set up by the same process. See: {@link rabbit_fifo_client:checkout/3.}
-type delivery() :: {delivery, consumer_tag(), [delivery_msg()]}.
%% Represents the delivery of one or more rabbit_fifo messages. %% Represents the delivery of one or more rabbit_fifo messages.
-type consumer_id() :: {consumer_tag(), pid()}. -type consumer_id() :: {rabbit_types:ctag(), pid()}.
%% The entity that receives messages. Uniquely identifies a consumer. %% The entity that receives messages. Uniquely identifies a consumer.
-type credit_mode() :: credited | -type credit_mode() :: credited |
@ -81,7 +79,10 @@
-type consumer_meta() :: #{ack => boolean(), -type consumer_meta() :: #{ack => boolean(),
username => binary(), username => binary(),
prefetch => non_neg_integer(), prefetch => non_neg_integer(),
args => list()}. args => list(),
%% set if and only if credit API v2 is in use
initial_delivery_count => rabbit_queue_type:delivery_count()
}.
%% static meta data associated with a consumer %% static meta data associated with a consumer
-type applied_mfa() :: {module(), atom(), list()}. -type applied_mfa() :: {module(), atom(), list()}.
@ -101,7 +102,7 @@
-record(consumer_cfg, -record(consumer_cfg,
{meta = #{} :: consumer_meta(), {meta = #{} :: consumer_meta(),
pid :: pid(), pid :: pid(),
tag :: consumer_tag(), tag :: rabbit_types:ctag(),
%% the mode of how credit is incremented %% the mode of how credit is incremented
%% simple_prefetch: credit is re-filled as deliveries are settled %% simple_prefetch: credit is re-filled as deliveries are settled
%% or returned. %% or returned.
@ -119,9 +120,8 @@
%% max number of messages that can be sent %% max number of messages that can be sent
%% decremented for each delivery %% decremented for each delivery
credit = 0 : non_neg_integer(), credit = 0 : non_neg_integer(),
%% total number of checked out messages - ever %% AMQP 1.0 §2.6.7
%% incremented for each delivery delivery_count :: rabbit_queue_type:delivery_count()
delivery_count = 0 :: non_neg_integer()
}). }).
-type consumer() :: #consumer{}. -type consumer() :: #consumer{}.
@ -200,7 +200,7 @@
dlx = rabbit_fifo_dlx:init() :: rabbit_fifo_dlx:state(), dlx = rabbit_fifo_dlx:init() :: rabbit_fifo_dlx:state(),
msg_bytes_enqueue = 0 :: non_neg_integer(), msg_bytes_enqueue = 0 :: non_neg_integer(),
msg_bytes_checkout = 0 :: non_neg_integer(), msg_bytes_checkout = 0 :: non_neg_integer(),
%% waiting consumers, one is picked active consumer is cancelled or dies %% one is picked if active consumer is cancelled or dies
%% used only when single active consumer is on %% used only when single active consumer is on
waiting_consumers = [] :: [{consumer_id(), consumer()}], waiting_consumers = [] :: [{consumer_id(), consumer()}],
last_active :: option(non_neg_integer()), last_active :: option(non_neg_integer()),

View File

@ -22,7 +22,8 @@
settle/3, settle/3,
return/3, return/3,
discard/3, discard/3,
credit/4, credit_v1/4,
credit/6,
handle_ra_event/4, handle_ra_event/4,
untracked_enqueue/2, untracked_enqueue/2,
purge/1, purge/1,
@ -39,15 +40,16 @@
-define(COMMAND_TIMEOUT, 30000). -define(COMMAND_TIMEOUT, 30000).
-type seq() :: non_neg_integer(). -type seq() :: non_neg_integer().
-type action() :: {send_credit_reply, Available :: non_neg_integer()} |
{send_drained, CTagCredit ::
{rabbit_fifo:consumer_tag(), non_neg_integer()}} |
rabbit_queue_type:action().
-type actions() :: [action()].
-record(consumer, {last_msg_id :: seq() | -1 | undefined, -record(consumer, {last_msg_id :: seq() | -1 | undefined,
ack = false :: boolean(), ack = false :: boolean(),
delivery_count = 0 :: non_neg_integer()}). %% 'echo' field from latest FLOW, see AMQP 1.0 §2.7.4
%% Quorum queue server will always echo back to us,
%% but we only emit a credit_reply if Echo=true
echo :: boolean(),
%% Remove this field when feature flag credit_api_v2 becomes required.
delivery_count :: {credit_api_v1, rabbit_queue_type:delivery_count()} | credit_api_v2
}).
-record(cfg, {servers = [] :: [ra:server_id()], -record(cfg, {servers = [] :: [ra:server_id()],
soft_limit = ?SOFT_LIMIT :: non_neg_integer(), soft_limit = ?SOFT_LIMIT :: non_neg_integer(),
@ -65,18 +67,14 @@
{[seq()], [seq()], [seq()]}}, {[seq()], [seq()], [seq()]}},
pending = #{} :: #{seq() => pending = #{} :: #{seq() =>
{term(), rabbit_fifo:command()}}, {term(), rabbit_fifo:command()}},
consumer_deliveries = #{} :: #{rabbit_fifo:consumer_tag() => consumer_deliveries = #{} :: #{rabbit_types:ctag() =>
#consumer{}}, #consumer{}},
timer_state :: term() timer_state :: term()
}). }).
-opaque state() :: #state{}. -opaque state() :: #state{}.
-export_type([ -export_type([state/0]).
state/0,
actions/0
]).
%% @doc Create the initial state for a new rabbit_fifo sessions. A state is needed %% @doc Create the initial state for a new rabbit_fifo sessions. A state is needed
%% to interact with a rabbit_fifo queue using @module. %% to interact with a rabbit_fifo queue using @module.
@ -111,7 +109,7 @@ init(Servers, SoftLimit) ->
%% by the {@link handle_ra_event/2. handle_ra_event/2} function. %% by the {@link handle_ra_event/2. handle_ra_event/2} function.
-spec enqueue(rabbit_amqqueue:name(), Correlation :: term(), -spec enqueue(rabbit_amqqueue:name(), Correlation :: term(),
Msg :: term(), State :: state()) -> Msg :: term(), State :: state()) ->
{ok, state(), actions()} | {reject_publish, state()}. {ok, state(), rabbit_queue_type:actions()} | {reject_publish, state()}.
enqueue(QName, Correlation, Msg, enqueue(QName, Correlation, Msg,
#state{queue_status = undefined, #state{queue_status = undefined,
next_enqueue_seq = 1, next_enqueue_seq = 1,
@ -177,7 +175,7 @@ enqueue(QName, Correlation, Msg,
%% by the {@link handle_ra_event/2. handle_ra_event/2} function. %% by the {@link handle_ra_event/2. handle_ra_event/2} function.
%% %%
-spec enqueue(rabbit_amqqueue:name(), Msg :: term(), State :: state()) -> -spec enqueue(rabbit_amqqueue:name(), Msg :: term(), State :: state()) ->
{ok, state(), actions()} | {reject_publish, state()}. {ok, state(), rabbit_queue_type:actions()} | {reject_publish, state()}.
enqueue(QName, Msg, State) -> enqueue(QName, Msg, State) ->
enqueue(QName, undefined, Msg, State). enqueue(QName, undefined, Msg, State).
@ -193,7 +191,7 @@ enqueue(QName, Msg, State) ->
%% @param State The {@module} state. %% @param State The {@module} state.
%% %%
%% @returns `{ok, IdMsg, State}' or `{error | timeout, term()}' %% @returns `{ok, IdMsg, State}' or `{error | timeout, term()}'
-spec dequeue(rabbit_amqqueue:name(), rabbit_fifo:consumer_tag(), -spec dequeue(rabbit_amqqueue:name(), rabbit_types:ctag(),
Settlement :: settled | unsettled, state()) -> Settlement :: settled | unsettled, state()) ->
{ok, non_neg_integer(), term(), non_neg_integer()} {ok, non_neg_integer(), term(), non_neg_integer()}
| {empty, state()} | {error | timeout, term()}. | {empty, state()} | {error | timeout, term()}.
@ -239,7 +237,7 @@ add_delivery_count_header(Msg, Count) ->
%% @param MsgIds the message ids received with the {@link rabbit_fifo:delivery/0.} %% @param MsgIds the message ids received with the {@link rabbit_fifo:delivery/0.}
%% @param State the {@module} state %% @param State the {@module} state
%% %%
-spec settle(rabbit_fifo:consumer_tag(), [rabbit_fifo:msg_id()], state()) -> -spec settle(rabbit_types:ctag(), [rabbit_fifo:msg_id()], state()) ->
{state(), list()}. {state(), list()}.
settle(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) -> settle(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) ->
ServerId = pick_server(State0), ServerId = pick_server(State0),
@ -267,7 +265,7 @@ settle(ConsumerTag, [_|_] = MsgIds,
%% @returns %% @returns
%% `{State, list()}' if the command was successfully sent. %% `{State, list()}' if the command was successfully sent.
%% %%
-spec return(rabbit_fifo:consumer_tag(), [rabbit_fifo:msg_id()], state()) -> -spec return(rabbit_types:ctag(), [rabbit_fifo:msg_id()], state()) ->
{state(), list()}. {state(), list()}.
return(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) -> return(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) ->
ServerId = pick_server(State0), ServerId = pick_server(State0),
@ -292,7 +290,7 @@ return(ConsumerTag, [_|_] = MsgIds,
%% @param MsgIds the message ids to discard %% @param MsgIds the message ids to discard
%% from {@link rabbit_fifo:delivery/0.} %% from {@link rabbit_fifo:delivery/0.}
%% @param State the {@module} state %% @param State the {@module} state
-spec discard(rabbit_fifo:consumer_tag(), [rabbit_fifo:msg_id()], state()) -> -spec discard(rabbit_types:ctag(), [rabbit_fifo:msg_id()], state()) ->
{state(), list()}. {state(), list()}.
discard(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) -> discard(ConsumerTag, [_|_] = MsgIds, #state{slow = false} = State0) ->
ServerId = pick_server(State0), ServerId = pick_server(State0),
@ -325,7 +323,7 @@ discard(ConsumerTag, [_|_] = MsgIds,
%% @param State The {@module} state. %% @param State The {@module} state.
%% %%
%% @returns `{ok, State}' or `{error | timeout, term()}' %% @returns `{ok, State}' or `{error | timeout, term()}'
-spec checkout(rabbit_fifo:consumer_tag(), -spec checkout(rabbit_types:ctag(),
NumUnsettled :: non_neg_integer(), NumUnsettled :: non_neg_integer(),
CreditMode :: rabbit_fifo:credit_mode(), CreditMode :: rabbit_fifo:credit_mode(),
Meta :: rabbit_fifo:consumer_meta(), Meta :: rabbit_fifo:consumer_meta(),
@ -362,10 +360,18 @@ checkout(ConsumerTag, NumUnsettled, CreditMode, Meta,
NextMsgId - 1 NextMsgId - 1
end end
end, end,
DeliveryCount = case maps:is_key(initial_delivery_count, Meta) of
true -> credit_api_v2;
false -> {credit_api_v1, 0}
end,
SDels = maps:update_with( SDels = maps:update_with(
ConsumerTag, fun (C) -> C#consumer{ack = Ack} end, ConsumerTag,
fun (C) -> C#consumer{ack = Ack} end,
#consumer{last_msg_id = LastMsgId, #consumer{last_msg_id = LastMsgId,
ack = Ack}, CDels0), ack = Ack,
echo = false,
delivery_count = DeliveryCount},
CDels0),
{ok, State0#state{leader = Leader, {ok, State0#state{leader = Leader,
consumer_deliveries = SDels}}; consumer_deliveries = SDels}};
Err -> Err ->
@ -385,29 +391,45 @@ query_single_active_consumer(#state{leader = Leader}) ->
Err Err
end. end.
-spec credit_v1(rabbit_types:ctag(),
Credit :: non_neg_integer(),
Drain :: boolean(),
state()) ->
{state(), rabbit_queue_type:actions()}.
credit_v1(ConsumerTag, Credit, Drain,
#state{consumer_deliveries = CDels} = State0) ->
ConsumerId = consumer_id(ConsumerTag),
#consumer{delivery_count = {credit_api_v1, Count}} = maps:get(ConsumerTag, CDels),
ServerId = pick_server(State0),
Cmd = rabbit_fifo:make_credit(ConsumerId, Credit, Count, Drain),
{send_command(ServerId, undefined, Cmd, normal, State0), []}.
%% @doc Provide credit to the queue %% @doc Provide credit to the queue
%% %%
%% This only has an effect if the consumer uses credit mode: credited %% This only has an effect if the consumer uses credit mode: credited
%% @param ConsumerTag a unique tag to identify this particular consumer. %% @param ConsumerTag a unique tag to identify this particular consumer.
%% @param Credit the amount of credit to provide to theq queue %% @param Credit the amount of credit to provide to the queue
%% @param Drain tells the queue to use up any credit that cannot be immediately %% @param Drain tells the queue to use up any credit that cannot be immediately
%% fulfilled. (i.e. there are not enough messages on queue to use up all the %% fulfilled. (i.e. there are not enough messages on queue to use up all the
%% provided credit). %% provided credit).
-spec credit(rabbit_fifo:consumer_tag(), %% @param Reply true if the queue client requests a credit_reply queue action
Credit :: non_neg_integer(), -spec credit(rabbit_types:ctag(),
rabbit_queue_type:delivery_count(),
rabbit_queue_type:credit(),
Drain :: boolean(), Drain :: boolean(),
Echo :: boolean(),
state()) -> state()) ->
{state(), actions()}. {state(), rabbit_queue_type:actions()}.
credit(ConsumerTag, Credit, Drain, credit(ConsumerTag, DeliveryCount, Credit, Drain, Echo,
#state{consumer_deliveries = CDels} = State0) -> #state{consumer_deliveries = CDels0} = State0) ->
ConsumerId = consumer_id(ConsumerTag), ConsumerId = consumer_id(ConsumerTag),
%% the last received msgid provides us with the delivery count if we
%% add one as it is 0 indexed
C = maps:get(ConsumerTag, CDels, #consumer{last_msg_id = -1}),
ServerId = pick_server(State0), ServerId = pick_server(State0),
Cmd = rabbit_fifo:make_credit(ConsumerId, Credit, Cmd = rabbit_fifo:make_credit(ConsumerId, Credit, DeliveryCount, Drain),
C#consumer.last_msg_id + 1, Drain), CDels = maps:update_with(ConsumerTag,
{send_command(ServerId, undefined, Cmd, normal, State0), []}. fun(C) -> C#consumer{echo = Echo} end,
CDels0),
State = State0#state{consumer_deliveries = CDels},
{send_command(ServerId, undefined, Cmd, normal, State), []}.
%% @doc Cancels a checkout with the rabbit_fifo queue for the consumer tag %% @doc Cancels a checkout with the rabbit_fifo queue for the consumer tag
%% %%
@ -418,7 +440,7 @@ credit(ConsumerTag, Credit, Drain,
%% @param State The {@module} state. %% @param State The {@module} state.
%% %%
%% @returns `{ok, State}' or `{error | timeout, term()}' %% @returns `{ok, State}' or `{error | timeout, term()}'
-spec cancel_checkout(rabbit_fifo:consumer_tag(), state()) -> -spec cancel_checkout(rabbit_types:ctag(), state()) ->
{ok, state()} | {error | timeout, term()}. {ok, state()} | {error | timeout, term()}.
cancel_checkout(ConsumerTag, #state{consumer_deliveries = CDels} = State0) -> cancel_checkout(ConsumerTag, #state{consumer_deliveries = CDels} = State0) ->
Servers = sorted_servers(State0), Servers = sorted_servers(State0),
@ -521,25 +543,25 @@ update_machine_state(Server, Conf) ->
%% with them.</li> %% with them.</li>
-spec handle_ra_event(rabbit_amqqueue:name(), ra:server_id(), -spec handle_ra_event(rabbit_amqqueue:name(), ra:server_id(),
ra_server_proc:ra_event_body(), state()) -> ra_server_proc:ra_event_body(), state()) ->
{internal, Correlators :: [term()], actions(), state()} | {internal, Correlators :: [term()], rabbit_queue_type:actions(), state()} |
{rabbit_fifo:client_msg(), state()} | {eol, actions()}. {rabbit_fifo:client_msg(), state()} | {eol, rabbit_queue_type:actions()}.
handle_ra_event(QName, From, {applied, Seqs}, handle_ra_event(QName, From, {applied, Seqs},
#state{cfg = #cfg{soft_limit = SftLmt}} = State0) -> #state{cfg = #cfg{soft_limit = SftLmt}} = State0) ->
{Corrs, Actions0, State1} = lists:foldl(fun seq_applied/2, {Corrs, ActionsRev, State1} = lists:foldl(fun seq_applied/2,
{[], [], State0#state{leader = From}}, {[], [], State0#state{leader = From}},
Seqs), Seqs),
Actions0 = lists:reverse(ActionsRev),
Actions = case Corrs of Actions = case Corrs of
[] -> [] ->
lists:reverse(Actions0); Actions0;
_ -> _ ->
%%TODO consider using lists:foldr/3 above because %%TODO consider using lists:foldr/3 above because
%% Corrs is returned in the wrong order here. %% Corrs is returned in the wrong order here.
%% The wrong order does not matter much because the channel sorts the %% The wrong order does not matter much because the channel sorts the
%% sequence numbers before confirming to the client. But rabbit_fifo_client %% sequence numbers before confirming to the client. But rabbit_fifo_client
%% is sequence numer agnostic: it handles any correlation terms. %% is sequence numer agnostic: it handles any correlation terms.
[{settled, QName, Corrs} [{settled, QName, Corrs} | Actions0]
| lists:reverse(Actions0)]
end, end,
case maps:size(State1#state.pending) < SftLmt of case maps:size(State1#state.pending) < SftLmt of
true when State1#state.slow == true -> true when State1#state.slow == true ->
@ -572,6 +594,21 @@ handle_ra_event(QName, From, {applied, Seqs},
end; end;
handle_ra_event(QName, From, {machine, {delivery, _ConsumerTag, _} = Del}, State0) -> handle_ra_event(QName, From, {machine, {delivery, _ConsumerTag, _} = Del}, State0) ->
handle_delivery(QName, From, Del, State0); handle_delivery(QName, From, Del, State0);
handle_ra_event(_QName, _From,
{machine, {credit_reply_v1, _CTag, _Credit, _Available, _Drain = false} = Action},
State) ->
{ok, State, [Action]};
handle_ra_event(_QName, _From,
{machine, {credit_reply, CTag, _DeliveryCount, _Credit, _Available, Drain} = Action},
#state{consumer_deliveries = CDels} = State) ->
Actions = case CDels of
#{CTag := #consumer{echo = Echo}}
when Echo orelse Drain ->
[Action];
_ ->
[]
end,
{ok, State, Actions};
handle_ra_event(_QName, _, {machine, {queue_status, Status}}, handle_ra_event(_QName, _, {machine, {queue_status, Status}},
#state{} = State) -> #state{} = State) ->
%% just set the queue status %% just set the queue status
@ -667,14 +704,12 @@ maybe_add_action({multi, Actions}, Acc0, State0) ->
lists:foldl(fun (Act, {Acc, State}) -> lists:foldl(fun (Act, {Acc, State}) ->
maybe_add_action(Act, Acc, State) maybe_add_action(Act, Acc, State)
end, {Acc0, State0}, Actions); end, {Acc0, State0}, Actions);
maybe_add_action({send_drained, {Tag, Credit}} = Action, Acc, maybe_add_action({send_drained, {Tag, Credit}}, Acc, State0) ->
#state{consumer_deliveries = CDels} = State) -> %% This function clause should be deleted when
%% add credit to consumer delivery_count %% feature flag credit_api_v2 becomes required.
C = maps:get(Tag, CDels), State = add_delivery_count(Credit, Tag, State0),
{[Action | Acc], Action = {credit_reply_v1, Tag, Credit, _Avail = 0, _Drain = true},
State#state{consumer_deliveries = {[Action | Acc], State};
update_consumer(Tag, C#consumer.last_msg_id,
Credit, C, CDels)}};
maybe_add_action(Action, Acc, State) -> maybe_add_action(Action, Acc, State) ->
%% anything else is assumed to be an action %% anything else is assumed to be an action
{[Action | Acc], State}. {[Action | Acc], State}.
@ -785,13 +820,20 @@ transform_msgs(QName, QRef, Msgs) ->
{QName, QRef, MsgId, Redelivered, Msg} {QName, QRef, MsgId, Redelivered, Msg}
end, Msgs). end, Msgs).
update_consumer(Tag, LastId, DelCntIncr, update_consumer(Tag, LastId, DelCntIncr, Consumer, Consumers) ->
#consumer{delivery_count = D} = C, Consumers) -> D = case Consumer#consumer.delivery_count of
maps:put(Tag, credit_api_v2 -> credit_api_v2;
C#consumer{last_msg_id = LastId, {credit_api_v1, Count} -> {credit_api_v1, Count + DelCntIncr}
delivery_count = D + DelCntIncr}, end,
Consumers). maps:update(Tag,
Consumer#consumer{last_msg_id = LastId,
delivery_count = D},
Consumers).
add_delivery_count(DelCntIncr, Tag, #state{consumer_deliveries = CDels0} = State) ->
Con = #consumer{last_msg_id = LastMsgId} = maps:get(Tag, CDels0),
CDels = update_consumer(Tag, LastMsgId, DelCntIncr, Con, CDels0),
State#state{consumer_deliveries = CDels}.
get_missing_deliveries(State, From, To, ConsumerTag) -> get_missing_deliveries(State, From, To, ConsumerTag) ->
%% find local server %% find local server

View File

@ -93,7 +93,6 @@
-define(MESSAGES_GET_EMPTY, 6). -define(MESSAGES_GET_EMPTY, 6).
-define(MESSAGES_REDELIVERED, 7). -define(MESSAGES_REDELIVERED, 7).
-define(MESSAGES_ACKNOWLEDGED, 8). -define(MESSAGES_ACKNOWLEDGED, 8).
%% Note: ?NUM_PROTOCOL_QUEUE_TYPE_COUNTERS needs to be up-to-date. See include/rabbit_global_counters.hrl
-define(PROTOCOL_QUEUE_TYPE_COUNTERS, -define(PROTOCOL_QUEUE_TYPE_COUNTERS,
[ [
{ {
@ -131,13 +130,15 @@
]). ]).
boot_step() -> boot_step() ->
%% Protocol counters [begin
init([{protocol, amqp091}]), %% Protocol counters
init([{protocol, Proto}]),
%% Protocol & Queue Type counters %% Protocol & Queue Type counters
init([{protocol, amqp091}, {queue_type, rabbit_classic_queue}]), init([{protocol, Proto}, {queue_type, rabbit_classic_queue}]),
init([{protocol, amqp091}, {queue_type, rabbit_quorum_queue}]), init([{protocol, Proto}, {queue_type, rabbit_quorum_queue}]),
init([{protocol, amqp091}, {queue_type, rabbit_stream_queue}]), init([{protocol, Proto}, {queue_type, rabbit_stream_queue}])
end || Proto <- [amqp091, amqp10]],
%% Dead Letter counters %% Dead Letter counters
%% %%

View File

@ -62,12 +62,11 @@
%% that's what the limit_prefetch/3, unlimit_prefetch/1, %% that's what the limit_prefetch/3, unlimit_prefetch/1,
%% get_prefetch_limit/1 API functions are about. They also tell the %% get_prefetch_limit/1 API functions are about. They also tell the
%% limiter queue state (via the queue) about consumer credit %% limiter queue state (via the queue) about consumer credit
%% changes and message acknowledgement - that's what credit/5 and %% changes and message acknowledgement - that's what credit/4 and
%% ack_from_queue/3 are for. %% ack_from_queue/3 are for.
%% %%
%% 2. Queues also tell the limiter queue state about the queue %% 2. Queues also tell the limiter queue state about consumers leaving
%% becoming empty (via drained/1) and consumers leaving (via %% (via forget_consumer/2).
%% forget_consumer/2).
%% %%
%% 3. Queues register with the limiter - this happens as part of %% 3. Queues register with the limiter - this happens as part of
%% activate/1. %% activate/1.
@ -120,8 +119,8 @@
get_prefetch_limit/1, ack/2, pid/1]). get_prefetch_limit/1, ack/2, pid/1]).
%% queue API %% queue API
-export([client/1, activate/1, can_send/3, resume/1, deactivate/1, -export([client/1, activate/1, can_send/3, resume/1, deactivate/1,
is_suspended/1, is_consumer_blocked/2, credit/5, ack_from_queue/3, is_suspended/1, is_consumer_blocked/2, credit/4, ack_from_queue/3,
drained/1, forget_consumer/2]). forget_consumer/2]).
%% callbacks %% callbacks
-export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2,
handle_info/2, prioritise_call/4]). handle_info/2, prioritise_call/4]).
@ -136,7 +135,7 @@
-type qstate() :: #qstate{pid :: pid() | none, -type qstate() :: #qstate{pid :: pid() | none,
state :: 'dormant' | 'active' | 'suspended'}. state :: 'dormant' | 'active' | 'suspended'}.
-type credit_mode() :: 'manual' | 'drain' | 'auto'. -type credit_mode() :: auto | manual.
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
@ -259,18 +258,11 @@ is_consumer_blocked(#qstate{credits = Credits}, CTag) ->
{value, #credit{}} -> true {value, #credit{}} -> true
end. end.
-spec credit -spec credit(qstate(), rabbit_types:ctag(), non_neg_integer(), credit_mode()) ->
(qstate(), rabbit_types:ctag(), non_neg_integer(), credit_mode(), qstate().
boolean()) -> credit(Limiter = #qstate{credits = Credits}, CTag, Crd, Mode) ->
{boolean(), qstate()}. Cr = #credit{credit = Crd, mode = Mode},
Limiter#qstate{credits = enter_credit(CTag, Cr, Credits)}.
credit(Limiter = #qstate{credits = Credits}, CTag, Crd, Mode, IsEmpty) ->
{Res, Cr} =
case IsEmpty andalso Mode =:= drain of
true -> {true, #credit{credit = 0, mode = manual}};
false -> {false, #credit{credit = Crd, mode = Mode}}
end,
{Res, Limiter#qstate{credits = enter_credit(CTag, Cr, Credits)}}.
-spec ack_from_queue(qstate(), rabbit_types:ctag(), non_neg_integer()) -> -spec ack_from_queue(qstate(), rabbit_types:ctag(), non_neg_integer()) ->
{boolean(), qstate()}. {boolean(), qstate()}.
@ -286,20 +278,6 @@ ack_from_queue(Limiter = #qstate{credits = Credits}, CTag, Credit) ->
end, end,
{Unblocked, Limiter#qstate{credits = Credits1}}. {Unblocked, Limiter#qstate{credits = Credits1}}.
-spec drained(qstate()) ->
{[{rabbit_types:ctag(), non_neg_integer()}], qstate()}.
drained(Limiter = #qstate{credits = Credits}) ->
Drain = fun(C) -> C#credit{credit = 0, mode = manual} end,
{CTagCredits, Credits2} =
rabbit_misc:gb_trees_fold(
fun (CTag, C = #credit{credit = Crd, mode = drain}, {Acc, Creds0}) ->
{[{CTag, Crd} | Acc], update_credit(CTag, Drain(C), Creds0)};
(_CTag, #credit{credit = _Crd, mode = _Mode}, {Acc, Creds0}) ->
{Acc, Creds0}
end, {[], Credits}, Credits),
{CTagCredits, Limiter#qstate{credits = Credits2}}.
-spec forget_consumer(qstate(), rabbit_types:ctag()) -> qstate(). -spec forget_consumer(qstate(), rabbit_types:ctag()) -> qstate().
forget_consumer(Limiter = #qstate{credits = Credits}, CTag) -> forget_consumer(Limiter = #qstate{credits = Credits}, CTag) ->
@ -309,13 +287,6 @@ forget_consumer(Limiter = #qstate{credits = Credits}, CTag) ->
%% Queue-local code %% Queue-local code
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
%% We want to do all the AMQP 1.0-ish link level credit calculations
%% in the queue (to do them elsewhere introduces a ton of
%% races). However, it's a big chunk of code that is conceptually very
%% linked to the limiter concept. So we get the queue to hold a bit of
%% state for us (#qstate.credits), and maintain a fiction that the
%% limiter is making the decisions...
decrement_credit(CTag, Credits) -> decrement_credit(CTag, Credits) ->
case gb_trees:lookup(CTag, Credits) of case gb_trees:lookup(CTag, Credits) of
{value, C = #credit{credit = Credit}} -> {value, C = #credit{credit = Credit}} ->
@ -325,16 +296,10 @@ decrement_credit(CTag, Credits) ->
end. end.
enter_credit(CTag, C, Credits) -> enter_credit(CTag, C, Credits) ->
gb_trees:enter(CTag, ensure_credit_invariant(C), Credits). gb_trees:enter(CTag, C, Credits).
update_credit(CTag, C, Credits) -> update_credit(CTag, C, Credits) ->
gb_trees:update(CTag, ensure_credit_invariant(C), Credits). gb_trees:update(CTag, C, Credits).
ensure_credit_invariant(C = #credit{credit = 0, mode = drain}) ->
%% Using up all credit implies no need to send a 'drained' event
C#credit{mode = manual};
ensure_credit_invariant(C) ->
C.
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks

View File

@ -49,9 +49,7 @@
-export([ -export([
local_connections/0, local_connections/0,
local_non_amqp_connections/0, local_non_amqp_connections/0
%% prefer local_connections/0
connections_local/0
]). ]).
-include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("rabbit_common/include/rabbit.hrl").
@ -448,19 +446,15 @@ register_connection(Pid) -> pg_local:join(rabbit_connections, Pid).
unregister_connection(Pid) -> pg_local:leave(rabbit_connections, Pid). unregister_connection(Pid) -> pg_local:leave(rabbit_connections, Pid).
-spec connections() -> [rabbit_types:connection()]. -spec connections() -> [rabbit_types:connection()].
connections() -> connections() ->
Nodes = rabbit_nodes:list_running(), Nodes = rabbit_nodes:list_running(),
rabbit_misc:append_rpc_all_nodes(Nodes, rabbit_networking, connections_local, [], ?RPC_TIMEOUT). rabbit_misc:append_rpc_all_nodes(Nodes, rabbit_networking, local_connections, [], ?RPC_TIMEOUT).
-spec local_connections() -> [rabbit_types:connection()]. -spec local_connections() -> [rabbit_types:connection()].
%% @doc Returns pids of AMQP 0-9-1 and AMQP 1.0 connections local to this node.
local_connections() -> local_connections() ->
connections_local(). Amqp091Pids = pg_local:get_members(rabbit_connections),
Amqp10Pids = rabbit_amqp1_0:list_local(),
-spec connections_local() -> [rabbit_types:connection()]. Amqp10Pids ++ Amqp091Pids.
%% @deprecated Prefer {@link local_connections}
connections_local() -> pg_local:get_members(rabbit_connections).
-spec register_non_amqp_connection(pid()) -> ok. -spec register_non_amqp_connection(pid()) -> ok.
@ -510,21 +504,16 @@ emit_connection_info_all(Nodes, Items, Ref, AggregatorPid) ->
emit_connection_info_local(Items, Ref, AggregatorPid) -> emit_connection_info_local(Items, Ref, AggregatorPid) ->
rabbit_control_misc:emitting_map_with_exit_handler( rabbit_control_misc:emitting_map_with_exit_handler(
AggregatorPid, Ref, fun(Q) -> connection_info(Q, Items) end, AggregatorPid, Ref, fun(Q) -> connection_info(Q, Items) end,
connections_local()). local_connections()).
-spec close_connection(pid(), string()) -> 'ok'. -spec close_connection(pid(), string()) -> 'ok'.
close_connection(Pid, Explanation) -> close_connection(Pid, Explanation) ->
case lists:member(Pid, connections()) of rabbit_log:info("Closing connection ~tp because ~tp",
true -> [Pid, Explanation]),
Res = rabbit_reader:shutdown(Pid, Explanation), try rabbit_reader:shutdown(Pid, Explanation)
rabbit_log:info("Closing connection ~tp because ~tp", [Pid, Explanation]), catch exit:{Reason, _Location} ->
Res; rabbit_log:warning("Could not close connection ~tp (reason: ~tp): ~p",
false -> [Pid, Explanation, Reason])
rabbit_log:warning("Asked to close connection ~tp (reason: ~tp) "
"but no running cluster node reported it as an active connection. Was it already closed? ",
[Pid, Explanation]),
ok
end. end.
-spec close_connections([pid()], string()) -> 'ok'. -spec close_connections([pid()], string()) -> 'ok'.

View File

@ -8,12 +8,13 @@
-module(rabbit_queue_consumers). -module(rabbit_queue_consumers).
-export([new/0, max_active_priority/1, inactive/1, all/1, all/3, count/0, -export([new/0, max_active_priority/1, inactive/1, all/1, all/3, count/0,
unacknowledged_message_count/0, add/11, remove/3, erase_ch/2, unacknowledged_message_count/0, add/9, remove/3, erase_ch/2,
send_drained/1, deliver/5, record_ack/3, subtract_acks/3, deliver/5, record_ack/3, subtract_acks/3,
possibly_unblock/3, possibly_unblock/3,
resume_fun/0, notify_sent_fun/1, activate_limit_fun/0, resume_fun/0, notify_sent_fun/1, activate_limit_fun/0,
credit/7, utilisation/1, capacity/1, is_same/3, get_consumer/1, get/3, drained/3, process_credit/5, get_link_state/2,
consumer_tag/1, get_infos/1]). utilisation/1, capacity/1, is_same/3, get_consumer/1, get/3,
consumer_tag/1, get_infos/1, parse_prefetch_count/1]).
-export([deactivate_limit_fun/0]). -export([deactivate_limit_fun/0]).
@ -30,7 +31,13 @@
-record(consumer, {tag, ack_required, prefetch, args, user}). -record(consumer, {tag, ack_required, prefetch, args, user}).
%% AMQP 1.0 link flow control state, see §2.6.7
%% Delete atom credit_api_v1 when feature flag credit_api_v2 becomes required.
-record(link_state, {delivery_count :: rabbit_queue_type:delivery_count() | credit_api_v1,
credit :: rabbit_queue_type:credit()}).
%% These are held in our process dictionary %% These are held in our process dictionary
%% channel record
-record(cr, {ch_pid, -record(cr, {ch_pid,
monitor_ref, monitor_ref,
acktags, acktags,
@ -41,7 +48,9 @@
%% The limiter itself %% The limiter itself
limiter, limiter,
%% Internal flow control for queue -> writer %% Internal flow control for queue -> writer
unsent_message_count}). unsent_message_count,
link_states :: #{rabbit_types:ctag() => #link_state{}}
}).
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
@ -120,33 +129,50 @@ count() -> lists:sum([Count || #cr{consumer_count = Count} <- all_ch_record()]).
unacknowledged_message_count() -> unacknowledged_message_count() ->
lists:sum([?QUEUE:len(C#cr.acktags) || C <- all_ch_record()]). lists:sum([?QUEUE:len(C#cr.acktags) || C <- all_ch_record()]).
-spec add(rabbit_amqqueue:name(), ch(), rabbit_types:ctag(), boolean(), pid() | none, boolean(), -spec add(ch(), rabbit_types:ctag(), boolean(), pid() | none, boolean(),
non_neg_integer(), rabbit_framing:amqp_table(), boolean(), %% credit API v1
rabbit_types:username(), state()) SimplePrefetch :: non_neg_integer() |
-> state(). %% credit API v2
{simple_prefetch, non_neg_integer()} | {credited, rabbit_queue_type:delivery_count()},
rabbit_framing:amqp_table(),
rabbit_types:username(), state()) ->
state().
add(QName, ChPid, CTag, NoAck, LimiterPid, LimiterActive, Prefetch, Args, IsEmpty, add(ChPid, CTag, NoAck, LimiterPid, LimiterActive,
ModeOrPrefetch, Args,
Username, State = #state{consumers = Consumers, Username, State = #state{consumers = Consumers,
use = CUInfo}) -> use = CUInfo}) ->
C = #cr{consumer_count = Count, C0 = #cr{consumer_count = Count,
limiter = Limiter} = ch_record(ChPid, LimiterPid), limiter = Limiter,
link_states = LinkStates} = ch_record(ChPid, LimiterPid),
Limiter1 = case LimiterActive of Limiter1 = case LimiterActive of
true -> rabbit_limiter:activate(Limiter); true -> rabbit_limiter:activate(Limiter);
false -> Limiter false -> Limiter
end, end,
C1 = C#cr{consumer_count = Count + 1, limiter = Limiter1}, C1 = C0#cr{consumer_count = Count + 1,
update_ch_record( limiter = Limiter1},
case parse_credit_args(Prefetch, Args) of C = case parse_credit_mode(ModeOrPrefetch, Args) of
{0, auto} -> C1; {0, auto} ->
{_Credit, auto} when NoAck -> C1; C1;
{Credit, Mode} -> credit_and_drain(QName, {Credit, auto = Mode} ->
C1, CTag, Credit, Mode, IsEmpty) case NoAck of
end), true ->
C1;
false ->
Limiter2 = rabbit_limiter:credit(Limiter1, CTag, Credit, Mode),
C1#cr{limiter = Limiter2}
end;
{InitialDeliveryCount, manual} ->
C1#cr{link_states = LinkStates#{CTag => #link_state{
credit = 0,
delivery_count = InitialDeliveryCount}}}
end,
update_ch_record(C),
Consumer = #consumer{tag = CTag, Consumer = #consumer{tag = CTag,
ack_required = not NoAck, ack_required = not NoAck,
prefetch = Prefetch, prefetch = parse_prefetch_count(ModeOrPrefetch),
args = Args, args = Args,
user = Username}, user = Username},
State#state{consumers = add_consumer({ChPid, Consumer}, Consumers), State#state{consumers = add_consumer({ChPid, Consumer}, Consumers),
use = update_use(CUInfo, active)}. use = update_use(CUInfo, active)}.
@ -159,7 +185,8 @@ remove(ChPid, CTag, State = #state{consumers = Consumers}) ->
not_found; not_found;
C = #cr{consumer_count = Count, C = #cr{consumer_count = Count,
limiter = Limiter, limiter = Limiter,
blocked_consumers = Blocked} -> blocked_consumers = Blocked,
link_states = LinkStates} ->
Blocked1 = remove_consumer(ChPid, CTag, Blocked), Blocked1 = remove_consumer(ChPid, CTag, Blocked),
Limiter1 = case Count of Limiter1 = case Count of
1 -> rabbit_limiter:deactivate(Limiter); 1 -> rabbit_limiter:deactivate(Limiter);
@ -168,9 +195,10 @@ remove(ChPid, CTag, State = #state{consumers = Consumers}) ->
Limiter2 = rabbit_limiter:forget_consumer(Limiter1, CTag), Limiter2 = rabbit_limiter:forget_consumer(Limiter1, CTag),
update_ch_record(C#cr{consumer_count = Count - 1, update_ch_record(C#cr{consumer_count = Count - 1,
limiter = Limiter2, limiter = Limiter2,
blocked_consumers = Blocked1}), blocked_consumers = Blocked1,
link_states = maps:remove(CTag, LinkStates)}),
State#state{consumers = State#state{consumers =
remove_consumer(ChPid, CTag, Consumers)} remove_consumer(ChPid, CTag, Consumers)}
end. end.
-spec erase_ch(ch(), state()) -> -spec erase_ch(ch(), state()) ->
@ -192,11 +220,6 @@ erase_ch(ChPid, State = #state{consumers = Consumers}) ->
State#state{consumers = remove_consumers(ChPid, Consumers)}} State#state{consumers = remove_consumers(ChPid, Consumers)}}
end. end.
-spec send_drained(rabbit_amqqueue:name()) -> 'ok'.
send_drained(QName) ->
[update_ch_record(send_drained(QName, C)) || C <- all_ch_record()],
ok.
-spec deliver(fun ((boolean()) -> {fetch_result(), T}), -spec deliver(fun ((boolean()) -> {fetch_result(), T}),
rabbit_amqqueue:name(), state(), boolean(), rabbit_amqqueue:name(), state(), boolean(),
none | {ch(), rabbit_types:ctag()} | {ch(), consumer()}) -> none | {ch(), rabbit_types:ctag()} | {ch(), consumer()}) ->
@ -252,17 +275,37 @@ deliver_to_consumer(FetchFun, E = {ChPid, Consumer}, QName) ->
true -> true ->
block_consumer(C, E), block_consumer(C, E),
undelivered; undelivered;
false -> case rabbit_limiter:can_send(C#cr.limiter, false ->
Consumer#consumer.ack_required, CTag = Consumer#consumer.tag,
Consumer#consumer.tag) of LinkStates = C#cr.link_states,
{suspend, Limiter} -> case maps:find(CTag, LinkStates) of
block_consumer(C#cr{limiter = Limiter}, E), {ok, #link_state{delivery_count = DeliveryCount0,
undelivered; credit = Credit} = LinkState0}
{continue, Limiter} -> when Credit > 0 ->
{delivered, deliver_to_consumer( DeliveryCount = case DeliveryCount0 of
FetchFun, Consumer, credit_api_v1 -> DeliveryCount0;
C#cr{limiter = Limiter}, QName)} _ -> serial_number:add(DeliveryCount0, 1)
end end,
LinkState = LinkState0#link_state{delivery_count = DeliveryCount,
credit = Credit - 1},
C1 = C#cr{link_states = maps:update(CTag, LinkState, LinkStates)},
{delivered, deliver_to_consumer(FetchFun, Consumer, C1, QName)};
{ok, _Exhausted} ->
block_consumer(C, E),
undelivered;
error ->
case rabbit_limiter:can_send(C#cr.limiter,
Consumer#consumer.ack_required,
CTag) of
{suspend, Limiter} ->
block_consumer(C#cr{limiter = Limiter}, E),
undelivered;
{continue, Limiter} ->
{delivered, deliver_to_consumer(
FetchFun, Consumer,
C#cr{limiter = Limiter}, QName)}
end
end
end. end.
deliver_to_consumer(FetchFun, deliver_to_consumer(FetchFun,
@ -349,11 +392,21 @@ possibly_unblock(Update, ChPid, State) ->
end end
end. end.
unblock(C = #cr{blocked_consumers = BlockedQ, limiter = Limiter}, unblock(C = #cr{blocked_consumers = BlockedQ,
limiter = Limiter,
link_states = LinkStates},
State = #state{consumers = Consumers, use = Use}) -> State = #state{consumers = Consumers, use = Use}) ->
case lists:partition( case lists:partition(
fun({_P, {_ChPid, #consumer{tag = CTag}}}) -> fun({_P, {_ChPid, #consumer{tag = CTag}}}) ->
rabbit_limiter:is_consumer_blocked(Limiter, CTag) case maps:find(CTag, LinkStates) of
{ok, #link_state{credit = Credits}}
when Credits > 0 ->
false;
{ok, _Exhausted} ->
true;
error ->
rabbit_limiter:is_consumer_blocked(Limiter, CTag)
end
end, priority_queue:to_list(BlockedQ)) of end, priority_queue:to_list(BlockedQ)) of
{_, []} -> {_, []} ->
update_ch_record(C), update_ch_record(C),
@ -395,28 +448,63 @@ deactivate_limit_fun() ->
C#cr{limiter = rabbit_limiter:deactivate(Limiter)} C#cr{limiter = rabbit_limiter:deactivate(Limiter)}
end. end.
-spec credit(rabbit_amqqueue:name(), boolean(), integer(), boolean(), ch(), -spec drained(rabbit_queue_type:delivery_count() | credit_api_v1, ch(), rabbit_types:ctag()) ->
rabbit_types:ctag(), ok.
state()) -> 'unchanged' | {'unblocked', state()}. drained(AdvancedDeliveryCount, ChPid, CTag) ->
credit(QName, IsEmpty, Credit, Drain, ChPid, CTag, State) ->
case lookup_ch(ChPid) of case lookup_ch(ChPid) of
not_found -> C0 = #cr{link_states = LinkStates = #{CTag := LinkState0}} ->
unchanged; LinkState = LinkState0#link_state{delivery_count = AdvancedDeliveryCount,
#cr{limiter = Limiter} = C -> credit = 0},
C1 = #cr{limiter = Limiter1} = C = C0#cr{link_states = maps:update(CTag, LinkState, LinkStates)},
credit_and_drain(QName, C, CTag, Credit, drain_mode(Drain), IsEmpty), update_ch_record(C);
case is_ch_blocked(C1) orelse _ ->
(not rabbit_limiter:is_consumer_blocked(Limiter, CTag)) orelse ok
rabbit_limiter:is_consumer_blocked(Limiter1, CTag) of
true -> update_ch_record(C1),
unchanged;
false -> unblock(C1, State)
end
end. end.
drain_mode(true) -> drain; -spec process_credit(rabbit_queue_type:delivery_count() | credit_api_v1,
drain_mode(false) -> manual. rabbit_queue_type:credit(), ch(), rabbit_types:ctag(), state()) ->
'unchanged' | {'unblocked', state()}.
process_credit(DeliveryCountRcv, LinkCredit, ChPid, CTag, State) ->
case lookup_ch(ChPid) of
#cr{link_states = LinkStates = #{CTag := LinkState = #link_state{delivery_count = DeliveryCountSnd,
credit = OldLinkCreditSnd}},
unsent_message_count = Count} = C0 ->
LinkCreditSnd = case DeliveryCountSnd of
credit_api_v1 ->
%% LinkCredit refers to LinkCreditSnd
LinkCredit;
_ ->
%% credit API v2
%% LinkCredit refers to LinkCreditRcv
%% See AMQP §2.6.7
serial_number:diff(
serial_number:add(DeliveryCountRcv, LinkCredit),
DeliveryCountSnd)
end,
C = C0#cr{link_states = maps:update(CTag, LinkState#link_state{credit = LinkCreditSnd}, LinkStates)},
case Count >= ?UNSENT_MESSAGE_LIMIT orelse
OldLinkCreditSnd > 0 orelse
LinkCreditSnd < 1 of
true ->
update_ch_record(C),
unchanged;
false ->
unblock(C, State)
end;
_ ->
unchanged
end.
-spec get_link_state(pid(), rabbit_types:ctag()) ->
{rabbit_queue_type:delivery_count() | credit_api_v1, rabbit_queue_type:credit()} | not_found.
get_link_state(ChPid, CTag) ->
case lookup_ch(ChPid) of
#cr{link_states = #{CTag := #link_state{delivery_count = DeliveryCount,
credit = Credit}}} ->
{DeliveryCount, Credit};
_ ->
not_found
end.
-spec utilisation(state()) -> ratio(). -spec utilisation(state()) -> ratio().
utilisation(State) -> utilisation(State) ->
@ -465,14 +553,39 @@ consumer_tag(#consumer{tag = CTag}) ->
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
parse_credit_args(Default, Args) -> %% credit API v2 uses mode
parse_prefetch_count({simple_prefetch, Prefetch}) ->
Prefetch;
parse_prefetch_count({credited, _InitialDeliveryCount}) ->
0;
%% credit API v1 uses prefetch
parse_prefetch_count(Prefetch)
when is_integer(Prefetch) ->
Prefetch.
-spec parse_credit_mode(rabbit_queue_type:consume_mode(), rabbit_framing:amqp_table()) ->
{Prefetch :: non_neg_integer(), auto | manual}.
%% credit API v2
parse_credit_mode({simple_prefetch, Prefetch}, _Args) ->
{Prefetch, auto};
parse_credit_mode({credited, InitialDeliveryCount}, _Args) ->
{InitialDeliveryCount, manual};
%% credit API v1
%% i.e. below function clause should be deleted when feature flag credit_api_v2 becomes required:
parse_credit_mode(Prefetch, Args)
when is_integer(Prefetch) ->
case rabbit_misc:table_lookup(Args, <<"x-credit">>) of case rabbit_misc:table_lookup(Args, <<"x-credit">>) of
{table, T} -> case {rabbit_misc:table_lookup(T, <<"credit">>), {table, T} ->
rabbit_misc:table_lookup(T, <<"drain">>)} of case {rabbit_misc:table_lookup(T, <<"credit">>),
{{long, C}, {bool, D}} -> {C, drain_mode(D)}; rabbit_misc:table_lookup(T, <<"drain">>)} of
_ -> {Default, auto} {{long, 0}, {bool, false}} ->
end; {credit_api_v1, manual};
undefined -> {Default, auto} _ ->
{Prefetch, auto}
end;
undefined ->
{Prefetch, auto}
end. end.
lookup_ch(ChPid) -> lookup_ch(ChPid) ->
@ -492,7 +605,8 @@ ch_record(ChPid, LimiterPid) ->
consumer_count = 0, consumer_count = 0,
blocked_consumers = priority_queue:new(), blocked_consumers = priority_queue:new(),
limiter = Limiter, limiter = Limiter,
unsent_message_count = 0}, unsent_message_count = 0,
link_states = #{}},
put(Key, C), put(Key, C),
C; C;
C = #cr{} -> C C = #cr{} -> C
@ -524,31 +638,14 @@ block_consumer(C = #cr{blocked_consumers = Blocked}, QEntry) ->
is_ch_blocked(#cr{unsent_message_count = Count, limiter = Limiter}) -> is_ch_blocked(#cr{unsent_message_count = Count, limiter = Limiter}) ->
Count >= ?UNSENT_MESSAGE_LIMIT orelse rabbit_limiter:is_suspended(Limiter). Count >= ?UNSENT_MESSAGE_LIMIT orelse rabbit_limiter:is_suspended(Limiter).
send_drained(QName, C = #cr{ch_pid = ChPid, limiter = Limiter}) ->
case rabbit_limiter:drained(Limiter) of
{[], Limiter} -> C;
{CTagCredits, Limiter2} ->
ok = rabbit_classic_queue:send_drained(ChPid, QName, CTagCredits),
C#cr{limiter = Limiter2}
end.
credit_and_drain(QName, C = #cr{ch_pid = ChPid, limiter = Limiter},
CTag, Credit, Mode, IsEmpty) ->
case rabbit_limiter:credit(Limiter, CTag, Credit, Mode, IsEmpty) of
{true, Limiter1} ->
ok = rabbit_classic_queue:send_drained(ChPid, QName, [{CTag, Credit}]),
C#cr{limiter = Limiter1};
{false, Limiter1} -> C#cr{limiter = Limiter1}
end.
tags(CList) -> [CTag || {_P, {_ChPid, #consumer{tag = CTag}}} <- CList]. tags(CList) -> [CTag || {_P, {_ChPid, #consumer{tag = CTag}}} <- CList].
add_consumer({ChPid, Consumer = #consumer{args = Args}}, Queue) -> add_consumer(Key = {_ChPid, #consumer{args = Args}}, Queue) ->
Priority = case rabbit_misc:table_lookup(Args, <<"x-priority">>) of Priority = case rabbit_misc:table_lookup(Args, <<"x-priority">>) of
{_, P} -> P; {_, P} -> P;
_ -> 0 _ -> 0
end, end,
priority_queue:in({ChPid, Consumer}, Priority, Queue). priority_queue:in(Key, Priority, Queue).
remove_consumer(ChPid, CTag, Queue) -> remove_consumer(ChPid, CTag, Queue) ->
priority_queue:filter(fun ({CP, #consumer{tag = CT}}) -> priority_queue:filter(fun ({CP, #consumer{tag = CT}}) ->

View File

@ -11,6 +11,7 @@
-include("amqqueue.hrl"). -include("amqqueue.hrl").
-include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("rabbit_common/include/rabbit.hrl").
-include_lib("amqp10_common/include/amqp10_types.hrl").
-export([ -export([
init/0, init/0,
@ -43,7 +44,8 @@
module/2, module/2,
deliver/4, deliver/4,
settle/5, settle/5,
credit/5, credit_v1/5,
credit/7,
dequeue/5, dequeue/5,
fold_state/3, fold_state/3,
is_policy_applicable/2, is_policy_applicable/2,
@ -63,11 +65,14 @@
-type queue_name() :: rabbit_amqqueue:name(). -type queue_name() :: rabbit_amqqueue:name().
-type queue_state() :: term(). -type queue_state() :: term().
-type msg_tag() :: term(). %% sequence number typically
-type correlation() :: term().
-type arguments() :: queue_arguments | consumer_arguments. -type arguments() :: queue_arguments | consumer_arguments.
-type queue_type() :: rabbit_classic_queue | rabbit_quorum_queue | rabbit_stream_queue. -type queue_type() :: rabbit_classic_queue | rabbit_quorum_queue | rabbit_stream_queue.
%% see AMQP 1.0 §2.6.7
-export_type([queue_type/0]). -type delivery_count() :: sequence_no().
%% Link credit can be negative, see AMQP 1.0 §2.6.7
-type credit() :: integer().
-define(STATE, ?MODULE). -define(STATE, ?MODULE).
@ -83,9 +88,15 @@
-type action() :: -type action() ::
%% indicate to the queue type module that a message has been delivered %% indicate to the queue type module that a message has been delivered
%% fully to the queue %% fully to the queue
{settled, Success :: boolean(), [msg_tag()]} | {settled, queue_name(), [correlation()]} |
{deliver, rabbit_types:ctag(), boolean(), [rabbit_amqqueue:qmsg()]} | {deliver, rabbit_types:ctag(), boolean(), [rabbit_amqqueue:qmsg()]} |
{block | unblock, QueueName :: term()}. {block | unblock, QueueName :: term()} |
%% credit API v2
{credit_reply, rabbit_types:ctag(), delivery_count(), credit(),
Available :: non_neg_integer(), Drain :: boolean()} |
%% credit API v1
{credit_reply_v1, rabbit_types:ctag(), credit(),
Available :: non_neg_integer(), Drain :: boolean()}.
-type actions() :: [action()]. -type actions() :: [action()].
@ -94,44 +105,42 @@
term(). term().
-record(ctx, {module :: module(), -record(ctx, {module :: module(),
%% "publisher confirm queue accounting"
%% queue type implementation should emit a:
%% {settle, Success :: boolean(), msg_tag()}
%% to either settle or reject the delivery of a
%% message to the queue instance
%% The queue type module will then emit a {confirm | reject, [msg_tag()}
%% action to the channel or channel like process when a msg_tag
%% has reached its conclusion
state :: queue_state()}). state :: queue_state()}).
-record(?STATE, {ctxs = #{} :: #{queue_name() => #ctx{}} -record(?STATE, {ctxs = #{} :: #{queue_name() => #ctx{}}
}). }).
-opaque state() :: #?STATE{}. -opaque state() :: #?STATE{}.
%% Delete atom 'credit_api_v1' when feature flag credit_api_v2 becomes required.
-type consume_mode() :: {simple_prefetch, non_neg_integer()} | {credited, Initial :: delivery_count() | credit_api_v1}.
-type consume_spec() :: #{no_ack := boolean(), -type consume_spec() :: #{no_ack := boolean(),
channel_pid := pid(), channel_pid := pid(),
limiter_pid => pid() | none, limiter_pid => pid() | none,
limiter_active => boolean(), limiter_active => boolean(),
prefetch_count => non_neg_integer(), mode := consume_mode(),
consumer_tag := rabbit_types:ctag(), consumer_tag := rabbit_types:ctag(),
exclusive_consume => boolean(), exclusive_consume => boolean(),
args => rabbit_framing:amqp_table(), args => rabbit_framing:amqp_table(),
ok_msg := term(), ok_msg := term(),
acting_user := rabbit_types:username()}. acting_user := rabbit_types:username()}.
-type delivery_options() :: #{correlation => term(), %% sequence no typically -type delivery_options() :: #{correlation => correlation(),
atom() => term()}. atom() => term()}.
-type settle_op() :: 'complete' | 'requeue' | 'discard'. -type settle_op() :: 'complete' | 'requeue' | 'discard'.
-export_type([state/0, -export_type([state/0,
consume_mode/0,
consume_spec/0, consume_spec/0,
delivery_options/0, delivery_options/0,
action/0, action/0,
actions/0, actions/0,
settle_op/0]). settle_op/0,
queue_type/0,
credit/0,
correlation/0,
delivery_count/0]).
-callback is_enabled() -> boolean(). -callback is_enabled() -> boolean().
@ -179,7 +188,8 @@
-callback consume(amqqueue:amqqueue(), -callback consume(amqqueue:amqqueue(),
consume_spec(), consume_spec(),
queue_state()) -> queue_state()) ->
{ok, queue_state(), actions()} | {error, term()} | {ok, queue_state(), actions()} |
{error, term()} |
{protocol_error, Type :: atom(), Reason :: string(), Args :: term()}. {protocol_error, Type :: atom(), Reason :: string(), Args :: term()}.
-callback cancel(amqqueue:amqqueue(), -callback cancel(amqqueue:amqqueue(),
@ -207,8 +217,13 @@
{queue_state(), actions()} | {queue_state(), actions()} |
{'protocol_error', Type :: atom(), Reason :: string(), Args :: term()}. {'protocol_error', Type :: atom(), Reason :: string(), Args :: term()}.
-callback credit(queue_name(), rabbit_types:ctag(), %% Delete this callback when feature flag credit_api_v2 becomes required.
non_neg_integer(), Drain :: boolean(), queue_state()) -> -callback credit_v1(queue_name(), rabbit_types:ctag(), credit(), Drain :: boolean(), queue_state()) ->
{queue_state(), actions()}.
%% credit API v2
-callback credit(queue_name(), rabbit_types:ctag(), delivery_count(), credit(),
Drain :: boolean(), Echo :: boolean(), queue_state()) ->
{queue_state(), actions()}. {queue_state(), actions()}.
-callback dequeue(queue_name(), NoAck :: boolean(), LimiterPid :: pid(), -callback dequeue(queue_name(), NoAck :: boolean(), LimiterPid :: pid(),
@ -414,7 +429,9 @@ new(Q, State) when ?is_amqqueue(Q) ->
set_ctx(Q, Ctx, State). set_ctx(Q, Ctx, State).
-spec consume(amqqueue:amqqueue(), consume_spec(), state()) -> -spec consume(amqqueue:amqqueue(), consume_spec(), state()) ->
{ok, state()} | {error, term()}. {ok, state()} |
{error, term()} |
{protocol_error, Type :: atom(), Reason :: string(), Args :: term()}.
consume(Q, Spec, State) -> consume(Q, Spec, State) ->
#ctx{state = CtxState0} = Ctx = get_ctx(Q, State), #ctx{state = CtxState0} = Ctx = get_ctx(Q, State),
Mod = amqqueue:get_type(Q), Mod = amqqueue:get_type(Q),
@ -629,15 +646,23 @@ settle(#resource{kind = queue} = QRef, Op, CTag, MsgIds, Ctxs) ->
end end
end. end.
-spec credit(amqqueue:amqqueue() | queue_name(), %% Delete this function when feature flag credit_api_v2 becomes required.
rabbit_types:ctag(), non_neg_integer(), -spec credit_v1(queue_name(), rabbit_types:ctag(), credit(), boolean(), state()) ->
boolean(), state()) -> {ok, state(), actions()}. {ok, state(), actions()}.
credit(Q, CTag, Credit, Drain, Ctxs) -> credit_v1(QName, CTag, LinkCreditSnd, Drain, Ctxs) ->
#ctx{state = State0, #ctx{state = State0,
module = Mod} = Ctx = get_ctx(Q, Ctxs), module = Mod} = Ctx = get_ctx(QName, Ctxs),
QName = amqqueue:get_name(Q), {State, Actions} = Mod:credit_v1(QName, CTag, LinkCreditSnd, Drain, State0),
{State, Actions} = Mod:credit(QName, CTag, Credit, Drain, State0), {ok, set_ctx(QName, Ctx#ctx{state = State}, Ctxs), Actions}.
{ok, set_ctx(Q, Ctx#ctx{state = State}, Ctxs), Actions}.
%% credit API v2
-spec credit(queue_name(), rabbit_types:ctag(), delivery_count(), credit(), boolean(), boolean(), state()) ->
{ok, state(), actions()}.
credit(QName, CTag, DeliveryCount, Credit, Drain, Echo, Ctxs) ->
#ctx{state = State0,
module = Mod} = Ctx = get_ctx(QName, Ctxs),
{State, Actions} = Mod:credit(QName, CTag, DeliveryCount, Credit, Drain, Echo, State0),
{ok, set_ctx(QName, Ctx#ctx{state = State}, Ctxs), Actions}.
-spec dequeue(amqqueue:amqqueue(), boolean(), -spec dequeue(amqqueue:amqqueue(), boolean(),
pid(), rabbit_types:ctag(), state()) -> pid(), rabbit_types:ctag(), state()) ->

View File

@ -25,7 +25,7 @@
delete_immediately/1]). delete_immediately/1]).
-export([state_info/1, info/2, stat/1, infos/1, infos/2]). -export([state_info/1, info/2, stat/1, infos/1, infos/2]).
-export([settle/5, dequeue/5, consume/3, cancel/5]). -export([settle/5, dequeue/5, consume/3, cancel/5]).
-export([credit/5]). -export([credit_v1/5, credit/7]).
-export([purge/1]). -export([purge/1]).
-export([stateless_deliver/2, deliver/3]). -export([stateless_deliver/2, deliver/3]).
-export([dead_letter_publish/5]). -export([dead_letter_publish/5]).
@ -130,6 +130,7 @@
-define(DELETE_TIMEOUT, 5000). -define(DELETE_TIMEOUT, 5000).
-define(ADD_MEMBER_TIMEOUT, 5000). -define(ADD_MEMBER_TIMEOUT, 5000).
-define(SNAPSHOT_INTERVAL, 8192). %% the ra default is 4096 -define(SNAPSHOT_INTERVAL, 8192). %% the ra default is 4096
-define(UNLIMITED_PREFETCH_COUNT, 2000). %% something large for ra
%%----------- QQ policies --------------------------------------------------- %%----------- QQ policies ---------------------------------------------------
@ -477,7 +478,7 @@ capabilities() ->
<<"x-single-active-consumer">>, <<"x-queue-type">>, <<"x-single-active-consumer">>, <<"x-queue-type">>,
<<"x-quorum-initial-group-size">>, <<"x-delivery-limit">>, <<"x-quorum-initial-group-size">>, <<"x-delivery-limit">>,
<<"x-message-ttl">>, <<"x-queue-leader-locator">>], <<"x-message-ttl">>, <<"x-queue-leader-locator">>],
consumer_arguments => [<<"x-priority">>, <<"x-credit">>], consumer_arguments => [<<"x-priority">>],
server_named => false}. server_named => false}.
rpc_delete_metrics(QName) -> rpc_delete_metrics(QName) ->
@ -800,8 +801,11 @@ settle(_QName, requeue, CTag, MsgIds, QState) ->
settle(_QName, discard, CTag, MsgIds, QState) -> settle(_QName, discard, CTag, MsgIds, QState) ->
rabbit_fifo_client:discard(quorum_ctag(CTag), MsgIds, QState). rabbit_fifo_client:discard(quorum_ctag(CTag), MsgIds, QState).
credit(_QName, CTag, Credit, Drain, QState) -> credit_v1(_QName, CTag, Credit, Drain, QState) ->
rabbit_fifo_client:credit(quorum_ctag(CTag), Credit, Drain, QState). rabbit_fifo_client:credit_v1(quorum_ctag(CTag), Credit, Drain, QState).
credit(_QName, CTag, DeliveryCount, Credit, Drain, Echo, QState) ->
rabbit_fifo_client:credit(quorum_ctag(CTag), DeliveryCount, Credit, Drain, Echo, QState).
-spec dequeue(rabbit_amqqueue:name(), NoAck :: boolean(), pid(), -spec dequeue(rabbit_amqqueue:name(), NoAck :: boolean(), pid(),
rabbit_types:ctag(), rabbit_fifo_client:state()) -> rabbit_types:ctag(), rabbit_fifo_client:state()) ->
@ -829,7 +833,7 @@ consume(Q, #{limiter_active := true}, _State)
consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) -> consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) ->
#{no_ack := NoAck, #{no_ack := NoAck,
channel_pid := ChPid, channel_pid := ChPid,
prefetch_count := ConsumerPrefetchCount, mode := Mode,
consumer_tag := ConsumerTag0, consumer_tag := ConsumerTag0,
exclusive_consume := ExclusiveConsume, exclusive_consume := ExclusiveConsume,
args := Args, args := Args,
@ -840,35 +844,33 @@ consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) ->
QName = amqqueue:get_name(Q), QName = amqqueue:get_name(Q),
maybe_send_reply(ChPid, OkMsg), maybe_send_reply(ChPid, OkMsg),
ConsumerTag = quorum_ctag(ConsumerTag0), ConsumerTag = quorum_ctag(ConsumerTag0),
%% A prefetch count of 0 means no limitation,
%% let's make it into something large for ra
Prefetch0 = case ConsumerPrefetchCount of
0 -> 2000;
Other -> Other
end,
%% consumer info is used to describe the consumer properties %% consumer info is used to describe the consumer properties
AckRequired = not NoAck, AckRequired = not NoAck,
ConsumerMeta = #{ack => AckRequired, {CreditMode, EffectivePrefetch, DeclaredPrefetch, ConsumerMeta0} =
prefetch => ConsumerPrefetchCount, case Mode of
args => Args, {credited, C} ->
username => ActingUser}, Meta = if C =:= credit_api_v1 ->
#{};
{CreditMode, Credit, Drain} = parse_credit_args(Prefetch0, Args), is_integer(C) ->
%% if the mode is credited we should send a separate credit command #{initial_delivery_count => C}
%% after checkout and give 0 credits initally end,
Prefetch = case CreditMode of {credited, 0, 0, Meta};
credited -> 0; {simple_prefetch = M, Declared} ->
simple_prefetch -> Prefetch0 Effective = case Declared of
end, 0 -> ?UNLIMITED_PREFETCH_COUNT;
{ok, QState1} = rabbit_fifo_client:checkout(ConsumerTag, Prefetch, _ -> Declared
CreditMode, ConsumerMeta, end,
QState0), {M, Effective, Declared, #{}}
QState = case CreditMode of end,
credited when Credit > 0 -> ConsumerMeta = maps:merge(
rabbit_fifo_client:credit(ConsumerTag, Credit, Drain, ConsumerMeta0,
QState1); #{ack => AckRequired,
_ -> QState1 prefetch => DeclaredPrefetch,
end, args => Args,
username => ActingUser}),
{ok, QState} = rabbit_fifo_client:checkout(ConsumerTag, EffectivePrefetch,
CreditMode, ConsumerMeta,
QState0),
case single_active_consumer_on(Q) of case single_active_consumer_on(Q) of
true -> true ->
%% get the leader from state %% get the leader from state
@ -883,10 +885,10 @@ consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) ->
rabbit_core_metrics:consumer_created( rabbit_core_metrics:consumer_created(
ChPid, ConsumerTag, ExclusiveConsume, ChPid, ConsumerTag, ExclusiveConsume,
AckRequired, QName, AckRequired, QName,
ConsumerPrefetchCount, ActivityStatus == single_active, %% Active DeclaredPrefetch, ActivityStatus == single_active, %% Active
ActivityStatus, Args), ActivityStatus, Args),
emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume, emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume,
AckRequired, QName, Prefetch, AckRequired, QName, DeclaredPrefetch,
Args, none, ActingUser), Args, none, ActingUser),
{ok, QState}; {ok, QState};
{error, Error} -> {error, Error} ->
@ -898,10 +900,10 @@ consume(Q, Spec, QState0) when ?amqqueue_is_quorum(Q) ->
rabbit_core_metrics:consumer_created( rabbit_core_metrics:consumer_created(
ChPid, ConsumerTag, ExclusiveConsume, ChPid, ConsumerTag, ExclusiveConsume,
AckRequired, QName, AckRequired, QName,
ConsumerPrefetchCount, true, %% Active DeclaredPrefetch, true, %% Active
up, Args), up, Args),
emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume, emit_consumer_created(ChPid, ConsumerTag, ExclusiveConsume,
AckRequired, QName, Prefetch, AckRequired, QName, DeclaredPrefetch,
Args, none, ActingUser), Args, none, ActingUser),
{ok, QState} {ok, QState}
end. end.
@ -1818,20 +1820,6 @@ overflow(<<"reject-publish-dlx">> = V, Def, QName) ->
[V, rabbit_misc:rs(QName)]), [V, rabbit_misc:rs(QName)]),
Def. Def.
parse_credit_args(Default, Args) ->
case rabbit_misc:table_lookup(Args, <<"x-credit">>) of
{table, T} ->
case {rabbit_misc:table_lookup(T, <<"credit">>),
rabbit_misc:table_lookup(T, <<"drain">>)} of
{{long, C}, {bool, D}} ->
{credited, C, D};
_ ->
{simple_prefetch, Default, false}
end;
undefined ->
{simple_prefetch, Default, false}
end.
-spec notify_decorators(amqqueue:amqqueue()) -> 'ok'. -spec notify_decorators(amqqueue:amqqueue()) -> 'ok'.
notify_decorators(Q) when ?is_amqqueue(Q) -> notify_decorators(Q) when ?is_amqqueue(Q) ->
QName = amqqueue:get_name(Q), QName = amqqueue:get_name(Q),

View File

@ -43,12 +43,12 @@
-include_lib("rabbit_common/include/rabbit_framing.hrl"). -include_lib("rabbit_common/include/rabbit_framing.hrl").
-include_lib("rabbit_common/include/rabbit.hrl"). -include_lib("rabbit_common/include/rabbit.hrl").
-export([start_link/2, info_keys/0, info/1, info/2, force_event_refresh/2, -export([start_link/1, info_keys/0, info/1, info/2, force_event_refresh/2,
shutdown/2]). shutdown/2]).
-export([system_continue/3, system_terminate/4, system_code_change/4]). -export([system_continue/3, system_terminate/4, system_code_change/4]).
-export([init/3, mainloop/4, recvloop/4]). -export([init/2, mainloop/4, recvloop/4]).
-export([conserve_resources/3, server_properties/1]). -export([conserve_resources/3, server_properties/1]).
@ -145,11 +145,10 @@
%%-------------------------------------------------------------------------- %%--------------------------------------------------------------------------
-spec start_link(pid(), any()) -> rabbit_types:ok(pid()). -spec start_link(ranch:ref()) ->
rabbit_types:ok(pid()).
start_link(HelperSup, Ref) -> start_link(Ref) ->
Pid = proc_lib:spawn_link(?MODULE, init, [self(), HelperSup, Ref]), Pid = proc_lib:spawn_link(?MODULE, init, [self(), Ref]),
{ok, Pid}. {ok, Pid}.
-spec shutdown(pid(), string()) -> 'ok'. -spec shutdown(pid(), string()) -> 'ok'.
@ -157,14 +156,14 @@ start_link(HelperSup, Ref) ->
shutdown(Pid, Explanation) -> shutdown(Pid, Explanation) ->
gen_server:call(Pid, {shutdown, Explanation}, infinity). gen_server:call(Pid, {shutdown, Explanation}, infinity).
-spec init(pid(), pid(), any()) -> no_return(). -spec init(pid(), ranch:ref()) ->
no_return().
init(Parent, HelperSup, Ref) -> init(Parent, Ref) ->
?LG_PROCESS_TYPE(reader), ?LG_PROCESS_TYPE(reader),
{ok, Sock} = rabbit_networking:handshake(Ref, {ok, Sock} = rabbit_networking:handshake(Ref,
application:get_env(rabbit, proxy_protocol, false)), application:get_env(rabbit, proxy_protocol, false)),
Deb = sys:debug_options([]), Deb = sys:debug_options([]),
start_connection(Parent, HelperSup, Ref, Deb, Sock). start_connection(Parent, Ref, Deb, Sock).
-spec system_continue(_,_,{[binary()], non_neg_integer(), #v1{}}) -> any(). -spec system_continue(_,_,{[binary()], non_neg_integer(), #v1{}}) -> any().
@ -291,10 +290,10 @@ socket_op(Sock, Fun) ->
exit(normal) exit(normal)
end. end.
-spec start_connection(pid(), pid(), ranch:ref(), any(), rabbit_net:socket()) -> -spec start_connection(pid(), ranch:ref(), any(), rabbit_net:socket()) ->
no_return(). no_return().
start_connection(Parent, HelperSup, RanchRef, Deb, Sock) -> start_connection(Parent, RanchRef, Deb, Sock) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),
RealSocket = rabbit_net:unwrap_socket(Sock), RealSocket = rabbit_net:unwrap_socket(Sock),
Name = case rabbit_net:connection_string(Sock, inbound) of Name = case rabbit_net:connection_string(Sock, inbound) of
@ -337,7 +336,7 @@ start_connection(Parent, HelperSup, RanchRef, Deb, Sock) ->
pending_recv = false, pending_recv = false,
connection_state = pre_init, connection_state = pre_init,
queue_collector = undefined, %% started on tune-ok queue_collector = undefined, %% started on tune-ok
helper_sup = HelperSup, helper_sup = none,
heartbeater = none, heartbeater = none,
channel_sup_sup_pid = none, channel_sup_sup_pid = none,
channel_count = 0, channel_count = 0,
@ -356,16 +355,16 @@ start_connection(Parent, HelperSup, RanchRef, Deb, Sock) ->
%% connection was closed cleanly by the client %% connection was closed cleanly by the client
#v1{connection = #connection{user = #user{username = Username}, #v1{connection = #connection{user = #user{username = Username},
vhost = VHost}} -> vhost = VHost}} ->
rabbit_log_connection:info("closing AMQP connection ~tp (~ts, vhost: '~ts', user: '~ts')", rabbit_log_connection:info("closing AMQP connection (~ts, vhost: '~ts', user: '~ts')",
[self(), dynamic_connection_name(Name), VHost, Username]); [dynamic_connection_name(Name), VHost, Username]);
%% just to be more defensive %% just to be more defensive
_ -> _ ->
rabbit_log_connection:info("closing AMQP connection ~tp (~ts)", rabbit_log_connection:info("closing AMQP connection (~ts)",
[self(), dynamic_connection_name(Name)]) [dynamic_connection_name(Name)])
end end
catch catch
Ex -> Ex ->
log_connection_exception(dynamic_connection_name(Name), Ex) log_connection_exception(dynamic_connection_name(Name), Ex)
after after
%% We don't call gen_tcp:close/1 here since it waits for %% We don't call gen_tcp:close/1 here since it waits for
%% pending output to be sent, which results in unnecessary %% pending output to be sent, which results in unnecessary
@ -499,8 +498,8 @@ mainloop(Deb, Buf, BufLen, State = #v1{sock = Sock,
%% %%
%% The goal is to not log TCP healthchecks (a connection %% The goal is to not log TCP healthchecks (a connection
%% with no data received) unless specified otherwise. %% with no data received) unless specified otherwise.
Fmt = "accepting AMQP connection ~tp (~ts)", Fmt = "accepting AMQP connection ~ts",
Args = [self(), ConnName], Args = [ConnName],
case Recv of case Recv of
closed -> _ = rabbit_log_connection:debug(Fmt, Args); closed -> _ = rabbit_log_connection:debug(Fmt, Args);
_ -> _ = rabbit_log_connection:info(Fmt, Args) _ -> _ = rabbit_log_connection:info(Fmt, Args)
@ -1078,75 +1077,64 @@ handle_input({frame_payload, Type, Channel, PayloadSize}, Data, State) ->
Type, Channel, Payload, State) Type, Channel, Payload, State)
end; end;
handle_input(handshake, <<"AMQP", A, B, C, D, Rest/binary>>, State) -> handle_input(handshake, <<"AMQP", A, B, C, D, Rest/binary>>, State) ->
{Rest, handshake({A, B, C, D}, State)}; {Rest, version_negotiation({A, B, C, D}, State)};
handle_input(handshake, <<Other:8/binary, _/binary>>, #v1{sock = Sock}) -> handle_input(handshake, <<Other:8/binary, _/binary>>, #v1{sock = Sock}) ->
refuse_connection(Sock, {bad_header, Other}); refuse_connection(Sock, {bad_header, Other});
handle_input(Callback, Data, _State) -> handle_input(Callback, Data, _State) ->
throw({bad_input, Callback, Data}). throw({bad_input, Callback, Data}).
%% The two rules pertaining to version negotiation: %% AMQP 1.0 §2.2
%% version_negotiation({Id, 1, 0, 0}, State) ->
%% * If the server cannot support the protocol specified in the become_10(Id, State);
%% protocol header, it MUST respond with a valid protocol header and version_negotiation({0, 0, 9, 1}, State) ->
%% then close the socket connection. start_091_connection({0, 9, 1}, rabbit_framing_amqp_0_9_1, State);
%% version_negotiation({1, 1, 0, 9}, State) ->
%% * The server MUST provide a protocol version that is lower than or %% This is the protocol header for 0-9, which we can safely treat as though it were 0-9-1.
%% equal to that requested by the client in the protocol header. start_091_connection({0, 9, 0}, rabbit_framing_amqp_0_9_1, State);
handshake({0, 0, 9, 1}, State) -> version_negotiation(Vsn = {0, 0, Minor, _}, #v1{sock = Sock})
start_connection({0, 9, 1}, rabbit_framing_amqp_0_9_1, State); when Minor >= 9 ->
refuse_connection(Sock, {bad_version, Vsn}, {0, 0, 9, 1});
%% This is the protocol header for 0-9, which we can safely treat as version_negotiation(Vsn, #v1{sock = Sock}) ->
%% though it were 0-9-1.
handshake({1, 1, 0, 9}, State) ->
start_connection({0, 9, 0}, rabbit_framing_amqp_0_9_1, State);
%% This is what most clients send for 0-8. The 0-8 spec, confusingly,
%% defines the version as 8-0.
handshake({1, 1, 8, 0}, State) ->
start_connection({8, 0, 0}, rabbit_framing_amqp_0_8, State);
%% The 0-8 spec as on the AMQP web site actually has this as the
%% protocol header; some libraries e.g., py-amqplib, send it when they
%% want 0-8.
handshake({1, 1, 9, 1}, State) ->
start_connection({8, 0, 0}, rabbit_framing_amqp_0_8, State);
%% ... and finally, the 1.0 spec is crystal clear!
handshake({Id, 1, 0, 0}, State) ->
become_1_0(Id, State);
handshake(Vsn, #v1{sock = Sock}) ->
refuse_connection(Sock, {bad_version, Vsn}). refuse_connection(Sock, {bad_version, Vsn}).
%% Offer a protocol version to the client. Connection.start only %% Offer a protocol version to the client. Connection.start only
%% includes a major and minor version number, Luckily 0-9 and 0-9-1 %% includes a major and minor version number, Luckily 0-9 and 0-9-1
%% are similar enough that clients will be happy with either. %% are similar enough that clients will be happy with either.
start_connection({ProtocolMajor, ProtocolMinor, _ProtocolRevision}, start_091_connection({ProtocolMajor, ProtocolMinor, _ProtocolRevision},
Protocol, Protocol,
State = #v1{sock = Sock, connection = Connection}) -> #v1{parent = Parent,
sock = Sock,
connection = Connection} = State0) ->
ConnectionHelperSupFlags = #{strategy => one_for_one,
intensity => 10,
period => 10,
auto_shutdown => any_significant},
{ok, ConnectionHelperSupPid} = rabbit_connection_sup:start_connection_helper_sup(
Parent, ConnectionHelperSupFlags),
rabbit_networking:register_connection(self()), rabbit_networking:register_connection(self()),
Start = #'connection.start'{ Start = #'connection.start'{
version_major = ProtocolMajor, version_major = ProtocolMajor,
version_minor = ProtocolMinor, version_minor = ProtocolMinor,
server_properties = server_properties(Protocol), server_properties = server_properties(Protocol),
mechanisms = auth_mechanisms_binary(Sock), mechanisms = auth_mechanisms_binary(Sock),
locales = <<"en_US">> }, locales = <<"en_US">> },
ok = send_on_channel0(Sock, Start, Protocol), ok = send_on_channel0(Sock, Start, Protocol),
switch_callback(State#v1{connection = Connection#connection{ State = State0#v1{connection = Connection#connection{
timeout_sec = ?NORMAL_TIMEOUT, timeout_sec = ?NORMAL_TIMEOUT,
protocol = Protocol}, protocol = Protocol},
connection_state = starting}, connection_state = starting,
frame_header, 7). helper_sup = ConnectionHelperSupPid},
switch_callback(State, frame_header, 7).
-spec refuse_connection(rabbit_net:socket(), any()) -> no_return().
refuse_connection(Sock, Exception) ->
refuse_connection(Sock, Exception, {0, 1, 0, 0}).
-spec refuse_connection(_, _, _) -> no_return(). -spec refuse_connection(_, _, _) -> no_return().
refuse_connection(Sock, Exception, {A, B, C, D}) -> refuse_connection(Sock, Exception, {A, B, C, D}) ->
ok = inet_op(fun () -> rabbit_net:send(Sock, <<"AMQP",A,B,C,D>>) end), ok = inet_op(fun () -> rabbit_net:send(Sock, <<"AMQP",A,B,C,D>>) end),
throw(Exception). throw(Exception).
-spec refuse_connection(rabbit_net:socket(), any()) -> no_return().
refuse_connection(Sock, Exception) ->
refuse_connection(Sock, Exception, {0, 0, 9, 1}).
ensure_stats_timer(State = #v1{connection_state = running}) -> ensure_stats_timer(State = #v1{connection_state = running}) ->
rabbit_event:ensure_stats_timer(State, #v1.stats_timer, emit_stats); rabbit_event:ensure_stats_timer(State, #v1.stats_timer, emit_stats);
@ -1283,9 +1271,8 @@ handle_method0(#'connection.open'{virtual_host = VHost},
rabbit_event:notify(connection_created, Infos), rabbit_event:notify(connection_created, Infos),
maybe_emit_stats(State1), maybe_emit_stats(State1),
rabbit_log_connection:info( rabbit_log_connection:info(
"connection ~tp (~ts): " "connection ~ts: user '~ts' authenticated and granted access to vhost '~ts'",
"user '~ts' authenticated and granted access to vhost '~ts'", [dynamic_connection_name(ConnName), Username, VHost]),
[self(), dynamic_connection_name(ConnName), Username, VHost]),
State1; State1;
handle_method0(#'connection.close'{}, State) when ?IS_RUNNING(State) -> handle_method0(#'connection.close'{}, State) when ?IS_RUNNING(State) ->
lists:foreach(fun rabbit_channel:shutdown/1, all_channels()), lists:foreach(fun rabbit_channel:shutdown/1, all_channels()),
@ -1309,9 +1296,9 @@ handle_method0(#'connection.update_secret'{new_secret = NewSecret, reason = Reas
log_name = ConnName} = Conn, log_name = ConnName} = Conn,
sock = Sock}) when ?IS_RUNNING(State) -> sock = Sock}) when ?IS_RUNNING(State) ->
rabbit_log_connection:debug( rabbit_log_connection:debug(
"connection ~tp (~ts) of user '~ts': " "connection ~ts of user '~ts': "
"asked to update secret, reason: ~ts", "asked to update secret, reason: ~ts",
[self(), dynamic_connection_name(ConnName), Username, Reason]), [dynamic_connection_name(ConnName), Username, Reason]),
case rabbit_access_control:update_state(User, NewSecret) of case rabbit_access_control:update_state(User, NewSecret) of
{ok, User1} -> {ok, User1} ->
%% User/auth backend state has been updated. Now we can propagate it to channels %% User/auth backend state has been updated. Now we can propagate it to channels
@ -1326,9 +1313,8 @@ handle_method0(#'connection.update_secret'{new_secret = NewSecret, reason = Reas
end, all_channels()), end, all_channels()),
ok = send_on_channel0(Sock, #'connection.update_secret_ok'{}, Protocol), ok = send_on_channel0(Sock, #'connection.update_secret_ok'{}, Protocol),
rabbit_log_connection:info( rabbit_log_connection:info(
"connection ~tp (~ts): " "connection ~ts: user '~ts' updated secret, reason: ~ts",
"user '~ts' updated secret, reason: ~ts", [dynamic_connection_name(ConnName), Username, Reason]),
[self(), dynamic_connection_name(ConnName), Username, Reason]),
State#v1{connection = Conn#connection{user = User1}}; State#v1{connection = Conn#connection{user = User1}};
{refused, Message} -> {refused, Message} ->
rabbit_log_connection:error("Secret update was refused for user '~ts': ~tp", rabbit_log_connection:error("Secret update was refused for user '~ts': ~tp",
@ -1643,32 +1629,34 @@ emit_stats(State) ->
ensure_stats_timer(State1). ensure_stats_timer(State1).
%% 1.0 stub %% 1.0 stub
-spec become_1_0(non_neg_integer(), #v1{}) -> no_return(). -spec become_10(non_neg_integer(), #v1{}) -> no_return().
become_10(Id, State = #v1{sock = Sock}) ->
Mode = case Id of
0 -> amqp;
3 -> sasl;
_ -> refuse_connection(
Sock, {unsupported_amqp1_0_protocol_id, Id},
{3, 1, 0, 0})
end,
F = fun (_Deb, Buf, BufLen, State0) ->
{rabbit_amqp_reader, init,
[Mode, pack_for_1_0(Buf, BufLen, State0)]}
end,
State#v1{connection_state = {become, F}}.
become_1_0(Id, State = #v1{sock = Sock}) -> pack_for_1_0(Buf, BufLen, #v1{sock = Sock,
case code:is_loaded(rabbit_amqp1_0_reader) of
false -> refuse_connection(Sock, amqp1_0_plugin_not_enabled);
_ -> Mode = case Id of
0 -> amqp;
3 -> sasl;
_ -> refuse_connection(
Sock, {unsupported_amqp1_0_protocol_id, Id},
{3, 1, 0, 0})
end,
F = fun (_Deb, Buf, BufLen, S) ->
{rabbit_amqp1_0_reader, init,
[Mode, pack_for_1_0(Buf, BufLen, S)]}
end,
State#v1{connection_state = {become, F}}
end.
pack_for_1_0(Buf, BufLen, #v1{parent = Parent,
sock = Sock,
recv_len = RecvLen, recv_len = RecvLen,
pending_recv = PendingRecv, pending_recv = PendingRecv,
helper_sup = SupPid, proxy_socket = ProxySocket,
proxy_socket = ProxySocket}) -> connection = #connection{
{Parent, Sock, RecvLen, PendingRecv, SupPid, Buf, BufLen, ProxySocket}. name = Name,
host = Host,
peer_host = PeerHost,
port = Port,
peer_port = PeerPort,
connected_at = ConnectedAt}}) ->
{Sock, RecvLen, PendingRecv, Buf, BufLen, ProxySocket,
Name, Host, PeerHost, Port, PeerPort, ConnectedAt}.
respond_and_close(State, Channel, Protocol, Reason, LogErr) -> respond_and_close(State, Channel, Protocol, Reason, LogErr) ->
log_hard_error(State, Channel, LogErr), log_hard_error(State, Channel, LogErr),
@ -1802,7 +1790,8 @@ augment_connection_log_name(#connection{name = Name} = Connection) ->
Connection; Connection;
UserSpecifiedName -> UserSpecifiedName ->
LogName = <<Name/binary, " - ", UserSpecifiedName/binary>>, LogName = <<Name/binary, " - ", UserSpecifiedName/binary>>,
rabbit_log_connection:info("connection ~tp (~ts) has a client-provided name: ~ts", [self(), Name, UserSpecifiedName]), rabbit_log_connection:info("connection ~ts has a client-provided name: ~ts",
[Name, UserSpecifiedName]),
?store_proc_name(LogName), ?store_proc_name(LogName),
Connection#connection{log_name = LogName} Connection#connection{log_name = LogName}
end. end.

View File

@ -23,7 +23,8 @@
handle_event/3, handle_event/3,
deliver/3, deliver/3,
settle/5, settle/5,
credit/5, credit_v1/5,
credit/7,
dequeue/5, dequeue/5,
info/2, info/2,
queue_length/1, queue_length/1,
@ -69,25 +70,32 @@
-type appender_seq() :: non_neg_integer(). -type appender_seq() :: non_neg_integer().
-type msg_id() :: non_neg_integer().
-type msg() :: term(). %% TODO: refine -type msg() :: term(). %% TODO: refine
-record(stream, {credit :: integer(), -record(stream, {mode :: rabbit_queue_type:consume_mode(),
max :: non_neg_integer(), delivery_count :: none | rabbit_queue_type:delivery_count(),
credit :: rabbit_queue_type:credit(),
ack :: boolean(),
start_offset = 0 :: non_neg_integer(), start_offset = 0 :: non_neg_integer(),
listening_offset = 0 :: non_neg_integer(), listening_offset = 0 :: non_neg_integer(),
last_consumed_offset = 0 :: non_neg_integer(),
log :: undefined | osiris_log:state(), log :: undefined | osiris_log:state(),
chunk_iterator :: undefined | osiris_log:chunk_iterator(),
%% These messages were already read ahead from the Osiris log,
%% were part of an uncompressed sub batch, and are buffered in
%% reversed order until the consumer has more credits to consume them.
buffer_msgs_rev = [] :: [rabbit_amqqueue:qmsg()],
reader_options :: map()}). reader_options :: map()}).
-record(stream_client, {stream_id :: string(), -record(stream_client, {stream_id :: string(),
name :: term(), name :: rabbit_amqqueue:name(),
leader :: pid(), leader :: pid(),
local_pid :: undefined | pid(), local_pid :: undefined | pid(),
next_seq = 1 :: non_neg_integer(), next_seq = 1 :: non_neg_integer(),
correlation = #{} :: #{appender_seq() => {msg_id(), msg()}}, correlation = #{} :: #{appender_seq() => {rabbit_queue_type:correlation(), msg()}},
soft_limit :: non_neg_integer(), soft_limit :: non_neg_integer(),
slow = false :: boolean(), slow = false :: boolean(),
readers = #{} :: #{term() => #stream{}}, readers = #{} :: #{rabbit_types:ctag() => #stream{}},
writer_id :: binary(), writer_id :: binary(),
filtering_supported :: boolean() filtering_supported :: boolean()
}). }).
@ -264,14 +272,15 @@ format(Q, Ctx) ->
{state, down}] {state, down}]
end. end.
consume(Q, #{prefetch_count := 0}, _) consume(Q, #{mode := {simple_prefetch, 0}}, _)
when ?amqqueue_is_stream(Q) -> when ?amqqueue_is_stream(Q) ->
{protocol_error, precondition_failed, "consumer prefetch count is not set for '~ts'", {protocol_error, precondition_failed, "consumer prefetch count is not set for stream ~ts",
[rabbit_misc:rs(amqqueue:get_name(Q))]}; [rabbit_misc:rs(amqqueue:get_name(Q))]};
consume(Q, #{no_ack := true}, _) consume(Q, #{no_ack := true,
mode := {simple_prefetch, _}}, _)
when ?amqqueue_is_stream(Q) -> when ?amqqueue_is_stream(Q) ->
{protocol_error, not_implemented, {protocol_error, not_implemented,
"automatic acknowledgement not supported by stream queues ~ts", "automatic acknowledgement not supported by stream ~ts",
[rabbit_misc:rs(amqqueue:get_name(Q))]}; [rabbit_misc:rs(amqqueue:get_name(Q))]};
consume(Q, #{limiter_active := true}, _State) consume(Q, #{limiter_active := true}, _State)
when ?amqqueue_is_stream(Q) -> when ?amqqueue_is_stream(Q) ->
@ -284,7 +293,7 @@ consume(Q, Spec,
{LocalPid, QState} when is_pid(LocalPid) -> {LocalPid, QState} when is_pid(LocalPid) ->
#{no_ack := NoAck, #{no_ack := NoAck,
channel_pid := ChPid, channel_pid := ChPid,
prefetch_count := ConsumerPrefetchCount, mode := Mode,
consumer_tag := ConsumerTag, consumer_tag := ConsumerTag,
exclusive_consume := ExclusiveConsume, exclusive_consume := ExclusiveConsume,
args := Args, args := Args,
@ -303,22 +312,24 @@ consume(Q, Spec,
{protocol_error, precondition_failed, {protocol_error, precondition_failed,
"Filtering is not supported", []}; "Filtering is not supported", []};
_ -> _ ->
rabbit_core_metrics:consumer_created(ChPid, ConsumerTag, ConsumerPrefetchCount = case Mode of
ExclusiveConsume, {simple_prefetch, C} -> C;
not NoAck, QName, _ -> 0
ConsumerPrefetchCount, end,
false, up, Args), AckRequired = not NoAck,
rabbit_core_metrics:consumer_created(
ChPid, ConsumerTag, ExclusiveConsume, AckRequired,
QName, ConsumerPrefetchCount, false, up, Args),
%% reply needs to be sent before the stream %% reply needs to be sent before the stream
%% begins sending %% begins sending
maybe_send_reply(ChPid, OkMsg), maybe_send_reply(ChPid, OkMsg),
_ = rabbit_stream_coordinator:register_local_member_listener(Q), _ = rabbit_stream_coordinator:register_local_member_listener(Q),
begin_stream(QState, ConsumerTag, OffsetSpec, begin_stream(QState, ConsumerTag, OffsetSpec, Mode, AckRequired, FilterSpec)
ConsumerPrefetchCount, FilterSpec)
end end
end; end;
{undefined, _} -> {undefined, _} ->
{protocol_error, precondition_failed, {protocol_error, precondition_failed,
"queue '~ts' does not have a running replica on the local node", "stream ~ts does not have a running replica on the local node",
[rabbit_misc:rs(amqqueue:get_name(Q))]} [rabbit_misc:rs(amqqueue:get_name(Q))]}
end. end.
@ -405,7 +416,7 @@ query_local_pid(#stream_client{stream_id = StreamId} = State) ->
begin_stream(#stream_client{name = QName, begin_stream(#stream_client{name = QName,
readers = Readers0, readers = Readers0,
local_pid = LocalPid} = State, local_pid = LocalPid} = State,
Tag, Offset, Max, Options) Tag, Offset, Mode, AckRequired, Options)
when is_pid(LocalPid) -> when is_pid(LocalPid) ->
CounterSpec = {{?MODULE, QName, Tag, self()}, []}, CounterSpec = {{?MODULE, QName, Tag, self()}, []},
{ok, Seg0} = osiris:init_reader(LocalPid, Offset, CounterSpec, Options), {ok, Seg0} = osiris:init_reader(LocalPid, Offset, CounterSpec, Options),
@ -418,14 +429,22 @@ begin_stream(#stream_client{name = QName,
{timestamp, _} -> NextOffset; {timestamp, _} -> NextOffset;
_ -> Offset _ -> Offset
end, end,
Str0 = #stream{credit = Max, {DeliveryCount, Credit} = case Mode of
{simple_prefetch, N} ->
{none, N};
{credited, InitialDC} ->
{InitialDC, 0}
end,
Str0 = #stream{mode = Mode,
delivery_count = DeliveryCount,
credit = Credit,
ack = AckRequired,
start_offset = StartOffset, start_offset = StartOffset,
listening_offset = NextOffset, listening_offset = NextOffset,
last_consumed_offset = StartOffset,
log = Seg0, log = Seg0,
max = Max,
reader_options = Options}, reader_options = Options},
{ok, State#stream_client{local_pid = LocalPid, {ok, State#stream_client{readers = Readers0#{Tag => Str0}}}.
readers = Readers0#{Tag => Str0}}}.
cancel(_Q, ConsumerTag, OkMsg, ActingUser, #stream_client{readers = Readers0, cancel(_Q, ConsumerTag, OkMsg, ActingUser, #stream_client{readers = Readers0,
name = QName} = State) -> name = QName} = State) ->
@ -444,34 +463,54 @@ cancel(_Q, ConsumerTag, OkMsg, ActingUser, #stream_client{readers = Readers0,
{ok, State} {ok, State}
end. end.
credit(QName, CTag, Credit, Drain, #stream_client{readers = Readers0, -dialyzer({nowarn_function, credit_v1/5}).
name = Name, credit_v1(_, _, _, _, _) ->
local_pid = LocalPid} = State) -> erlang:error(credit_v1_unsupported).
case Readers0 of
#{CTag := #stream{credit = Credit0} = Str0} -> credit(QName, CTag, DeliveryCountRcv, LinkCreditRcv, Drain, Echo,
Str1 = Str0#stream{credit = Credit0 + Credit}, #stream_client{readers = Readers,
{Str, Msgs} = stream_entries(QName, Name, LocalPid, Str1), name = Name,
Actions = case Msgs of local_pid = LocalPid} = State0) ->
[] -> case Readers of
[{send_credit_reply, 0}]; #{CTag := Str0 = #stream{delivery_count = DeliveryCountSnd}} ->
_ -> LinkCreditSnd = serial_number:diff(
[{send_credit_reply, length(Msgs)}, serial_number:add(DeliveryCountRcv, LinkCreditRcv),
{deliver, CTag, true, Msgs}] DeliveryCountSnd),
Str1 = Str0#stream{credit = LinkCreditSnd},
{Str2 = #stream{delivery_count = DeliveryCount,
credit = Credit,
ack = Ack}, Msgs} = stream_entries(QName, Name, LocalPid, Str1),
DrainedInsufficientMsgs = Drain andalso Credit > 0,
Str = case DrainedInsufficientMsgs of
true ->
Str2#stream{delivery_count = serial_number:add(DeliveryCount, Credit),
credit = 0};
false ->
Str2
end,
DeliverActions = deliver_actions(CTag, Ack, Msgs),
State = State0#stream_client{readers = maps:update(CTag, Str, Readers)},
Actions = case Echo orelse DrainedInsufficientMsgs of
true ->
DeliverActions ++ [{credit_reply,
CTag,
Str#stream.delivery_count,
Str#stream.credit,
available_messages(Str),
Drain}];
false ->
DeliverActions
end, end,
case Drain of {State, Actions};
true ->
Readers = Readers0#{CTag => Str#stream{credit = 0}},
{State#stream_client{readers = Readers},
%% send_drained needs to come after deliver
Actions ++ [{send_drained, {CTag, Str#stream.credit}}]};
false ->
Readers = Readers0#{CTag => Str},
{State#stream_client{readers = Readers}, Actions}
end;
_ -> _ ->
{State, []} {State0, []}
end. end.
%% Returns only an approximation.
available_messages(#stream{log = Log,
last_consumed_offset = LastConsumedOffset}) ->
max(0, osiris_log:committed_offset(Log) - LastConsumedOffset).
deliver(QSs, Msg, Options) -> deliver(QSs, Msg, Options) ->
lists:foldl( lists:foldl(
fun({Q, stateless}, {Qs, Actions}) -> fun({Q, stateless}, {Qs, Actions}) ->
@ -500,7 +539,7 @@ deliver0(MsgId, Msg,
Correlation = case MsgId of Correlation = case MsgId of
undefined -> undefined ->
Correlation0; Correlation0;
_ when is_number(MsgId) -> _ ->
Correlation0#{Seq => {MsgId, Msg}} Correlation0#{Seq => {MsgId, Msg}}
end, end,
{Slow, Actions} = case maps:size(Correlation) >= SftLmt of {Slow, Actions} = case maps:size(Correlation) >= SftLmt of
@ -513,16 +552,21 @@ deliver0(MsgId, Msg,
correlation = Correlation, correlation = Correlation,
slow = Slow}, Actions}. slow = Slow}, Actions}.
stream_message(Msg, _FilteringSupported = true) -> stream_message(Msg, FilteringSupported) ->
MsgData = msg_to_iodata(Msg), McAmqp = mc:convert(mc_amqp, Msg),
case mc:x_header(<<"x-stream-filter-value">>, Msg) of Sections = mc:protocol_state(McAmqp),
undefined -> MsgData = mc_amqp:serialize(Sections),
MsgData; case FilteringSupported of
{utf8, Value} -> true ->
{Value, MsgData} case mc:x_header(<<"x-stream-filter-value">>, McAmqp) of
end; undefined ->
stream_message(Msg, _FilteringSupported = false) -> MsgData;
msg_to_iodata(Msg). {utf8, Value} ->
{Value, MsgData}
end;
false ->
MsgData
end.
-spec dequeue(_, _, _, _, client()) -> no_return(). -spec dequeue(_, _, _, _, client()) -> no_return().
dequeue(_, _, _, _, #stream_client{name = Name}) -> dequeue(_, _, _, _, #stream_client{name = Name}) ->
@ -530,43 +574,41 @@ dequeue(_, _, _, _, #stream_client{name = Name}) ->
[rabbit_misc:rs(Name)]}. [rabbit_misc:rs(Name)]}.
handle_event(_QName, {osiris_written, From, _WriterId, Corrs}, handle_event(_QName, {osiris_written, From, _WriterId, Corrs},
State = #stream_client{correlation = Correlation0, State0 = #stream_client{correlation = Correlation0,
soft_limit = SftLmt, soft_limit = SftLmt,
slow = Slow0, slow = Slow0,
name = Name}) -> name = Name}) ->
MsgIds = lists:sort(maps:fold( MsgIds = lists:sort(maps:fold(
fun (_Seq, {I, _M}, Acc) -> fun (_Seq, {I, _M}, Acc) ->
[I | Acc] [I | Acc]
end, [], maps:with(Corrs, Correlation0))), end, [], maps:with(Corrs, Correlation0))),
Correlation = maps:without(Corrs, Correlation0), Correlation = maps:without(Corrs, Correlation0),
{Slow, Actions} = case maps:size(Correlation) < SftLmt of {Slow, Actions0} = case maps:size(Correlation) < SftLmt of
true when Slow0 -> true when Slow0 ->
{false, [{unblock, Name}]}; {false, [{unblock, Name}]};
_ -> _ ->
{Slow0, []} {Slow0, []}
end, end,
{ok, State#stream_client{correlation = Correlation, Actions = case MsgIds of
slow = Slow}, [{settled, From, MsgIds} | Actions]}; [] -> Actions0;
[_|_] -> [{settled, From, MsgIds} | Actions0]
end,
State = State0#stream_client{correlation = Correlation,
slow = Slow},
{ok, State, Actions};
handle_event(QName, {osiris_offset, _From, _Offs}, handle_event(QName, {osiris_offset, _From, _Offs},
State = #stream_client{local_pid = LocalPid, State = #stream_client{local_pid = LocalPid,
readers = Readers0, readers = Readers0,
name = Name}) -> name = Name}) ->
Ack = true,
%% offset isn't actually needed as we use the atomic to read the %% offset isn't actually needed as we use the atomic to read the
%% current committed %% current committed
{Readers, Deliveries} = {Readers, Actions} = maps:fold(
maps:fold( fun (Tag, Str0, {Rds, As}) ->
fun (Tag, Str0, {Acc, TM}) -> {Str, Msgs} = stream_entries(QName, Name, LocalPid, Str0),
case stream_entries(QName, Name, LocalPid, Str0) of {Rds#{Tag => Str}, deliver_actions(Tag, Str#stream.ack, Msgs) ++ As}
{Str, []} -> end, {#{}, []}, Readers0),
{Acc#{Tag => Str}, TM}; {ok, State#stream_client{readers = Readers}, Actions};
{Str, Msgs} ->
{Acc#{Tag => Str},
[{deliver, Tag, Ack, Msgs} | TM]}
end
end, {#{}, []}, Readers0),
{ok, State#stream_client{readers = Readers}, Deliveries};
handle_event(_QName, {stream_leader_change, Pid}, State) -> handle_event(_QName, {stream_leader_change, Pid}, State) ->
{ok, update_leader_pid(Pid, State), []}; {ok, update_leader_pid(Pid, State), []};
handle_event(_QName, {stream_local_member_change, Pid}, handle_event(_QName, {stream_local_member_change, Pid},
@ -611,19 +653,22 @@ recover(_VHost, Queues) ->
end, {[], []}, Queues). end, {[], []}, Queues).
settle(QName, _, CTag, MsgIds, #stream_client{readers = Readers0, settle(QName, _, CTag, MsgIds, #stream_client{readers = Readers0,
local_pid = LocalPid, local_pid = LocalPid,
name = Name} = State) -> name = Name} = State) ->
%% all settle reasons will "give credit" to the stream queue case Readers0 of
Credit = length(MsgIds), #{CTag := #stream{mode = {simple_prefetch, _MaxCredit},
{Readers, Msgs} = case Readers0 of ack = Ack,
#{CTag := #stream{credit = Credit0} = Str0} -> credit = Credit0} = Str0} ->
Str1 = Str0#stream{credit = Credit0 + Credit}, %% all settle reasons will "give credit" to the stream queue
{Str, Msgs0} = stream_entries(QName, Name, LocalPid, Str1), Credit = length(MsgIds),
{Readers0#{CTag => Str}, Msgs0}; Str1 = Str0#stream{credit = Credit0 + Credit},
_ -> {Str, Msgs} = stream_entries(QName, Name, LocalPid, Str1),
{Readers0, []} Readers = maps:update(CTag, Str, Readers0),
end, {State#stream_client{readers = Readers},
{State#stream_client{readers = Readers}, [{deliver, CTag, true, Msgs}]}. deliver_actions(CTag, Ack, Msgs)};
_ ->
{State, []}
end.
info(Q, all_keys) -> info(Q, all_keys) ->
info(Q, ?INFO_KEYS); info(Q, ?INFO_KEYS);
@ -1064,72 +1109,164 @@ recover(Q) ->
maybe_send_reply(_ChPid, undefined) -> ok; maybe_send_reply(_ChPid, undefined) -> ok;
maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg). maybe_send_reply(ChPid, Msg) -> ok = rabbit_channel:send_command(ChPid, Msg).
stream_entries(QName, Name, LocalPid,
#stream{chunk_iterator = undefined,
credit = Credit} = Str0) ->
case Credit > 0 of
true ->
case chunk_iterator(Str0, LocalPid) of
{ok, Str} ->
stream_entries(QName, Name, LocalPid, Str);
{end_of_stream, Str} ->
{Str, []}
end;
false ->
{Str0, []}
end;
stream_entries(QName, Name, LocalPid,
#stream{delivery_count = DC,
credit = Credit,
buffer_msgs_rev = Buf0,
last_consumed_offset = LastOff} = Str0)
when Credit > 0 andalso Buf0 =/= [] ->
BufLen = length(Buf0),
case Credit =< BufLen of
true ->
%% Entire credit worth of messages can be served from the buffer.
{Buf, BufMsgsRev} = lists:split(BufLen - Credit, Buf0),
{Str0#stream{delivery_count = delivery_count_add(DC, Credit),
credit = 0,
buffer_msgs_rev = Buf,
last_consumed_offset = LastOff + Credit},
lists:reverse(BufMsgsRev)};
false ->
Str = Str0#stream{delivery_count = delivery_count_add(DC, BufLen),
credit = Credit - BufLen,
buffer_msgs_rev = [],
last_consumed_offset = LastOff + BufLen},
stream_entries(QName, Name, LocalPid, Str, Buf0)
end;
stream_entries(QName, Name, LocalPid, Str) -> stream_entries(QName, Name, LocalPid, Str) ->
stream_entries(QName, Name, LocalPid, Str, []). stream_entries(QName, Name, LocalPid, Str, []).
stream_entries(_, _, _, #stream{credit = Credit} = Str, Acc)
when Credit < 1 ->
{Str, lists:reverse(Acc)};
stream_entries(QName, Name, LocalPid, stream_entries(QName, Name, LocalPid,
#stream{credit = Credit, #stream{chunk_iterator = Iter0,
start_offset = StartOffs, delivery_count = DC,
listening_offset = LOffs, credit = Credit,
log = Seg0} = Str0, MsgIn) start_offset = StartOffset} = Str0, Acc0) ->
when Credit > 0 -> case osiris_log:iterator_next(Iter0) of
case osiris_log:read_chunk_parsed(Seg0) of end_of_chunk ->
{end_of_stream, Seg} -> case chunk_iterator(Str0, LocalPid) of
NextOffset = osiris_log:next_offset(Seg), {ok, Str} ->
case NextOffset > LOffs of stream_entries(QName, Name, LocalPid, Str, Acc0);
true -> {end_of_stream, Str} ->
osiris:register_offset_listener(LocalPid, NextOffset), {Str, lists:reverse(Acc0)}
{Str0#stream{log = Seg,
listening_offset = NextOffset}, MsgIn};
false ->
{Str0#stream{log = Seg}, MsgIn}
end; end;
{error, Err} -> {{Offset, Entry}, Iter} ->
rabbit_log:debug("stream client: error reading chunk ~w", [Err]), {Str, Acc} = case Entry of
exit(Err); {batch, _NumRecords, 0, _Len, BatchedEntries} ->
{Records, Seg} -> {MsgsRev, NumMsgs} = parse_uncompressed_subbatch(
Msgs = [begin BatchedEntries, Offset, StartOffset,
Msg0 = binary_to_msg(QName, B), QName, Name, LocalPid, {[], 0}),
Msg = mc:set_annotation(<<"x-stream-offset">>, O, Msg0), case Credit >= NumMsgs of
{Name, LocalPid, O, false, Msg} true ->
end || {O, B} <- Records, {Str0#stream{chunk_iterator = Iter,
O >= StartOffs], delivery_count = delivery_count_add(DC, NumMsgs),
credit = Credit - NumMsgs,
NumMsgs = length(Msgs), last_consumed_offset = Offset + NumMsgs - 1},
MsgsRev ++ Acc0};
Str = Str0#stream{credit = Credit - NumMsgs, false ->
log = Seg}, %% Consumer doesn't have sufficient credit.
case Str#stream.credit < 1 of %% Buffer the remaining messages.
true -> [] = Str0#stream.buffer_msgs_rev, % assertion
%% we are done here {Buf, MsgsRev1} = lists:split(NumMsgs - Credit, MsgsRev),
{Str, MsgIn ++ Msgs}; {Str0#stream{chunk_iterator = Iter,
false -> delivery_count = delivery_count_add(DC, Credit),
%% if there are fewer Msgs than Entries0 it means there were non-events credit = 0,
%% in the log and we should recurse and try again buffer_msgs_rev = Buf,
stream_entries(QName, Name, LocalPid, Str, MsgIn ++ Msgs) last_consumed_offset = Offset + Credit - 1},
end MsgsRev1 ++ Acc0}
end; end;
stream_entries(_QName, _Name, _LocalPid, Str, Msgs) -> {batch, _, _CompressionType, _, _} ->
{Str, Msgs}. %% Skip compressed sub batch.
%% It can only be consumed by Stream protocol clients.
binary_to_msg(#resource{kind = queue, {Str0#stream{chunk_iterator = Iter}, Acc0};
name = QName}, Data) -> _SimpleEntry ->
Mc0 = mc:init(mc_amqp, amqp10_framing:decode_bin(Data), #{}), case Offset >= StartOffset of
%% If exchange or routing_keys annotation isn't present the data most likely came true ->
%% from the rabbitmq-stream plugin so we'll choose defaults that simulate use Msg = entry_to_msg(Entry, Offset, QName, Name, LocalPid),
%% of the direct exchange. {Str0#stream{chunk_iterator = Iter,
Mc = case mc:exchange(Mc0) of delivery_count = delivery_count_add(DC, 1),
undefined -> mc:set_annotation(?ANN_EXCHANGE, <<>>, Mc0); credit = Credit - 1,
_ -> Mc0 last_consumed_offset = Offset},
end, [Msg | Acc0]};
case mc:routing_keys(Mc) of false ->
[] -> mc:set_annotation(?ANN_ROUTING_KEYS, [QName], Mc); {Str0#stream{chunk_iterator = Iter}, Acc0}
_ -> Mc end
end,
stream_entries(QName, Name, LocalPid, Str, Acc)
end. end.
msg_to_iodata(Msg0) -> chunk_iterator(#stream{credit = Credit,
Sections = mc:protocol_state(mc:convert(mc_amqp, Msg0)), listening_offset = LOffs,
mc_amqp:serialize(Sections). log = Log0} = Str0, LocalPid) ->
case osiris_log:chunk_iterator(Log0, Credit) of
{ok, _ChunkHeader, Iter, Log} ->
{ok, Str0#stream{chunk_iterator = Iter,
log = Log}};
{end_of_stream, Log} ->
NextOffset = osiris_log:next_offset(Log),
Str = case NextOffset > LOffs of
true ->
osiris:register_offset_listener(LocalPid, NextOffset),
Str0#stream{log = Log,
listening_offset = NextOffset};
false ->
Str0#stream{log = Log}
end,
{end_of_stream, Str};
{error, Err} ->
rabbit_log:info("stream client: failed to create chunk iterator ~p", [Err]),
exit(Err)
end.
%% Deliver each record of an uncompressed sub batch individually.
parse_uncompressed_subbatch(<<>>, _Offset, _StartOffset, _QName, _Name, _LocalPid, Acc) ->
Acc;
parse_uncompressed_subbatch(
<<0:1, %% simple entry
Len:31/unsigned,
Entry:Len/binary,
Rem/binary>>,
Offset, StartOffset, QName, Name, LocalPid, Acc0 = {AccList, AccCount}) ->
Acc = case Offset >= StartOffset of
true ->
Msg = entry_to_msg(Entry, Offset, QName, Name, LocalPid),
{[Msg | AccList], AccCount + 1};
false ->
Acc0
end,
parse_uncompressed_subbatch(Rem, Offset + 1, StartOffset, QName, Name, LocalPid, Acc).
entry_to_msg(Entry, Offset, #resource{kind = queue,
name = QName}, Name, LocalPid) ->
Mc0 = mc:init(mc_amqp, amqp10_framing:decode_bin(Entry), #{}),
%% If exchange or routing_keys annotation isn't present the entry most likely came
%% from the rabbitmq-stream plugin so we'll choose defaults that simulate use
%% of the direct exchange.
Mc1 = case mc:exchange(Mc0) of
undefined -> mc:set_annotation(?ANN_EXCHANGE, <<>>, Mc0);
_ -> Mc0
end,
Mc2 = case mc:routing_keys(Mc1) of
[] -> mc:set_annotation(?ANN_ROUTING_KEYS, [QName], Mc1);
_ -> Mc1
end,
Mc = mc:set_annotation(<<"x-stream-offset">>, Offset, Mc2),
{Name, LocalPid, Offset, false, Mc}.
capabilities() -> capabilities() ->
#{unsupported_policies => [%% Classic policies #{unsupported_policies => [%% Classic policies
@ -1146,7 +1283,7 @@ capabilities() ->
queue_arguments => [<<"x-max-length-bytes">>, <<"x-queue-type">>, queue_arguments => [<<"x-max-length-bytes">>, <<"x-queue-type">>,
<<"x-max-age">>, <<"x-stream-max-segment-size-bytes">>, <<"x-max-age">>, <<"x-stream-max-segment-size-bytes">>,
<<"x-initial-cluster-size">>, <<"x-queue-leader-locator">>], <<"x-initial-cluster-size">>, <<"x-queue-leader-locator">>],
consumer_arguments => [<<"x-stream-offset">>, <<"x-credit">>], consumer_arguments => [<<"x-stream-offset">>],
server_named => false}. server_named => false}.
notify_decorators(Q) when ?is_amqqueue(Q) -> notify_decorators(Q) when ?is_amqqueue(Q) ->
@ -1211,3 +1348,13 @@ get_nodes(Q) when ?is_amqqueue(Q) ->
is_minority(All, Up) -> is_minority(All, Up) ->
MinQuorum = length(All) div 2 + 1, MinQuorum = length(All) div 2 + 1,
length(Up) < MinQuorum. length(Up) < MinQuorum.
deliver_actions(_, _, []) ->
[];
deliver_actions(CTag, Ack, Msgs) ->
[{deliver, CTag, Ack, Msgs}].
delivery_count_add(none, _) ->
none;
delivery_count_add(Count, N) ->
serial_number:add(Count, N).

View File

@ -26,26 +26,24 @@
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
-spec init(rabbit_types:vhost()) -> state(). -spec init(rabbit_types:vhost()) -> state().
init(VHost) init(VHost)
when is_binary(VHost) -> when is_binary(VHost) ->
case enabled(VHost) of case enabled(VHost) of
false -> none; false ->
true -> {ok, X} = rabbit_exchange:lookup( none;
rabbit_misc:r(VHost, exchange, ?XNAME)), true ->
X {ok, X} = rabbit_exchange:lookup(rabbit_misc:r(VHost, exchange, ?XNAME)),
X
end. end.
-spec enabled(rabbit_types:vhost() | state()) -> boolean(). -spec enabled(rabbit_types:vhost() | state()) -> boolean().
enabled(VHost)
when is_binary(VHost) ->
{ok, VHosts} = application:get_env(rabbit, ?TRACE_VHOSTS),
lists:member(VHost, VHosts);
enabled(none) -> enabled(none) ->
false; false;
enabled(#exchange{}) -> enabled(#exchange{}) ->
true. true;
enabled(VHost)
when is_binary(VHost) ->
lists:member(VHost, vhosts_with_tracing_enabled()).
-spec tap_in(mc:state(), rabbit_exchange:route_return(), -spec tap_in(mc:state(), rabbit_exchange:route_return(),
binary(), rabbit_types:username(), state()) -> 'ok'. binary(), rabbit_types:username(), state()) -> 'ok'.
@ -55,7 +53,8 @@ tap_in(Msg, QNames, ConnName, Username, State) ->
-spec tap_in(mc:state(), rabbit_exchange:route_return(), -spec tap_in(mc:state(), rabbit_exchange:route_return(),
binary(), rabbit_channel:channel_number(), binary(), rabbit_channel:channel_number(),
rabbit_types:username(), state()) -> 'ok'. rabbit_types:username(), state()) -> 'ok'.
tap_in(_Msg, _QNames, _ConnName, _ChannelNum, _Username, none) -> ok; tap_in(_Msg, _QNames, _ConnName, _ChannelNum, _Username, none) ->
ok;
tap_in(Msg, QNames, ConnName, ChannelNum, Username, TraceX) -> tap_in(Msg, QNames, ConnName, ChannelNum, Username, TraceX) ->
XName = mc:exchange(Msg), XName = mc:exchange(Msg),
#exchange{name = #resource{virtual_host = VHost}} = TraceX, #exchange{name = #resource{virtual_host = VHost}} = TraceX,
@ -79,11 +78,15 @@ tap_out(Msg, ConnName, Username, State) ->
-spec tap_out(rabbit_amqqueue:qmsg(), binary(), -spec tap_out(rabbit_amqqueue:qmsg(), binary(),
rabbit_channel:channel_number(), rabbit_channel:channel_number(),
rabbit_types:username(), state()) -> 'ok'. rabbit_types:username(), state()) -> 'ok'.
tap_out(_Msg, _ConnName, _ChannelNum, _Username, none) -> ok; tap_out(_Msg, _ConnName, _ChannelNum, _Username, none) ->
ok;
tap_out({#resource{name = QName, virtual_host = VHost}, tap_out({#resource{name = QName, virtual_host = VHost},
_QPid, _QMsgId, Redelivered, Msg}, _QPid, _QMsgId, Redelivered, Msg},
ConnName, ChannelNum, Username, TraceX) -> ConnName, ChannelNum, Username, TraceX) ->
RedeliveredNum = case Redelivered of true -> 1; false -> 0 end, RedeliveredNum = case Redelivered of
true -> 1;
false -> 0
end,
trace(TraceX, Msg, <<"deliver">>, QName, trace(TraceX, Msg, <<"deliver">>, QName,
[{<<"redelivered">>, signedint, RedeliveredNum}, [{<<"redelivered">>, signedint, RedeliveredNum},
{<<"vhost">>, longstr, VHost}, {<<"vhost">>, longstr, VHost},
@ -94,28 +97,24 @@ tap_out({#resource{name = QName, virtual_host = VHost},
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
-spec start(rabbit_types:vhost()) -> 'ok'. -spec start(rabbit_types:vhost()) -> 'ok'.
start(VHost) start(VHost)
when is_binary(VHost) -> when is_binary(VHost) ->
case lists:member(VHost, vhosts_with_tracing_enabled()) of case enabled(VHost) of
true -> true ->
rabbit_log:info("Tracing is already enabled for vhost '~ts'", [VHost]), rabbit_log:info("Tracing is already enabled for vhost '~ts'", [VHost]),
ok; ok;
false -> false ->
rabbit_log:info("Enabling tracing for vhost '~ts'", [VHost]), rabbit_log:info("Enabling tracing for vhost '~ts'", [VHost]),
update_config(fun (VHosts) -> update_config(fun(VHosts) -> lists:usort([VHost | VHosts]) end)
lists:usort([VHost | VHosts])
end)
end. end.
-spec stop(rabbit_types:vhost()) -> 'ok'. -spec stop(rabbit_types:vhost()) -> 'ok'.
stop(VHost) stop(VHost)
when is_binary(VHost) -> when is_binary(VHost) ->
case lists:member(VHost, vhosts_with_tracing_enabled()) of case enabled(VHost) of
true -> true ->
rabbit_log:info("Disabling tracing for vhost '~ts'", [VHost]), rabbit_log:info("Disabling tracing for vhost '~ts'", [VHost]),
update_config(fun (VHosts) -> VHosts -- [VHost] end); update_config(fun(VHosts) -> VHosts -- [VHost] end);
false -> false ->
rabbit_log:info("Tracing is already disabled for vhost '~ts'", [VHost]), rabbit_log:info("Tracing is already disabled for vhost '~ts'", [VHost]),
ok ok
@ -125,17 +124,20 @@ update_config(Fun) ->
VHosts0 = vhosts_with_tracing_enabled(), VHosts0 = vhosts_with_tracing_enabled(),
VHosts = Fun(VHosts0), VHosts = Fun(VHosts0),
application:set_env(rabbit, ?TRACE_VHOSTS, VHosts), application:set_env(rabbit, ?TRACE_VHOSTS, VHosts),
Sessions = rabbit_amqp_session:list_local(),
NonAmqpPids = rabbit_networking:local_non_amqp_connections(), NonAmqpPids = rabbit_networking:local_non_amqp_connections(),
rabbit_log:debug("Will now refresh state of channels and of ~b non AMQP 0.9.1 " rabbit_log:debug("Refreshing state of channels, ~b sessions and ~b non "
"connections after virtual host tracing changes", "AMQP 0.9.1 connections after virtual host tracing changes...",
[length(NonAmqpPids)]), [length(Sessions), length(NonAmqpPids)]),
lists:foreach(fun(Pid) -> gen_server:cast(Pid, refresh_config) end, NonAmqpPids), Pids = Sessions ++ NonAmqpPids,
{Time, _} = timer:tc(fun rabbit_channel:refresh_config_local/0), lists:foreach(fun(Pid) -> gen_server:cast(Pid, refresh_config) end, Pids),
rabbit_log:debug("Refreshed channel state in ~fs", [Time/1_000_000]), {Time, ok} = timer:tc(fun rabbit_channel:refresh_config_local/0),
rabbit_log:debug("Refreshed channel states in ~fs", [Time / 1_000_000]),
ok. ok.
vhosts_with_tracing_enabled() -> vhosts_with_tracing_enabled() ->
application:get_env(rabbit, ?TRACE_VHOSTS, []). {ok, Vhosts} = application:get_env(rabbit, ?TRACE_VHOSTS),
Vhosts.
%%---------------------------------------------------------------------------- %%----------------------------------------------------------------------------
@ -148,9 +150,7 @@ trace(X, Msg0, RKPrefix, RKSuffix, Extra) ->
RoutingKeys = mc:routing_keys(Msg0), RoutingKeys = mc:routing_keys(Msg0),
%% for now convert into amqp legacy %% for now convert into amqp legacy
Msg = mc:prepare(read, mc:convert(mc_amqpl, Msg0)), Msg = mc:prepare(read, mc:convert(mc_amqpl, Msg0)),
%% check exchange name in case it is same as target #content{properties = Props} = Content0 = mc:protocol_state(Msg),
#content{properties = Props} = Content0 =
mc:protocol_state(Msg),
Key = <<RKPrefix/binary, ".", RKSuffix/binary>>, Key = <<RKPrefix/binary, ".", RKSuffix/binary>>,
Content = Content0#content{properties = Content = Content0#content{properties =
@ -159,26 +159,23 @@ trace(X, Msg0, RKPrefix, RKSuffix, Extra) ->
properties_bin = none}, properties_bin = none},
TargetXName = SourceXName#resource{name = ?XNAME}, TargetXName = SourceXName#resource{name = ?XNAME},
{ok, TraceMsg} = mc_amqpl:message(TargetXName, Key, Content), {ok, TraceMsg} = mc_amqpl:message(TargetXName, Key, Content),
ok = rabbit_queue_type:publish_at_most_once(X, TraceMsg), ok = rabbit_queue_type:publish_at_most_once(X, TraceMsg)
ok
end. end.
msg_to_table(XName, RoutingKeys, Props) -> msg_to_table(XName, RoutingKeys, Props) ->
{PropsTable, _Ix} = {PropsTable, _Ix} =
lists:foldl(fun (K, {L, Ix}) -> lists:foldl(fun(K, {L, Ix}) ->
V = element(Ix, Props), V = element(Ix, Props),
NewL = case V of NewL = case V of
undefined -> L; undefined -> L;
_ -> [{a2b(K), type(V), V} | L] _ -> [{atom_to_binary(K), type(V), V} | L]
end, end,
{NewL, Ix + 1} {NewL, Ix + 1}
end, {[], 2}, record_info(fields, 'P_basic')), end, {[], 2}, record_info(fields, 'P_basic')),
[{<<"exchange_name">>, longstr, XName}, [{<<"exchange_name">>, longstr, XName},
{<<"routing_keys">>, array, [{longstr, K} || K <- RoutingKeys]}, {<<"routing_keys">>, array, [{longstr, K} || K <- RoutingKeys]},
{<<"properties">>, table, PropsTable}, {<<"properties">>, table, PropsTable},
{<<"node">>, longstr, a2b(node())}]. {<<"node">>, longstr, atom_to_binary(node())}].
a2b(A) -> list_to_binary(atom_to_list(A)).
type(V) when is_list(V) -> table; type(V) when is_list(V) -> table;
type(V) when is_integer(V) -> signedint; type(V) when is_integer(V) -> signedint;

621
deps/rabbit/test/amqp_auth_SUITE.erl vendored Normal file
View File

@ -0,0 +1,621 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.
-module(amqp_auth_SUITE).
-compile([export_all,
nowarn_export_all]).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("amqp_client/include/amqp_client.hrl").
-include_lib("amqp10_common/include/amqp10_framing.hrl").
-import(rabbit_ct_broker_helpers,
[rpc/4]).
-import(rabbit_ct_helpers,
[eventually/1]).
-import(event_recorder,
[assert_event_type/2,
assert_event_prop/2]).
all() ->
[
{group, tests}
].
groups() ->
[
{tests, [shuffle],
[
attach_target_queue,
attach_source_exchange,
send_to_topic,
send_to_topic_using_subject,
attach_source_topic,
attach_target_internal_exchange,
authn_failure_event,
sasl_anonymous_success,
sasl_none_success,
sasl_plain_success,
sasl_anonymous_failure,
sasl_none_failure,
sasl_plain_failure,
vhost_absent,
vhost_connection_limit,
user_connection_limit,
vhost_queue_limit
]
}
].
init_per_suite(Config) ->
application:ensure_all_started(amqp10_client),
rabbit_ct_helpers:log_environment(),
Config.
end_per_suite(Config) ->
Config.
init_per_group(_Group, Config0) ->
Config = rabbit_ct_helpers:run_setup_steps(
Config0,
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps()),
Vhost = <<"test vhost">>,
User = <<"test user">>,
ok = rabbit_ct_broker_helpers:add_vhost(Config, Vhost),
ok = rabbit_ct_broker_helpers:add_user(Config, User),
[{test_vhost, Vhost},
{test_user, User}] ++ Config.
end_per_group(_Group, Config) ->
ok = rabbit_ct_broker_helpers:delete_user(Config, ?config(test_user, Config)),
ok = rabbit_ct_broker_helpers:delete_vhost(Config, ?config(test_vhost, Config)),
rabbit_ct_helpers:run_teardown_steps(
Config,
rabbit_ct_client_helpers:teardown_steps() ++
rabbit_ct_broker_helpers:teardown_steps()).
init_per_testcase(Testcase, Config) ->
ok = set_permissions(Config, <<>>, <<>>, <<"^some vhost permission">>),
rabbit_ct_helpers:testcase_started(Config, Testcase).
end_per_testcase(Testcase, Config) ->
delete_all_queues(Config),
ok = clear_permissions(Config),
rabbit_ct_helpers:testcase_finished(Config, Testcase).
attach_target_queue(Config) ->
QName = <<"test queue">>,
%% This target address means RabbitMQ will create a queue
%% requiring configure access on the queue.
%% We will also need write access to the default exchange to send to this queue.
TargetAddress = <<"/queue/", QName/binary>>,
OpnConf = connection_config(Config),
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session1} = amqp10_client:begin_session_sync(Connection),
{ok, _Sender1} = amqp10_client:attach_sender_link(
Session1, <<"test-sender-1">>, TargetAddress),
ExpectedErr1 = error_unauthorized(
<<"configure access to queue 'test queue' in vhost "
"'test vhost' refused for user 'test user'">>),
receive {amqp10_event, {session, Session1, {ended, ExpectedErr1}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
%% Give the user configure permissions on the queue.
ok = set_permissions(Config, QName, <<>>, <<>>),
{ok, Session2} = amqp10_client:begin_session_sync(Connection),
{ok, _Sender2} = amqp10_client:attach_sender_link(
Session2, <<"test-sender-2">>, TargetAddress),
ExpectedErr2 = error_unauthorized(
<<"write access to exchange 'amq.default' in vhost "
"'test vhost' refused for user 'test user'">>),
receive {amqp10_event, {session, Session2, {ended, ExpectedErr2}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
%% Give the user configure permissions on the queue and
%% write access to the default exchange.
ok = set_permissions(Config, QName, <<"amq\.default">>, <<>>),
{ok, Session3} = amqp10_client:begin_session_sync(Connection),
{ok, Sender3} = amqp10_client:attach_sender_link(
Session3, <<"test-sender-3">>, TargetAddress),
receive {amqp10_event, {link, Sender3, attached}} -> ok
after 5000 -> flush(missing_attached),
ct:fail("missing ATTACH from server")
end,
ok = close_connection_sync(Connection).
attach_source_exchange(Config) ->
%% This source address means RabbitMQ will create a queue with a generated name
%% prefixed with amq.gen requiring configure access on the queue.
%% The queue is bound to the fanout exchange requiring write access on the queue
%% and read access on the fanout exchange.
%% To consume from the queue, we will also need read access on the queue.
SourceAddress = <<"/exchange/amq.fanout/ignored">>,
OpnConf = connection_config(Config),
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session1} = amqp10_client:begin_session_sync(Connection),
{ok, _Recv1} = amqp10_client:attach_receiver_link(
Session1, <<"receiver-1">>, SourceAddress),
receive
{amqp10_event,
{session, Session1,
{ended,
#'v1_0.error'{
condition = ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
description = {utf8, <<"configure access to queue 'amq.gen", _/binary>>}}}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
%% Give the user configure permissions on the queue.
ok = set_permissions(Config, <<"^amq\.gen">>, <<>>, <<>>),
{ok, Session2} = amqp10_client:begin_session_sync(Connection),
{ok, _Recv2} = amqp10_client:attach_receiver_link(
Session2, <<"receiver-2">>, SourceAddress),
receive
{amqp10_event,
{session, Session2,
{ended,
#'v1_0.error'{
condition = ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
description = {utf8, <<"write access to queue 'amq.gen", _/binary>>}}}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
%% Give the user configure and write permissions on the queue.
ok = set_permissions(Config, <<"^amq\.gen">>, <<"^amq\.gen">>, <<>>),
{ok, Session3} = amqp10_client:begin_session_sync(Connection),
{ok, _Recv3} = amqp10_client:attach_receiver_link(
Session3, <<"receiver-3">>, SourceAddress),
ExpectedErr1 = error_unauthorized(
<<"read access to exchange 'amq.fanout' in vhost "
"'test vhost' refused for user 'test user'">>),
receive {amqp10_event, {session, Session3, {ended, ExpectedErr1}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
%% Give the user configure and write permissions on the queue, and read access on the exchange.
ok = set_permissions(Config, <<"^amq\.gen">>, <<"^amq\.gen">>, <<"amq\.fanout">>),
{ok, Session4} = amqp10_client:begin_session_sync(Connection),
{ok, _Recv4} = amqp10_client:attach_receiver_link(
Session4, <<"receiver-4">>, SourceAddress),
receive
{amqp10_event,
{session, Session4,
{ended,
#'v1_0.error'{
condition = ?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS,
description = {utf8, <<"read access to queue 'amq.gen", _/binary>>}}}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
%% Give the user configure, write, and read permissions on the queue,
%% and read access on the exchange.
ok = set_permissions(Config, <<"^amq\.gen">>, <<"^amq\.gen">>, <<"^(amq\.gen|amq\.fanout)">>),
{ok, Session5} = amqp10_client:begin_session_sync(Connection),
{ok, Recv5} = amqp10_client:attach_receiver_link(
Session5, <<"receiver-5">>, SourceAddress),
receive {amqp10_event, {link, Recv5, attached}} -> ok
after 5000 -> flush(missing_attached),
ct:fail("missing ATTACH from server")
end,
ok = close_connection_sync(Connection).
send_to_topic(Config) ->
TargetAddresses = [<<"/topic/test vhost.test user.a.b">>,
<<"/exchange/amq.topic/test vhost.test user.a.b">>],
lists:foreach(fun(Address) ->
ok = send_to_topic0(Address, Config)
end, TargetAddresses).
send_to_topic0(TargetAddress, Config) ->
User = ?config(test_user, Config),
Vhost = ?config(test_vhost, Config),
ok = rabbit_ct_broker_helpers:set_full_permissions(Config, User, Vhost),
ok = set_topic_permissions(Config, <<"amq.topic">>, <<"^$">>, <<"^$">>),
OpnConf = connection_config(Config),
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session1} = amqp10_client:begin_session_sync(Connection),
{ok, Sender1} = amqp10_client:attach_sender_link_sync(
Session1, <<"sender-1">>, TargetAddress),
ok = wait_for_credit(Sender1),
Msg1 = amqp10_msg:new(<<255>>, <<1>>, true),
ok = amqp10_client:send_msg(Sender1, Msg1),
ExpectedErr = error_unauthorized(
<<"write access to topic 'test vhost.test user.a.b' in exchange "
"'amq.topic' in vhost 'test vhost' refused for user 'test user'">>),
receive {amqp10_event, {session, Session1, {ended, ExpectedErr}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
ok = set_topic_permissions(Config, <<"amq.topic">>, <<"^{vhost}\.{username}\.a\.b$">>, <<"^$">>),
{ok, Session2} = amqp10_client:begin_session_sync(Connection),
{ok, Sender2} = amqp10_client:attach_sender_link_sync(
Session2, <<"sender-2">>, TargetAddress),
ok = wait_for_credit(Sender2),
Dtag = <<0, 0>>,
Msg2 = amqp10_msg:new(Dtag, <<2>>, false),
ok = amqp10_client:send_msg(Sender2, Msg2),
%% We expect RELEASED since no queue is bound.
receive {amqp10_disposition, {released, Dtag}} -> ok
after 5000 -> ct:fail(released_timeout)
end,
ok = amqp10_client:detach_link(Sender2),
ok = close_connection_sync(Connection).
send_to_topic_using_subject(Config) ->
TargetAddress = <<"/exchange/amq.topic">>,
User = ?config(test_user, Config),
Vhost = ?config(test_vhost, Config),
ok = rabbit_ct_broker_helpers:set_full_permissions(Config, User, Vhost),
ok = set_topic_permissions(Config, <<"amq.topic">>, <<"^\.a$">>, <<"^$">>),
OpnConf = connection_config(Config),
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session} = amqp10_client:begin_session_sync(Connection),
{ok, Sender} = amqp10_client:attach_sender_link_sync(
Session, <<"sender">>, TargetAddress),
ok = wait_for_credit(Sender),
Dtag1 = <<"dtag 1">>,
Msg1a = amqp10_msg:new(Dtag1, <<"m1">>, false),
Msg1b = amqp10_msg:set_properties(#{subject => <<".a">>}, Msg1a),
ok = amqp10_client:send_msg(Sender, Msg1b),
%% We have sufficient authorization, but expect RELEASED since no queue is bound.
receive {amqp10_disposition, {released, Dtag1}} -> ok
after 5000 -> ct:fail(released_timeout)
end,
Dtag2 = <<"dtag 2">>,
Msg2a = amqp10_msg:new(Dtag2, <<"m2">>, false),
%% We don't have sufficient authorization.
Msg2b = amqp10_msg:set_properties(#{subject => <<".a.b">>}, Msg2a),
ok = amqp10_client:send_msg(Sender, Msg2b),
ExpectedErr = error_unauthorized(
<<"write access to topic '.a.b' in exchange 'amq.topic' in "
"vhost 'test vhost' refused for user 'test user'">>),
receive {amqp10_event, {session, Session, {ended, ExpectedErr}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
ok = close_connection_sync(Connection).
attach_source_topic(Config) ->
%% These source addresses mean RabbitMQ will bind a queue to the default topic
%% exchange with binding key 'test vhost.test user.a.b'.
%% Therefore, we need read access to that topic.
%% We also test variable expansion in topic permission patterns.
SourceAddresses = [<<"/topic/test vhost.test user.a.b">>,
<<"/exchange/amq.topic/test vhost.test user.a.b">>],
lists:foreach(fun(Address) ->
ok = attach_source_topic0(Address, Config)
end, SourceAddresses).
attach_source_topic0(SourceAddress, Config) ->
User = ?config(test_user, Config),
Vhost = ?config(test_vhost, Config),
ok = rabbit_ct_broker_helpers:set_full_permissions(Config, User, Vhost),
ok = set_topic_permissions(Config, <<"amq.topic">>, <<"^$">>, <<"^$">>),
OpnConf = connection_config(Config),
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session1} = amqp10_client:begin_session_sync(Connection),
{ok, _Recv1} = amqp10_client:attach_receiver_link(
Session1, <<"receiver-1">>, SourceAddress),
ExpectedErr = error_unauthorized(
<<"read access to topic 'test vhost.test user.a.b' in exchange "
"'amq.topic' in vhost 'test vhost' refused for user 'test user'">>),
receive {amqp10_event, {session, Session1, {ended, ExpectedErr}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
ok = set_topic_permissions(Config, <<"amq.topic">>, <<"^$">>, <<"^{vhost}\.{username}\.a\.b$">>),
{ok, Session2} = amqp10_client:begin_session_sync(Connection),
{ok, Recv2} = amqp10_client:attach_receiver_link(
Session2, <<"receiver-2">>, SourceAddress),
receive {amqp10_event, {link, Recv2, attached}} -> ok
after 5000 -> flush(missing_attached),
ct:fail("missing ATTACH from server")
end,
ok = close_connection_sync(Connection).
attach_target_internal_exchange(Config) ->
XName = <<"test exchange">>,
Ch = rabbit_ct_client_helpers:open_channel(Config),
#'exchange.declare_ok'{} = amqp_channel:call(Ch, #'exchange.declare'{internal = true,
exchange = XName}),
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := anon},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session} = amqp10_client:begin_session_sync(Connection),
Address = <<"/exchange/", XName/binary, "/some-routing-key">>,
{ok, _} = amqp10_client:attach_sender_link(
Session, <<"test-sender">>, Address),
ExpectedErr = error_unauthorized(
<<"attach to internal exchange 'test exchange' in vhost '/' is forbidden">>),
receive {amqp10_event, {session, Session, {ended, ExpectedErr}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive AMQP_ERROR_UNAUTHORIZED_ACCESS")
end,
ok = amqp10_client:close_connection(Connection),
#'exchange.delete_ok'{} = amqp_channel:call(Ch, #'exchange.delete'{exchange = XName}),
ok = rabbit_ct_client_helpers:close_channel(Ch).
authn_failure_event(Config) ->
ok = event_recorder:start(Config),
Host = ?config(rmq_hostname, Config),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
Vhost = ?config(test_vhost, Config),
User = ?config(test_user, Config),
OpnConf = #{address => Host,
port => Port,
container_id => <<"my container">>,
sasl => {plain, User, <<"wrong password">>},
hostname => <<"vhost:", Vhost/binary>>},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, Connection, {closed, sasl_auth_failure}}} -> ok
after 5000 -> flush(missing_closed),
ct:fail("did not receive sasl_auth_failure")
end,
[E | _] = event_recorder:get_events(Config),
ok = event_recorder:stop(Config),
assert_event_type(user_authentication_failure, E),
assert_event_prop([{name, <<"test user">>},
{auth_mechanism, <<"PLAIN">>},
{ssl, false},
{protocol, {1, 0}}],
E).
sasl_anonymous_success(Config) ->
Mechanism = anon,
ok = sasl_success(Mechanism, Config).
sasl_none_success(Config) ->
Mechanism = none,
ok = sasl_success(Mechanism, Config).
sasl_plain_success(Config) ->
Mechanism = {plain, <<"guest">>, <<"guest">>},
ok = sasl_success(Mechanism, Config).
sasl_success(Mechanism, Config) ->
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := Mechanism},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, Connection, opened}} -> ok
after 5000 -> ct:fail(missing_opened)
end,
ok = amqp10_client:close_connection(Connection).
sasl_anonymous_failure(Config) ->
Mechanism = anon,
?assertEqual(
{sasl_not_supported, Mechanism},
sasl_failure(Mechanism, Config)
).
sasl_none_failure(Config) ->
Mechanism = none,
sasl_failure(Mechanism, Config).
sasl_plain_failure(Config) ->
Mechanism = {plain, <<"guest">>, <<"wrong password">>},
?assertEqual(
sasl_auth_failure,
sasl_failure(Mechanism, Config)
).
sasl_failure(Mechanism, Config) ->
App = rabbit,
Par = amqp1_0_default_user,
{ok, Default} = rpc(Config, application, get_env, [App, Par]),
ok = rpc(Config, application, set_env, [App, Par, none]),
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := Mechanism},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
Reason = receive {amqp10_event, {connection, Connection, {closed, Reason0}}} -> Reason0
after 5000 -> ct:fail(missing_closed)
end,
ok = rpc(Config, application, set_env, [App, Par, Default]),
Reason.
vhost_absent(Config) ->
OpnConf = connection_config(Config, <<"vhost does not exist">>),
{ok, Connection} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, Connection, {closed, _}}} -> ok
after 5000 -> ct:fail(missing_closed)
end.
vhost_connection_limit(Config) ->
Vhost = proplists:get_value(test_vhost, Config),
ok = rabbit_ct_broker_helpers:set_vhost_limit(Config, 0, Vhost, max_connections, 1),
OpnConf = connection_config(Config),
{ok, C1} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, C1, opened}} -> ok
after 5000 -> ct:fail({missing_event, ?LINE})
end,
{ok, C2} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, C2, {closed, _}}} -> ok
after 5000 -> ct:fail({missing_event, ?LINE})
end,
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf1 = OpnConf0#{sasl := anon},
{ok, C3} = amqp10_client:open_connection(OpnConf1),
receive {amqp10_event, {connection, C3, opened}} -> ok
after 5000 -> ct:fail({missing_event, ?LINE})
end,
{ok, C4} = amqp10_client:open_connection(OpnConf1),
receive {amqp10_event, {connection, C4, opened}} -> ok
after 5000 -> ct:fail({missing_event, ?LINE})
end,
[ok = close_connection_sync(C) || C <- [C1, C3, C4]],
ok = rabbit_ct_broker_helpers:clear_vhost_limit(Config, 0, Vhost).
user_connection_limit(Config) ->
DefaultUser = <<"guest">>,
Limit = max_connections,
ok = rabbit_ct_broker_helpers:set_user_limits(Config, DefaultUser, #{Limit => 0}),
OpnConf0 = connection_config(Config, <<"/">>),
OpnConf = OpnConf0#{sasl := anon},
{ok, C1} = amqp10_client:open_connection(OpnConf),
receive {amqp10_event, {connection, C1, {closed, _}}} -> ok
after 5000 -> ct:fail({missing_event, ?LINE})
end,
{ok, C2} = amqp10_client:open_connection(connection_config(Config)),
receive {amqp10_event, {connection, C2, opened}} -> ok
after 5000 -> ct:fail({missing_event, ?LINE})
end,
ok = close_connection_sync(C2),
ok = rabbit_ct_broker_helpers:clear_user_limits(Config, DefaultUser, Limit).
vhost_queue_limit(Config) ->
Vhost = proplists:get_value(test_vhost, Config),
ok = rabbit_ct_broker_helpers:set_vhost_limit(Config, 0, Vhost, max_queues, 0),
QName = <<"q1">>,
ok = set_permissions(Config, QName, <<>>, <<>>),
OpnConf1 = connection_config(Config),
{ok, C1} = amqp10_client:open_connection(OpnConf1),
{ok, Session1} = amqp10_client:begin_session_sync(C1),
TargetAddress = <<"/queue/", QName/binary>>,
{ok, _Sender1} = amqp10_client:attach_sender_link(
Session1, <<"test-sender-1">>, TargetAddress),
ExpectedErr = amqp_error(
?V_1_0_AMQP_ERROR_RESOURCE_LIMIT_EXCEEDED,
<<"cannot declare queue 'q1' in vhost 'test vhost': vhost queue limit (0) is reached">>),
receive {amqp10_event, {session, Session1, {ended, ExpectedErr}}} -> ok
after 5000 -> flush(missing_ended),
ct:fail("did not receive expected error")
end,
OpnConf2 = connection_config(Config, <<"/">>),
OpnConf3 = OpnConf2#{sasl := anon},
{ok, C2} = amqp10_client:open_connection(OpnConf3),
{ok, Session2} = amqp10_client:begin_session_sync(C2),
{ok, Sender2} = amqp10_client:attach_sender_link(
Session2, <<"test-sender-2">>, TargetAddress),
receive {amqp10_event, {link, Sender2, attached}} -> ok
after 5000 -> flush(missing_attached),
ct:fail("missing ATTACH from server")
end,
ok = close_connection_sync(C1),
ok = close_connection_sync(C2),
ok = rabbit_ct_broker_helpers:clear_vhost_limit(Config, 0, Vhost).
connection_config(Config) ->
Vhost = ?config(test_vhost, Config),
connection_config(Config, Vhost).
connection_config(Config, Vhost) ->
Host = ?config(rmq_hostname, Config),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
User = Password = ?config(test_user, Config),
#{address => Host,
port => Port,
container_id => <<"my container">>,
sasl => {plain, User, Password},
hostname => <<"vhost:", Vhost/binary>>}.
set_permissions(Config, ConfigurePerm, WritePerm, ReadPerm) ->
ok = rabbit_ct_broker_helpers:set_permissions(Config,
?config(test_user, Config),
?config(test_vhost, Config),
ConfigurePerm,
WritePerm,
ReadPerm).
set_topic_permissions(Config, Exchange, WritePat, ReadPat) ->
ok = rpc(Config,
rabbit_auth_backend_internal,
set_topic_permissions,
[?config(test_user, Config),
?config(test_vhost, Config),
Exchange,
WritePat,
ReadPat,
<<"acting-user">>]).
clear_permissions(Config) ->
User = ?config(test_user, Config),
Vhost = ?config(test_vhost, Config),
ok = rabbit_ct_broker_helpers:clear_permissions(Config, User, Vhost),
ok = rpc(Config,
rabbit_auth_backend_internal,
clear_topic_permissions,
[User, Vhost, <<"acting-user">>]).
error_unauthorized(Description) ->
amqp_error(?V_1_0_AMQP_ERROR_UNAUTHORIZED_ACCESS, Description).
amqp_error(Condition, Description)
when is_binary(Description) ->
#'v1_0.error'{
condition = Condition,
description = {utf8, Description}}.
% before we can send messages we have to wait for credit from the server
wait_for_credit(Sender) ->
receive
{amqp10_event, {link, Sender, credited}} ->
flush(?FUNCTION_NAME),
ok
after 5000 ->
flush("wait_for_credit timed out"),
ct:fail(credited_timeout)
end.
flush(Prefix) ->
receive Msg ->
ct:pal("~ts flushed: ~p~n", [Prefix, Msg]),
flush(Prefix)
after 1 ->
ok
end.
delete_all_queues(Config) ->
Qs = rpc(Config, rabbit_amqqueue, list, []),
[{ok, _QLen} = rpc(Config, rabbit_amqqueue, delete, [Q, false, false, <<"fake-user">>])
|| Q <- Qs].
close_connection_sync(Connection)
when is_pid(Connection) ->
ok = amqp10_client:close_connection(Connection),
receive {amqp10_event, {connection, Connection, {closed, normal}}} -> ok
after 5000 -> flush(missing_closed),
ct:fail("missing CLOSE from server")
end.

3654
deps/rabbit/test/amqp_client_SUITE.erl vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,221 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2016-2023 VMware, Inc. or its affiliates. All rights reserved.
-module(amqp_credit_api_v2_SUITE).
-compile([export_all, nowarn_export_all]).
-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("amqp_client/include/amqp_client.hrl").
all() ->
[
{group, cluster_size_1}
].
groups() ->
[
{cluster_size_1, [],
[credit_api_v2]}
].
suite() ->
[
{timetrap, {minutes, 10}}
].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(amqp10_client),
rabbit_ct_helpers:log_environment(),
rabbit_ct_helpers:run_setup_steps(Config, []).
end_per_suite(Config) ->
rabbit_ct_helpers:run_teardown_steps(Config).
init_per_group(_Group, Config0) ->
Config = rabbit_ct_helpers:merge_app_env(
Config0, {rabbit, [{forced_feature_flags_on_init, []}]}),
rabbit_ct_helpers:run_steps(Config,
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps()).
end_per_group(_Group, Config) ->
rabbit_ct_helpers:run_steps(Config,
rabbit_ct_client_helpers:teardown_steps() ++
rabbit_ct_broker_helpers:teardown_steps()).
init_per_testcase(TestCase, Config) ->
case rabbit_ct_broker_helpers:is_feature_flag_supported(Config, TestCase) of
true ->
?assertNot(rabbit_ct_broker_helpers:is_feature_flag_enabled(Config, TestCase)),
Config;
false ->
{skip, io_lib:format("feature flag ~s is unsupported", [TestCase])}
end.
end_per_testcase(_TestCase, Config) ->
Config.
credit_api_v2(Config) ->
CQ = <<"classic queue">>,
QQ = <<"quorum queue">>,
CQAddr = <<"/amq/queue/", CQ/binary>>,
QQAddr = <<"/amq/queue/", QQ/binary>>,
Ch = rabbit_ct_client_helpers:open_channel(Config),
#'queue.declare_ok'{} = amqp_channel:call(Ch, #'queue.declare'{queue = CQ}),
#'queue.declare_ok'{} = amqp_channel:call(
Ch, #'queue.declare'{
queue = QQ,
durable = true,
arguments = [{<<"x-queue-type">>, longstr, <<"quorum">>}]}),
ok = rabbit_ct_client_helpers:close_channel(Ch),
Host = ?config(rmq_hostname, Config),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
OpnConf = #{address => Host,
port => Port,
container_id => <<"my container">>,
sasl => {plain, <<"guest">>, <<"guest">>}},
{ok, Connection} = amqp10_client:open_connection(OpnConf),
{ok, Session} = amqp10_client:begin_session_sync(Connection),
{ok, CQSender} = amqp10_client:attach_sender_link(Session, <<"cq sender">>, CQAddr),
{ok, QQSender} = amqp10_client:attach_sender_link(Session, <<"qq sender">>, QQAddr),
receive {amqp10_event, {link, CQSender, credited}} -> ok
after 5000 -> ct:fail(credited_timeout)
end,
receive {amqp10_event, {link, QQSender, credited}} -> ok
after 5000 -> ct:fail(credited_timeout)
end,
%% Send 40 messages to each queue.
NumMsgs = 40,
[begin
Bin = integer_to_binary(N),
ok = amqp10_client:send_msg(CQSender, amqp10_msg:new(Bin, Bin, true)),
ok = amqp10_client:send_msg(QQSender, amqp10_msg:new(Bin, Bin, true))
end || N <- lists:seq(1, NumMsgs)],
ok = amqp10_client:detach_link(CQSender),
ok = amqp10_client:detach_link(QQSender),
%% Consume with credit API v1
CQAttachArgs = #{handle => 300,
name => <<"cq receiver 1">>,
role => {receiver, #{address => CQAddr,
durable => configuration}, self()},
snd_settle_mode => unsettled,
rcv_settle_mode => first,
filter => #{}},
{ok, CQReceiver1} = amqp10_client:attach_link(Session, CQAttachArgs),
QQAttachArgs = #{handle => 400,
name => <<"qq receiver 1">>,
role => {receiver, #{address => QQAddr,
durable => configuration}, self()},
snd_settle_mode => unsettled,
rcv_settle_mode => first,
filter => #{}},
{ok, QQReceiver1} = amqp10_client:attach_link(Session, QQAttachArgs),
ok = consume_and_accept(10, CQReceiver1, Session),
ok = consume_and_accept(10, QQReceiver1, Session),
?assertEqual(ok,
rabbit_ct_broker_helpers:enable_feature_flag(Config, ?FUNCTION_NAME)),
flush(enabled_feature_flag),
%% Consume with credit API v2
{ok, CQReceiver2} = amqp10_client:attach_receiver_link(
Session, <<"cq receiver 2">>, CQAddr, unsettled),
{ok, QQReceiver2} = amqp10_client:attach_receiver_link(
Session, <<"qq receiver 2">>, QQAddr, unsettled),
ok = consume_and_accept(10, CQReceiver2, Session),
ok = consume_and_accept(10, QQReceiver2, Session),
%% Consume via with credit API v1
ok = consume_and_accept(10, CQReceiver1, Session),
ok = consume_and_accept(10, QQReceiver1, Session),
%% Detach the credit API v1 links and attach with the same output handle.
ok = detach_sync(CQReceiver1),
ok = detach_sync(QQReceiver1),
{ok, CQReceiver3} = amqp10_client:attach_link(Session, CQAttachArgs),
{ok, QQReceiver3} = amqp10_client:attach_link(Session, QQAttachArgs),
%% The new links should use credit API v2
ok = consume_and_accept(10, CQReceiver3, Session),
ok = consume_and_accept(10, QQReceiver3, Session),
flush(pre_drain),
%% Draining should also work.
ok = amqp10_client:flow_link_credit(CQReceiver3, 10, never, true),
receive {amqp10_event, {link, CQReceiver3, credit_exhausted}} -> ok
after 5000 -> ct:fail({missing_credit_exhausted, ?LINE})
end,
receive Unexpected1 -> ct:fail({unexpected, ?LINE, Unexpected1})
after 20 -> ok
end,
ok = amqp10_client:flow_link_credit(QQReceiver3, 10, never, true),
receive {amqp10_event, {link, QQReceiver3, credit_exhausted}} -> ok
after 5000 -> ct:fail({missing_credit_exhausted, ?LINE})
end,
receive Unexpected2 -> ct:fail({unexpected, ?LINE, Unexpected2})
after 20 -> ok
end,
ok = detach_sync(CQReceiver2),
ok = detach_sync(QQReceiver2),
ok = detach_sync(CQReceiver3),
ok = detach_sync(QQReceiver3),
ok = amqp10_client:end_session(Session),
receive {amqp10_event, {session, Session, {ended, _}}} -> ok
after 5000 -> ct:fail(missing_ended)
end,
ok = amqp10_client:close_connection(Connection),
receive {amqp10_event, {connection, Connection, {closed, normal}}} -> ok
after 5000 -> ct:fail(missing_closed)
end.
consume_and_accept(NumMsgs, Receiver, Session) ->
ok = amqp10_client:flow_link_credit(Receiver, NumMsgs, never),
Msgs = receive_messages(Receiver, NumMsgs),
ok = amqp10_client_session:disposition(
Session,
receiver,
amqp10_msg:delivery_id(hd(Msgs)),
amqp10_msg:delivery_id(lists:last(Msgs)),
true,
accepted).
receive_messages(Receiver, N) ->
receive_messages0(Receiver, N, []).
receive_messages0(_Receiver, 0, Acc) ->
lists:reverse(Acc);
receive_messages0(Receiver, N, Acc) ->
receive
{amqp10_msg, Receiver, Msg} ->
receive_messages0(Receiver, N - 1, [Msg | Acc])
after 5000 ->
exit({timeout, {num_received, length(Acc)}, {num_missing, N}})
end.
detach_sync(Receiver) ->
ok = amqp10_client:detach_link(Receiver),
receive {amqp10_event, {link, Receiver, {detached, normal}}} -> ok
after 5000 -> ct:fail({missing_detached, Receiver})
end.
flush(Prefix) ->
receive
Msg ->
ct:pal("~ts flushed: ~p~n", [Prefix, Msg]),
flush(Prefix)
after 1 ->
ok
end.

View File

@ -5,88 +5,83 @@
%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. %% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%% %%
-module(proxy_protocol_SUITE). -module(amqp_proxy_protocol_SUITE).
-include_lib("common_test/include/ct.hrl"). -compile([export_all, nowarn_export_all]).
-compile(export_all). -include_lib("eunit/include/eunit.hrl").
-import(rabbit_ct_helpers, [eventually/3]).
-import(rabbit_ct_broker_helpers, [rpc/4]).
-define(TIMEOUT, 5000). -define(TIMEOUT, 5000).
all() -> all() ->
[ [{group, tests}].
{group, sequential_tests}
].
groups() -> [ groups() ->
{sequential_tests, [], [ [{tests, [shuffle],
proxy_protocol_v1, [
proxy_protocol_v1_tls, v1,
proxy_protocol_v2_local v1_tls,
]} v2_local
]}
]. ].
init_per_suite(Config) -> init_per_suite(Config) ->
rabbit_ct_helpers:log_environment(), rabbit_ct_helpers:log_environment(),
Config1 = rabbit_ct_helpers:set_config(Config, [ Config1 = rabbit_ct_helpers:set_config(
{rmq_nodename_suffix, ?MODULE} Config,
]), [{rmq_nodename_suffix, ?MODULE},
Config2 = rabbit_ct_helpers:merge_app_env(Config1, [ {rabbitmq_ct_tls_verify, verify_none}]),
{rabbit, [ Config2 = rabbit_ct_helpers:merge_app_env(
{proxy_protocol, true} Config1,
]} [{rabbit, [{proxy_protocol, true}]}]),
]), rabbit_ct_helpers:run_setup_steps(
Config3 = rabbit_ct_helpers:set_config(Config2, {rabbitmq_ct_tls_verify, verify_none}), Config2,
rabbit_ct_helpers:run_setup_steps(Config3, rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_broker_helpers:setup_steps() ++ rabbit_ct_client_helpers:setup_steps()).
rabbit_ct_client_helpers:setup_steps()).
end_per_suite(Config) -> end_per_suite(Config) ->
rabbit_ct_helpers:run_teardown_steps(Config, rabbit_ct_helpers:run_teardown_steps(Config,
rabbit_ct_client_helpers:teardown_steps() ++ rabbit_ct_client_helpers:teardown_steps() ++
rabbit_ct_broker_helpers:teardown_steps()). rabbit_ct_broker_helpers:teardown_steps()).
init_per_group(_, Config) -> Config.
end_per_group(_, Config) -> Config.
init_per_testcase(Testcase, Config) -> init_per_testcase(Testcase, Config) ->
rabbit_ct_helpers:testcase_started(Config, Testcase). rabbit_ct_helpers:testcase_started(Config, Testcase).
end_per_testcase(Testcase, Config) -> end_per_testcase(Testcase, Config) ->
eventually(?_assertEqual(0, rpc(Config, ets, info, [connection_created, size])), 1000, 10),
rabbit_ct_helpers:testcase_finished(Config, Testcase). rabbit_ct_helpers:testcase_finished(Config, Testcase).
proxy_protocol_v1(Config) -> v1(Config) ->
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
{ok, Socket} = gen_tcp:connect({127,0,0,1}, Port, {ok, Socket} = gen_tcp:connect({127,0,0,1}, Port,
[binary, {active, false}, {packet, raw}]), [binary, {active, false}, {packet, raw}]),
ok = inet:send(Socket, "PROXY TCP4 192.168.1.1 192.168.1.2 80 81\r\n"), ok = inet:send(Socket, "PROXY TCP4 192.168.1.1 192.168.1.2 80 81\r\n"),
[ok = inet:send(Socket, amqp_1_0_frame(FrameType)) [ok = inet:send(Socket, amqp_1_0_frame(FrameType))
|| FrameType <- [header_sasl, sasl_init, header_amqp, open, 'begin']], || FrameType <- [header_sasl, sasl_init, header_amqp, open]],
{ok, _Packet} = gen_tcp:recv(Socket, 0, ?TIMEOUT), {ok, _Packet} = gen_tcp:recv(Socket, 0, ?TIMEOUT),
ConnectionName = rabbit_ct_broker_helpers:rpc(Config, 0, ConnectionName = rpc(Config, ?MODULE, connection_name, []),
?MODULE, connection_name, []), match = re:run(ConnectionName, <<"^192.168.1.1:80 -> 192.168.1.2:81$">>, [{capture, none}]),
match = re:run(ConnectionName, <<"^192.168.1.1:80 -> 192.168.1.2:81 \\(\\d\\)">>, [{capture, none}]), ok = gen_tcp:close(Socket).
gen_tcp:close(Socket),
ok.
proxy_protocol_v1_tls(Config) -> v1_tls(Config) ->
app_utils:start_applications([asn1, crypto, public_key, ssl]), app_utils:start_applications([asn1, crypto, public_key, ssl]),
Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp_tls), Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp_tls),
{ok, Socket} = gen_tcp:connect({127,0,0,1}, Port, {ok, Socket} = gen_tcp:connect({127,0,0,1}, Port,
[binary, {active, false}, {packet, raw}]), [binary, {active, false}, {packet, raw}]),
ok = inet:send(Socket, "PROXY TCP4 192.168.1.1 192.168.1.2 80 81\r\n"), ok = inet:send(Socket, "PROXY TCP4 192.168.1.1 192.168.1.2 80 82\r\n"),
{ok, SslSocket} = ssl:connect(Socket, [{verify, verify_none}], ?TIMEOUT), {ok, SslSocket} = ssl:connect(Socket, [{verify, verify_none}], ?TIMEOUT),
[ok = ssl:send(SslSocket, amqp_1_0_frame(FrameType)) [ok = ssl:send(SslSocket, amqp_1_0_frame(FrameType))
|| FrameType <- [header_sasl, sasl_init, header_amqp, open, 'begin']], || FrameType <- [header_sasl, sasl_init, header_amqp, open]],
{ok, _Packet} = ssl:recv(SslSocket, 0, ?TIMEOUT), {ok, _Packet} = ssl:recv(SslSocket, 0, ?TIMEOUT),
timer:sleep(1000), timer:sleep(1000),
ConnectionName = rabbit_ct_broker_helpers:rpc(Config, 0, ConnectionName = rpc(Config, ?MODULE, connection_name, []),
?MODULE, connection_name, []), match = re:run(ConnectionName, <<"^192.168.1.1:80 -> 192.168.1.2:82$">>, [{capture, none}]),
match = re:run(ConnectionName, <<"^192.168.1.1:80 -> 192.168.1.2:81 \\(\\d\\)$">>, [{capture, none}]), ok = gen_tcp:close(Socket).
gen_tcp:close(Socket),
ok.
proxy_protocol_v2_local(Config) -> v2_local(Config) ->
ProxyInfo = #{ ProxyInfo = #{
command => local, command => local,
version => 2 version => 2
@ -96,14 +91,11 @@ proxy_protocol_v2_local(Config) ->
[binary, {active, false}, {packet, raw}]), [binary, {active, false}, {packet, raw}]),
ok = inet:send(Socket, ranch_proxy_header:header(ProxyInfo)), ok = inet:send(Socket, ranch_proxy_header:header(ProxyInfo)),
[ok = inet:send(Socket, amqp_1_0_frame(FrameType)) [ok = inet:send(Socket, amqp_1_0_frame(FrameType))
|| FrameType <- [header_sasl, sasl_init, header_amqp, open, 'begin']], || FrameType <- [header_sasl, sasl_init, header_amqp, open]],
{ok, _Packet} = gen_tcp:recv(Socket, 0, ?TIMEOUT), {ok, _Packet} = gen_tcp:recv(Socket, 0, ?TIMEOUT),
ConnectionName = rabbit_ct_broker_helpers:rpc(Config, 0, ConnectionName = rpc(Config, ?MODULE, connection_name, []),
?MODULE, connection_name, []), match = re:run(ConnectionName, <<"^127.0.0.1:\\d+ -> 127.0.0.1:\\d+$">>, [{capture, none}]),
match = re:run(ConnectionName, <<"^127.0.0.1:\\d+ -> 127.0.0.1:\\d+ \\(\\d\\)$">>, [{capture, none}]), ok = gen_tcp:close(Socket).
gen_tcp:close(Socket),
ok.
%% hex frames to send to have the connection recorded in RabbitMQ %% hex frames to send to have the connection recorded in RabbitMQ
%% use wireshark with one of the Java tests to record those %% use wireshark with one of the Java tests to record those
@ -114,9 +106,7 @@ amqp_1_0_frame(header_amqp) ->
amqp_1_0_frame(sasl_init) -> amqp_1_0_frame(sasl_init) ->
hex_frame_to_binary("0000001902010000005341c00c01a309414e4f4e594d4f5553"); hex_frame_to_binary("0000001902010000005341c00c01a309414e4f4e594d4f5553");
amqp_1_0_frame(open) -> amqp_1_0_frame(open) ->
hex_frame_to_binary("0000003f02000000005310c03202a12438306335323662332d653530662d343835352d613564302d336466643738623537633730a1096c6f63616c686f7374"); hex_frame_to_binary("0000003f02000000005310c03202a12438306335323662332d653530662d343835352d613564302d336466643738623537633730a1096c6f63616c686f7374").
amqp_1_0_frame('begin') ->
hex_frame_to_binary("0000002002000000005311c01305405201707fffffff707fffffff700000ffff").
hex_frame_to_binary(HexsString) -> hex_frame_to_binary(HexsString) ->
Hexs = split(HexsString, []), Hexs = split(HexsString, []),
@ -135,18 +125,16 @@ connection_name() ->
%% hence the retry %% hence the retry
case retry(fun connection_registered/0, 20) of case retry(fun connection_registered/0, 20) of
true -> true ->
Connections = ets:tab2list(connection_created), [{_Key, Values}] = ets:tab2list(connection_created),
{_Key, Values} = lists:nth(1, Connections),
{_, Name} = lists:keyfind(name, 1, Values), {_, Name} = lists:keyfind(name, 1, Values),
Name; Name;
false -> false ->
error ct:fail("not 1 connection registered")
end. end.
connection_registered() -> connection_registered() ->
I = ets:info(connection_created), Size = ets:info(connection_created, size),
Size = proplists:get_value(size, I), Size =:= 1.
Size > 0.
retry(_Function, 0) -> retry(_Function, 0) ->
false; false;

View File

@ -5,7 +5,7 @@
%% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. %% Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%% %%
-module(system_SUITE). -module(amqp_system_SUITE).
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("rabbit_common/include/rabbit_framing.hrl"). -include_lib("rabbit_common/include/rabbit_framing.hrl").
@ -58,27 +58,11 @@ init_per_suite(Config) ->
end_per_suite(Config) -> end_per_suite(Config) ->
Config. Config.
init_per_group(streams, Config) ->
case rabbit_ct_helpers:is_mixed_versions() of
false ->
Suffix = rabbit_ct_helpers:testcase_absname(Config, "", "-"),
Config1 = rabbit_ct_helpers:set_config(Config, [
{rmq_nodename_suffix, Suffix},
{amqp10_client_library, dotnet}
]),
rabbit_ct_helpers:run_setup_steps(Config1, [
fun build_dotnet_test_project/1
] ++
rabbit_ct_broker_helpers:setup_steps() ++
rabbit_ct_client_helpers:setup_steps());
_ ->
{skip, "stream tests are skipped in mixed mode"}
end;
init_per_group(Group, Config) -> init_per_group(Group, Config) ->
Suffix = rabbit_ct_helpers:testcase_absname(Config, "", "-"), Suffix = rabbit_ct_helpers:testcase_absname(Config, "", "-"),
Config1 = rabbit_ct_helpers:set_config(Config, [ Config1 = rabbit_ct_helpers:set_config(Config, [
{rmq_nodename_suffix, Suffix}, {rmq_nodename_suffix, Suffix},
{amqp10_client_library, Group} {amqp_client_library, Group}
]), ]),
GroupSetupStep = case Group of GroupSetupStep = case Group of
dotnet -> fun build_dotnet_test_project/1; dotnet -> fun build_dotnet_test_project/1;
@ -131,76 +115,51 @@ build_maven_test_project(Config) ->
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------
roundtrip(Config) -> roundtrip(Config) ->
run(Config, [ run(Config, [{dotnet, "roundtrip"},
{dotnet, "roundtrip"}, {java, "RoundTripTest"}]).
{java, "RoundTripTest"}
]).
streams(Config) -> streams(Config) ->
Ch = rabbit_ct_client_helpers:open_channel(Config, 0), Ch = rabbit_ct_client_helpers:open_channel(Config),
#'queue.declare_ok'{} = amqp_channel:call(Ch, #'queue.declare'{queue = <<"stream_q2">>,
amqp_channel:call(Ch, #'queue.declare'{queue = <<"stream_q2">>, durable = true,
durable = true, arguments = [{<<"x-queue-type">>, longstr, "stream"}]}),
arguments = [{<<"x-queue-type">>, longstr, "stream"}]}), run(Config, [{dotnet, "streams"}]).
run(Config, [
{dotnet, "streams"}
]).
roundtrip_to_amqp_091(Config) -> roundtrip_to_amqp_091(Config) ->
run(Config, [ run(Config, [{dotnet, "roundtrip_to_amqp_091"}]).
{dotnet, "roundtrip_to_amqp_091"}
]).
default_outcome(Config) -> default_outcome(Config) ->
run(Config, [ run(Config, [{dotnet, "default_outcome"}]).
{dotnet, "default_outcome"}
]).
no_routes_is_released(Config) -> no_routes_is_released(Config) ->
Ch = rabbit_ct_client_helpers:open_channel(Config, 0), Ch = rabbit_ct_client_helpers:open_channel(Config),
amqp_channel:call(Ch, #'exchange.declare'{exchange = <<"no_routes_is_released">>, amqp_channel:call(Ch, #'exchange.declare'{exchange = <<"no_routes_is_released">>,
durable = true}), durable = true}),
run(Config, [ run(Config, [{dotnet, "no_routes_is_released"}]).
{dotnet, "no_routes_is_released"}
]).
outcomes(Config) -> outcomes(Config) ->
run(Config, [ run(Config, [{dotnet, "outcomes"}]).
{dotnet, "outcomes"}
]).
fragmentation(Config) -> fragmentation(Config) ->
run(Config, [ run(Config, [{dotnet, "fragmentation"}]).
{dotnet, "fragmentation"}
]).
message_annotations(Config) -> message_annotations(Config) ->
run(Config, [ run(Config, [{dotnet, "message_annotations"}]).
{dotnet, "message_annotations"}
]).
footer(Config) -> footer(Config) ->
run(Config, [ run(Config, [{dotnet, "footer"}]).
{dotnet, "footer"}
]).
data_types(Config) -> data_types(Config) ->
run(Config, [ run(Config, [{dotnet, "data_types"}]).
{dotnet, "data_types"}
]).
reject(Config) -> reject(Config) ->
run(Config, [ run(Config, [{dotnet, "reject"}]).
{dotnet, "reject"}
]).
redelivery(Config) -> redelivery(Config) ->
run(Config, [ run(Config, [{dotnet, "redelivery"}]).
{dotnet, "redelivery"}
]).
routing(Config) -> routing(Config) ->
Ch = rabbit_ct_client_helpers:open_channel(Config, 0), Ch = rabbit_ct_client_helpers:open_channel(Config),
amqp_channel:call(Ch, #'queue.declare'{queue = <<"transient_q">>, amqp_channel:call(Ch, #'queue.declare'{queue = <<"transient_q">>,
durable = false}), durable = false}),
amqp_channel:call(Ch, #'queue.declare'{queue = <<"durable_q">>, amqp_channel:call(Ch, #'queue.declare'{queue = <<"durable_q">>,
@ -217,18 +176,6 @@ routing(Config) ->
{dotnet, "routing"} {dotnet, "routing"}
]). ]).
%% TODO: this tests doesn't test anything that the standard routing test
%% already does. We should test stream specific things here like attaching
%% to a given offset
stream_interop_basics(Config) ->
Ch = rabbit_ct_client_helpers:open_channel(Config, 0),
amqp_channel:call(Ch, #'queue.declare'{queue = <<"stream_q">>,
durable = true,
arguments = [{<<"x-queue-type">>, longstr, <<"stream">>}]}),
run(Config, [
{dotnet, "routing"}
]).
invalid_routes(Config) -> invalid_routes(Config) ->
run(Config, [ run(Config, [
{dotnet, "invalid_routes"} {dotnet, "invalid_routes"}
@ -238,7 +185,7 @@ auth_failure(Config) ->
run(Config, [ {dotnet, "auth_failure"} ]). run(Config, [ {dotnet, "auth_failure"} ]).
access_failure(Config) -> access_failure(Config) ->
User = <<"access_failure">>, User = atom_to_binary(?FUNCTION_NAME),
rabbit_ct_broker_helpers:add_user(Config, User, <<"boo">>), rabbit_ct_broker_helpers:add_user(Config, User, <<"boo">>),
rabbit_ct_broker_helpers:set_permissions(Config, User, <<"/">>, rabbit_ct_broker_helpers:set_permissions(Config, User, <<"/">>,
<<".*">>, %% configure <<".*">>, %% configure
@ -248,12 +195,12 @@ access_failure(Config) ->
run(Config, [ {dotnet, "access_failure"} ]). run(Config, [ {dotnet, "access_failure"} ]).
access_failure_not_allowed(Config) -> access_failure_not_allowed(Config) ->
User = <<"access_failure_not_allowed">>, User = atom_to_binary(?FUNCTION_NAME),
rabbit_ct_broker_helpers:add_user(Config, User, <<"boo">>), rabbit_ct_broker_helpers:add_user(Config, User, <<"boo">>),
run(Config, [ {dotnet, "access_failure_not_allowed"} ]). run(Config, [ {dotnet, "access_failure_not_allowed"} ]).
access_failure_send(Config) -> access_failure_send(Config) ->
User = <<"access_failure_send">>, User = atom_to_binary(?FUNCTION_NAME),
rabbit_ct_broker_helpers:add_user(Config, User, <<"boo">>), rabbit_ct_broker_helpers:add_user(Config, User, <<"boo">>),
rabbit_ct_broker_helpers:set_permissions(Config, User, <<"/">>, rabbit_ct_broker_helpers:set_permissions(Config, User, <<"/">>,
<<".*">>, %% configure <<".*">>, %% configure
@ -263,15 +210,13 @@ access_failure_send(Config) ->
run(Config, [ {dotnet, "access_failure_send"} ]). run(Config, [ {dotnet, "access_failure_send"} ]).
run(Config, Flavors) -> run(Config, Flavors) ->
ClientLibrary = ?config(amqp10_client_library, Config), ClientLibrary = ?config(amqp_client_library, Config),
Fun = case ClientLibrary of Fun = case ClientLibrary of
dotnet -> fun run_dotnet_test/2; dotnet -> fun run_dotnet_test/2;
java -> fun run_java_test/2 java -> fun run_java_test/2
end, end,
case proplists:get_value(ClientLibrary, Flavors) of {ClientLibrary, TestName} = proplists:lookup(ClientLibrary, Flavors),
false -> ok; Fun(Config, TestName).
TestName -> Fun(Config, TestName)
end.
run_dotnet_test(Config, Method) -> run_dotnet_test(Config, Method) ->
TestProjectDir = ?config(dotnet_test_project_dir, Config), TestProjectDir = ?config(dotnet_test_project_dir, Config),

View File

@ -203,9 +203,11 @@ module Test =
receiver.SetCredit(100, true) receiver.SetCredit(100, true)
let rtd = receiver.Receive() let rtd = receiver.Receive()
assertNotNull rtd assertNotNull rtd
assertTrue (rtd.MessageAnnotations.Map.Count = 1) assertEqual 3 rtd.MessageAnnotations.Map.Count
let (result, _) = rtd.MessageAnnotations.Map.TryGetValue("x-stream-offset") assertTrue (rtd.MessageAnnotations.Map.ContainsKey(Symbol "x-stream-offset"))
assertTrue result assertTrue (rtd.MessageAnnotations.Map.ContainsKey(Symbol "x-exchange"))
assertTrue (rtd.MessageAnnotations.Map.ContainsKey(Symbol "x-routing-key"))
assertEqual body rtd.Body assertEqual body rtd.Body
assertEqual rtd.Properties.CorrelationId corr assertEqual rtd.Properties.CorrelationId corr
receiver.Close() receiver.Close()
@ -216,7 +218,7 @@ module Test =
let roundtrip_to_amqp_091 uri = let roundtrip_to_amqp_091 uri =
use c = connect uri use c = connect uri
let q = "roundtrip-091-q" let q = "roundtrip-091-q"
let corr = "corrlation" let corr = "correlation"
let sender = SenderLink(c.Session, q + "-sender" , q) let sender = SenderLink(c.Session, q + "-sender" , q)
new Message("hi"B, Header = Header(), new Message("hi"B, Header = Header(),
@ -300,7 +302,8 @@ module Test =
assertEqual m.Body m'.Body assertEqual m.Body m'.Body
assertEqual (m.MessageAnnotations.Descriptor) (m'.MessageAnnotations.Descriptor) assertEqual (m.MessageAnnotations.Descriptor) (m'.MessageAnnotations.Descriptor)
assertEqual 2 (m'.MessageAnnotations.Map.Count) // our 2 custom annotations + x-exchange + x-routing-key = 4
assertEqual 4 (m'.MessageAnnotations.Map.Count)
assertTrue (m.MessageAnnotations.[k1] = m'.MessageAnnotations.[k1]) assertTrue (m.MessageAnnotations.[k1] = m'.MessageAnnotations.[k1])
assertTrue (m.MessageAnnotations.[k2] = m'.MessageAnnotations.[k2]) assertTrue (m.MessageAnnotations.[k2] = m'.MessageAnnotations.[k2])
@ -312,7 +315,7 @@ module Test =
let k2 = Symbol "key2" let k2 = Symbol "key2"
footer.[Symbol "key1"] <- "value1" footer.[Symbol "key1"] <- "value1"
footer.[Symbol "key2"] <- "value2" footer.[Symbol "key2"] <- "value2"
let m = new Message("testing annotations", Footer = footer) let m = new Message("testing footer", Footer = footer)
sender.Send m sender.Send m
let m' = receive receiver let m' = receive receiver
@ -432,7 +435,7 @@ module Test =
receiver.Close() receiver.Close()
with with
| :? Amqp.AmqpException as ae -> | :? Amqp.AmqpException as ae ->
assertEqual (ae.Error.Condition) (Symbol cond) assertEqual (Symbol cond) (ae.Error.Condition)
| _ -> failwith "invalid expection thrown" | _ -> failwith "invalid expection thrown"
let authFailure uri = let authFailure uri =
@ -456,8 +459,6 @@ module Test =
)) ))
let sender = new SenderLink(ac.Session, "test-sender", dest) let sender = new SenderLink(ac.Session, "test-sender", dest)
sender.Send(new Message "hi", TimeSpan.FromSeconds 15.) sender.Send(new Message "hi", TimeSpan.FromSeconds 15.)
failwith "expected exception not received" failwith "expected exception not received"
with with
| :? Amqp.AmqpException as ex -> | :? Amqp.AmqpException as ex ->

View File

@ -8,7 +8,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.*" /> <PackageReference Include="RabbitMQ.Client" Version="6.*" />
<PackageReference Include="AmqpNetLite" Version="2.4.1" /> <PackageReference Include="AmqpNetLite" Version="2.4.8" />
<PackageReference Include="AmqpNetLite.Serialization" Version="2.4.1" /> <PackageReference Include="AmqpNetLite.Serialization" Version="2.4.8" />
</ItemGroup> </ItemGroup>
</Project> </Project>

71
deps/rabbit/test/event_recorder.erl vendored Normal file
View File

@ -0,0 +1,71 @@
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2023 VMware, Inc. or its affiliates. All rights reserved.
%%
-module(event_recorder).
-behaviour(gen_event).
-include_lib("stdlib/include/assert.hrl").
-include_lib("rabbit_common/include/rabbit.hrl").
%% gen_event callbacks
-export([init/1,
handle_event/2,
handle_call/2]).
%% client API
-export([start/1,
stop/1,
get_events/1]).
-export([assert_event_type/2,
assert_event_prop/2]).
-import(rabbit_ct_broker_helpers,
[get_node_config/3]).
-define(INIT_STATE, []).
init(_) ->
{ok, ?INIT_STATE}.
handle_event(#event{type = T}, State)
when T =:= node_stats orelse
T =:= node_node_stats orelse
T =:= node_node_deleted ->
{ok, State};
handle_event(Event, State) ->
{ok, [Event | State]}.
handle_call(take_state, State) ->
{ok, lists:reverse(State), ?INIT_STATE}.
start(Config) ->
ok = rabbit_ct_broker_helpers:add_code_path_to_all_nodes(Config, ?MODULE),
ok = gen_event:add_handler(event_manager_ref(Config), ?MODULE, []).
stop(Config) ->
ok = gen_event:delete_handler(event_manager_ref(Config), ?MODULE, []).
get_events(Config) ->
%% events are sent and processed asynchronously
timer:sleep(500),
Result = gen_event:call(event_manager_ref(Config), ?MODULE, take_state),
?assert(is_list(Result)),
Result.
event_manager_ref(Config) ->
Node = get_node_config(Config, 0, nodename),
{rabbit_event, Node}.
assert_event_type(ExpectedType, #event{type = ActualType}) ->
?assertEqual(ExpectedType, ActualType).
assert_event_prop(ExpectedProp = {Key, _Value}, #event{props = Props}) ->
?assertEqual(ExpectedProp, lists:keyfind(Key, 1, Props));
assert_event_prop(ExpectedProps, Event)
when is_list(ExpectedProps) ->
lists:foreach(fun(P) ->
assert_event_prop(P, Event)
end, ExpectedProps).

View File

@ -7,29 +7,21 @@
-module(message_size_limit_SUITE). -module(message_size_limit_SUITE).
-include_lib("common_test/include/ct.hrl"). -compile([export_all, nowarn_export_all]).
-include_lib("kernel/include/file.hrl").
-include_lib("amqp_client/include/amqp_client.hrl"). -include_lib("amqp_client/include/amqp_client.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-compile(export_all).
-define(TIMEOUT_LIST_OPS_PASS, 5000).
-define(TIMEOUT, 30000).
-define(TIMEOUT_CHANNEL_EXCEPTION, 5000). -define(TIMEOUT_CHANNEL_EXCEPTION, 5000).
-define(CLEANUP_QUEUE_NAME, <<"cleanup-queue">>).
all() -> all() ->
[ [
{group, parallel_tests} {group, tests}
]. ].
groups() -> groups() ->
[ [
{parallel_tests, [parallel], [ {tests, [], [
max_message_size max_message_size
]} ]}
]. ].
suite() -> suite() ->
@ -81,8 +73,7 @@ max_message_size(Config) ->
Size2Mb = 1024 * 1024 * 2, Size2Mb = 1024 * 1024 * 2,
Size2Mb = byte_size(Binary2M), Size2Mb = byte_size(Binary2M),
rabbit_ct_broker_helpers:rpc(Config, 0, ok = rabbit_ct_broker_helpers:rpc(Config, persistent_term, put, [max_message_size, 1024 * 1024 * 3]),
application, set_env, [rabbit, max_message_size, 1024 * 1024 * 3]),
{_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0), {_, Ch} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
@ -96,8 +87,7 @@ max_message_size(Config) ->
assert_channel_fail_max_size(Ch, Monitor), assert_channel_fail_max_size(Ch, Monitor),
%% increase the limit %% increase the limit
rabbit_ct_broker_helpers:rpc(Config, 0, ok = rabbit_ct_broker_helpers:rpc(Config, persistent_term, put, [max_message_size, 1024 * 1024 * 8]),
application, set_env, [rabbit, max_message_size, 1024 * 1024 * 8]),
{_, Ch1} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0), {_, Ch1} = rabbit_ct_client_helpers:open_connection_and_channel(Config, 0),
@ -112,15 +102,7 @@ max_message_size(Config) ->
Monitor1 = monitor(process, Ch1), Monitor1 = monitor(process, Ch1),
amqp_channel:call(Ch1, #'basic.publish'{routing_key = <<"none">>}, #amqp_msg{payload = Binary10M}), amqp_channel:call(Ch1, #'basic.publish'{routing_key = <<"none">>}, #amqp_msg{payload = Binary10M}),
assert_channel_fail_max_size(Ch1, Monitor1), assert_channel_fail_max_size(Ch1, Monitor1).
%% increase beyond the hard limit
rabbit_ct_broker_helpers:rpc(Config, 0,
application, set_env, [rabbit, max_message_size, 1024 * 1024 * 600]),
Val = rabbit_ct_broker_helpers:rpc(Config, 0,
rabbit_channel, get_max_message_size, []),
?assertEqual(?MAX_MSG_SIZE, Val).
%% ------------------------------------------------------------------- %% -------------------------------------------------------------------
%% Implementation %% Implementation

View File

@ -64,8 +64,8 @@ confirm(_Config) ->
?assertEqual(undefined, rabbit_confirms:smallest(U7)), ?assertEqual(undefined, rabbit_confirms:smallest(U7)),
U8 = rabbit_confirms:insert(2, [QName], XName, U1), U8 = rabbit_confirms:insert(2, [QName], XName, U1),
{[{1, XName}, {2, XName}], _U9} = rabbit_confirms:confirm([1, 2], QName, U8), {[{Seq1, XName}, {Seq2, XName}], _U9} = rabbit_confirms:confirm([1, 2], QName, U8),
ok. ?assertEqual([1, 2], lists:sort([Seq1, Seq2])).
reject(_Config) -> reject(_Config) ->
@ -94,8 +94,7 @@ reject(_Config) ->
{ok, {2, XName}, U5} = rabbit_confirms:reject(2, U3), {ok, {2, XName}, U5} = rabbit_confirms:reject(2, U3),
{error, not_found} = rabbit_confirms:reject(2, U5), {error, not_found} = rabbit_confirms:reject(2, U5),
?assertEqual(1, rabbit_confirms:size(U5)), ?assertEqual(1, rabbit_confirms:size(U5)),
?assertEqual(1, rabbit_confirms:smallest(U5)), ?assertEqual(1, rabbit_confirms:smallest(U5)).
ok.
remove_queue(_Config) -> remove_queue(_Config) ->
XName = rabbit_misc:r(<<"/">>, exchange, <<"X">>), XName = rabbit_misc:r(<<"/">>, exchange, <<"X">>),
@ -114,5 +113,5 @@ remove_queue(_Config) ->
U5 = rabbit_confirms:insert(1, [QName], XName, U0), U5 = rabbit_confirms:insert(1, [QName], XName, U0),
U6 = rabbit_confirms:insert(2, [QName], XName, U5), U6 = rabbit_confirms:insert(2, [QName], XName, U5),
{[{1, XName}, {2, XName}], _U} = rabbit_confirms:remove_queue(QName, U6), {[{Seq1, XName}, {Seq2, XName}], _U} = rabbit_confirms:remove_queue(QName, U6),
ok. ?assertEqual([1, 2], lists:sort([Seq1, Seq2])).

View File

@ -34,17 +34,11 @@ all_tests() ->
groups() -> groups() ->
[ [
{machine_version_2, [], all_tests()}, {machine_version_2, [shuffle], all_tests()},
{machine_version_3, [], all_tests()}, {machine_version_3, [shuffle], all_tests()},
{machine_version_conversion, [], [convert_v2_to_v3]} {machine_version_conversion, [shuffle], [convert_v2_to_v3]}
]. ].
init_per_suite(Config) ->
Config.
end_per_suite(_Config) ->
ok.
init_per_group(machine_version_2, Config) -> init_per_group(machine_version_2, Config) ->
[{machine_version, 2} | Config]; [{machine_version, 2} | Config];
init_per_group(machine_version_3, Config) -> init_per_group(machine_version_3, Config) ->
@ -55,12 +49,6 @@ init_per_group(machine_version_conversion, Config) ->
end_per_group(_Group, _Config) -> end_per_group(_Group, _Config) ->
ok. ok.
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
ok.
%%%=================================================================== %%%===================================================================
%%% Test cases %%% Test cases
%%%=================================================================== %%%===================================================================
@ -91,8 +79,7 @@ end_per_testcase(_TestCase, _Config) ->
test_init(Name) -> test_init(Name) ->
init(#{name => Name, init(#{name => Name,
max_in_memory_length => 0, max_in_memory_length => 0,
queue_resource => rabbit_misc:r("/", queue, queue_resource => rabbit_misc:r("/", queue, atom_to_binary(Name)),
atom_to_binary(Name, utf8)),
release_cursor_interval => 0}). release_cursor_interval => 0}).
enq_enq_checkout_test(C) -> enq_enq_checkout_test(C) ->
@ -109,7 +96,7 @@ enq_enq_checkout_test(C) ->
?ASSERT_EFF({log, [1,2], _Fun, _Local}, Effects), ?ASSERT_EFF({log, [1,2], _Fun, _Local}, Effects),
ok. ok.
credit_enq_enq_checkout_settled_credit_test(C) -> credit_enq_enq_checkout_settled_credit_v1_test(C) ->
Cid = {?FUNCTION_NAME, self()}, Cid = {?FUNCTION_NAME, self()},
{State1, _} = enq(C, 1, 1, first, test_init(test)), {State1, _} = enq(C, 1, 1, first, test_init(test)),
{State2, _} = enq(C, 2, 2, second, State1), {State2, _} = enq(C, 2, 2, second, State1),
@ -122,7 +109,8 @@ credit_enq_enq_checkout_settled_credit_test(C) ->
{State4, SettledEffects} = settle(C, Cid, 4, 1, State3), {State4, SettledEffects} = settle(C, Cid, 4, 1, State3),
?assertEqual(false, lists:any(fun ({log, _, _, _}) -> ?assertEqual(false, lists:any(fun ({log, _, _, _}) ->
true; true;
(_) -> false (_) ->
false
end, SettledEffects)), end, SettledEffects)),
%% granting credit (3) should deliver the second msg if the receivers %% granting credit (3) should deliver the second msg if the receivers
%% delivery count is (1) %% delivery count is (1)
@ -136,8 +124,43 @@ credit_enq_enq_checkout_settled_credit_test(C) ->
end, FinalEffects)), end, FinalEffects)),
ok. ok.
credit_with_drained_test(C) -> credit_enq_enq_checkout_settled_credit_v2_test(C) ->
Cid = {?FUNCTION_NAME, self()}, Ctag = ?FUNCTION_NAME,
Cid = {Ctag, self()},
{State1, _} = enq(C, 1, 1, first, test_init(test)),
{State2, _} = enq(C, 2, 2, second, State1),
{State3, _, Effects} = apply(meta(C, 3),
rabbit_fifo:make_checkout(
Cid,
{auto, 1, credited},
%% denotes that credit API v2 is used
#{initial_delivery_count => 16#ff_ff_ff_ff}),
State2),
?ASSERT_EFF({monitor, _, _}, Effects),
?ASSERT_EFF({log, [1], _Fun, _Local}, Effects),
%% Settling the delivery should not grant new credit.
{State4, SettledEffects} = settle(C, Cid, 4, 1, State3),
?assertEqual(false, lists:any(fun ({log, _, _, _}) ->
true;
(_) ->
false
end, SettledEffects)),
{State5, CreditEffects} = credit(C, Cid, 5, 1, 0, false, State4),
?ASSERT_EFF({log, [2], _, _}, CreditEffects),
%% The credit_reply should be sent **after** the delivery.
?assertEqual({send_msg, self(),
{credit_reply, Ctag, _DeliveryCount = 1, _Credit = 0, _Available = 0, _Drain = false},
?DELIVERY_SEND_MSG_OPTS},
lists:last(CreditEffects)),
{_State6, FinalEffects} = enq(C, 6, 3, third, State5),
?assertEqual(false, lists:any(fun ({log, _, _, _}) ->
true;
(_) -> false
end, FinalEffects)).
credit_with_drained_v1_test(C) ->
Ctag = ?FUNCTION_NAME,
Cid = {Ctag, self()},
State0 = test_init(test), State0 = test_init(test),
%% checkout with a single credit %% checkout with a single credit
{State1, _, _} = {State1, _, _} =
@ -147,17 +170,42 @@ credit_with_drained_test(C) ->
delivery_count = 0}}}, delivery_count = 0}}},
State1), State1),
{State, Result, _} = {State, Result, _} =
apply(meta(C, 3), rabbit_fifo:make_credit(Cid, 0, 5, true), State1), apply(meta(C, 3), rabbit_fifo:make_credit(Cid, 5, 0, true), State1),
?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 0, ?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 0,
delivery_count = 5}}}, delivery_count = 5}}},
State), State),
?assertEqual({multi, [{send_credit_reply, 0}, ?assertEqual({multi, [{send_credit_reply, 0},
{send_drained, {?FUNCTION_NAME, 5}}]}, {send_drained, {Ctag, 5}}]},
Result), Result),
ok. ok.
credit_and_drain_test(C) -> credit_with_drained_v2_test(C) ->
Cid = {?FUNCTION_NAME, self()}, Ctag = ?FUNCTION_NAME,
Cid = {Ctag, self()},
State0 = test_init(test),
%% checkout with a single credit
{State1, _, _} = apply(meta(C, 1),
rabbit_fifo:make_checkout(
Cid,
{auto, 1, credited},
%% denotes that credit API v2 is used
#{initial_delivery_count => 0}),
State0),
?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 1,
delivery_count = 0}}},
State1),
{State, ok, Effects} = apply(meta(C, 3), rabbit_fifo:make_credit(Cid, 5, 0, true), State1),
?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 0,
delivery_count = 5}}},
State),
?assertEqual([{send_msg, self(),
{credit_reply, Ctag, _DeliveryCount = 5, _Credit = 0, _Available = 0, _Drain = true},
?DELIVERY_SEND_MSG_OPTS}],
Effects).
credit_and_drain_v1_test(C) ->
Ctag = ?FUNCTION_NAME,
Cid = {Ctag, self()},
{State1, _} = enq(C, 1, 1, first, test_init(test)), {State1, _} = enq(C, 1, 1, first, test_init(test)),
{State2, _} = enq(C, 2, 2, second, State1), {State2, _} = enq(C, 2, 2, second, State1),
%% checkout without any initial credit (like AMQP 1.0 would) %% checkout without any initial credit (like AMQP 1.0 would)
@ -167,7 +215,7 @@ credit_and_drain_test(C) ->
?ASSERT_NO_EFF({log, _, _, _}, CheckEffs), ?ASSERT_NO_EFF({log, _, _, _}, CheckEffs),
{State4, {multi, [{send_credit_reply, 0}, {State4, {multi, [{send_credit_reply, 0},
{send_drained, {?FUNCTION_NAME, 2}}]}, {send_drained, {Ctag, 2}}]},
Effects} = apply(meta(C, 4), rabbit_fifo:make_credit(Cid, 4, 0, true), State3), Effects} = apply(meta(C, 4), rabbit_fifo:make_credit(Cid, 4, 0, true), State3),
?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 0, ?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 0,
delivery_count = 4}}}, delivery_count = 4}}},
@ -178,7 +226,36 @@ credit_and_drain_test(C) ->
?ASSERT_NO_EFF({log, _, _, _}, EnqEffs), ?ASSERT_NO_EFF({log, _, _, _}, EnqEffs),
ok. ok.
credit_and_drain_v2_test(C) ->
Ctag = ?FUNCTION_NAME,
Cid = {Ctag, self()},
{State1, _} = enq(C, 1, 1, first, test_init(test)),
{State2, _} = enq(C, 2, 2, second, State1),
{State3, _, CheckEffs} = apply(meta(C, 3),
rabbit_fifo:make_checkout(
Cid,
%% checkout without any initial credit (like AMQP 1.0 would)
{auto, 0, credited},
%% denotes that credit API v2 is used
#{initial_delivery_count => 16#ff_ff_ff_ff - 1}),
State2),
?ASSERT_NO_EFF({log, _, _, _}, CheckEffs),
{State4, ok, Effects} = apply(meta(C, 4),
rabbit_fifo:make_credit(Cid, 4, 16#ff_ff_ff_ff - 1, true),
State3),
?assertMatch(#rabbit_fifo{consumers = #{Cid := #consumer{credit = 0,
delivery_count = 2}}},
State4),
?ASSERT_EFF({log, [1, 2], _, _}, Effects),
%% The credit_reply should be sent **after** the deliveries.
?assertEqual({send_msg, self(),
{credit_reply, Ctag, _DeliveryCount = 2, _Credit = 0, _Available = 0, _Drain = true},
?DELIVERY_SEND_MSG_OPTS},
lists:last(Effects)),
{_State5, EnqEffs} = enq(C, 5, 2, third, State4),
?ASSERT_NO_EFF({log, _, _, _}, EnqEffs).
enq_enq_deq_test(C) -> enq_enq_deq_test(C) ->
Cid = {?FUNCTION_NAME, self()}, Cid = {?FUNCTION_NAME, self()},
@ -1402,10 +1479,9 @@ single_active_cancelled_with_unacked_test(C) ->
?assertMatch([], rabbit_fifo:query_waiting_consumers(State6)), ?assertMatch([], rabbit_fifo:query_waiting_consumers(State6)),
ok. ok.
single_active_with_credited_test(C) -> single_active_with_credited_v1_test(C) ->
State0 = init(#{name => ?FUNCTION_NAME, State0 = init(#{name => ?FUNCTION_NAME,
queue_resource => rabbit_misc:r("/", queue, queue_resource => rabbit_misc:r("/", queue, atom_to_binary(?FUNCTION_NAME)),
atom_to_binary(?FUNCTION_NAME, utf8)),
release_cursor_interval => 0, release_cursor_interval => 0,
single_active_consumer_on => true}), single_active_consumer_on => true}),
@ -1435,6 +1511,45 @@ single_active_with_credited_test(C) ->
rabbit_fifo:query_waiting_consumers(State3)), rabbit_fifo:query_waiting_consumers(State3)),
ok. ok.
single_active_with_credited_v2_test(C) ->
State0 = init(#{name => ?FUNCTION_NAME,
queue_resource => rabbit_misc:r("/", queue, atom_to_binary(?FUNCTION_NAME)),
release_cursor_interval => 0,
single_active_consumer_on => true}),
C1 = {<<"ctag1">>, self()},
{State1, _, _} = apply(meta(C, 1),
make_checkout(C1,
{auto, 0, credited},
%% denotes that credit API v2 is used
#{initial_delivery_count => 0}),
State0),
C2 = {<<"ctag2">>, self()},
{State2, _, _} = apply(meta(C, 2),
make_checkout(C2,
{auto, 0, credited},
%% denotes that credit API v2 is used
#{initial_delivery_count => 0}),
State1),
%% add some credit
C1Cred = rabbit_fifo:make_credit(C1, 5, 0, false),
{State3, ok, Effects1} = apply(meta(C, 3), C1Cred, State2),
?assertEqual([{send_msg, self(),
{credit_reply, <<"ctag1">>, _DeliveryCount = 0, _Credit = 5, _Available = 0, _Drain = false},
?DELIVERY_SEND_MSG_OPTS}],
Effects1),
C2Cred = rabbit_fifo:make_credit(C2, 4, 0, false),
{State, ok, Effects2} = apply(meta(C, 4), C2Cred, State3),
?assertEqual({send_msg, self(),
{credit_reply, <<"ctag2">>, _DeliveryCount = 0, _Credit = 4, _Available = 0, _Drain = false},
?DELIVERY_SEND_MSG_OPTS},
Effects2),
%% both consumers should have credit
?assertMatch(#{C1 := #consumer{credit = 5}},
State#rabbit_fifo.consumers),
?assertMatch([{C2, #consumer{credit = 4}}],
rabbit_fifo:query_waiting_consumers(State)).
register_enqueuer_test(C) -> register_enqueuer_test(C) ->
State0 = init(#{name => ?FUNCTION_NAME, State0 = init(#{name => ?FUNCTION_NAME,

View File

@ -32,7 +32,8 @@ all_tests() ->
discard, discard,
cancel_checkout, cancel_checkout,
lost_delivery, lost_delivery,
credit, credit_api_v1,
credit_api_v2,
untracked_enqueue, untracked_enqueue,
flow, flow,
test_queries, test_queries,
@ -42,7 +43,7 @@ all_tests() ->
groups() -> groups() ->
[ [
{tests, [], all_tests()} {tests, [shuffle], all_tests()}
]. ].
init_per_group(_, Config) -> init_per_group(_, Config) ->
@ -441,7 +442,7 @@ lost_delivery(Config) ->
end), end),
ok. ok.
credit(Config) -> credit_api_v1(Config) ->
ClusterName = ?config(cluster_name, Config), ClusterName = ?config(cluster_name, Config),
ServerId = ?config(node_id, Config), ServerId = ?config(node_id, Config),
ok = start_cluster(ClusterName, [ServerId]), ok = start_cluster(ClusterName, [ServerId]),
@ -450,21 +451,27 @@ credit(Config) ->
{ok, F2, []} = rabbit_fifo_client:enqueue(ClusterName, m2, F1), {ok, F2, []} = rabbit_fifo_client:enqueue(ClusterName, m2, F1),
{_, _, F3} = process_ra_events(receive_ra_events(2, 0), ClusterName, F2), {_, _, F3} = process_ra_events(receive_ra_events(2, 0), ClusterName, F2),
%% checkout with 0 prefetch %% checkout with 0 prefetch
{ok, F4} = rabbit_fifo_client:checkout(<<"tag">>, 0, credited, #{}, F3), CTag = <<"my-tag">>,
{ok, F4} = rabbit_fifo_client:checkout(CTag, 0, credited, #{}, F3),
%% assert no deliveries %% assert no deliveries
{_, _, F5} = process_ra_events(receive_ra_events(), ClusterName, F4, [], [], {_, _, F5} = process_ra_events(receive_ra_events(), ClusterName, F4, [], [],
fun fun
(D, _) -> error({unexpected_delivery, D}) (D, _) -> error({unexpected_delivery, D})
end), end),
%% provide some credit %% provide some credit
{F6, []} = rabbit_fifo_client:credit(<<"tag">>, 1, false, F5), {F6, []} = rabbit_fifo_client:credit_v1(CTag, 1, false, F5),
{[{_, _, _, _, m1}], [{send_credit_reply, _}], F7} = {[{_, _, _, _, m1}], [{send_credit_reply, 1}], F7} =
process_ra_events(receive_ra_events(1, 1), ClusterName, F6), process_ra_events(receive_ra_events(1, 1), ClusterName, F6),
%% credit and drain %% credit and drain
{F8, []} = rabbit_fifo_client:credit(<<"tag">>, 4, true, F7), Drain = true,
{[{_, _, _, _, m2}], [{send_credit_reply, _}, {send_drained, _}], F9} = {F8, []} = rabbit_fifo_client:credit_v1(CTag, 4, Drain, F7),
process_ra_events(receive_ra_events(2, 1), ClusterName, F8), AvailableAfterCheckout = 0,
{[{_, _, _, _, m2}],
[{send_credit_reply, AvailableAfterCheckout},
{credit_reply_v1, CTag, _CreditAfterCheckout = 3,
AvailableAfterCheckout, Drain}],
F9} = process_ra_events(receive_ra_events(2, 1), ClusterName, F8),
flush(), flush(),
%% enqueue another message - at this point the consumer credit should be %% enqueue another message - at this point the consumer credit should be
@ -476,10 +483,78 @@ credit(Config) ->
(D, _) -> error({unexpected_delivery, D}) (D, _) -> error({unexpected_delivery, D})
end), end),
%% credit again and receive the last message %% credit again and receive the last message
{F12, []} = rabbit_fifo_client:credit(<<"tag">>, 10, false, F11), {F12, []} = rabbit_fifo_client:credit_v1(CTag, 10, false, F11),
{[{_, _, _, _, m3}], _, _} = process_ra_events(receive_ra_events(1, 1), ClusterName, F12), {[{_, _, _, _, m3}], _, _} = process_ra_events(receive_ra_events(1, 1), ClusterName, F12),
ok. ok.
credit_api_v2(Config) ->
ClusterName = ?config(cluster_name, Config),
ServerId = ?config(node_id, Config),
ok = start_cluster(ClusterName, [ServerId]),
F0 = rabbit_fifo_client:init([ServerId], 4),
%% Enqueue 2 messages.
{ok, F1, []} = rabbit_fifo_client:enqueue(ClusterName, m1, F0),
{ok, F2, []} = rabbit_fifo_client:enqueue(ClusterName, m2, F1),
{_, _, F3} = process_ra_events(receive_ra_events(2, 0), ClusterName, F2),
CTag = <<"my-tag">>,
DC0 = 16#ff_ff_ff_ff,
DC1 = 0, %% = DC0 + 1 using 32 bit serial number arithmetic
{ok, F4} = rabbit_fifo_client:checkout(
%% initial_delivery_count in consumer meta means credit API v2.
CTag, 0, credited, #{initial_delivery_count => DC0}, F3),
%% assert no deliveries
{_, _, F5} = process_ra_events(receive_ra_events(), ClusterName, F4, [], [],
fun
(D, _) -> error({unexpected_delivery, D})
end),
%% Grant 1 credit.
{F6, []} = rabbit_fifo_client:credit(CTag, DC0, 1, false, _Echo0 = true, F5),
%% We expect exactly 1 message due to 1 credit being granted.
{[{_, _, _, _, m1}],
%% We expect a credit_reply action due to echo=true
[{credit_reply, CTag, DC1, _Credit0 = 0, _Available0 = 1, _Drain0 = false}],
F7} = process_ra_events(receive_ra_events(), ClusterName, F6),
%% Again, grant 1 credit.
%% However, because we still use the initial delivery count DC0, rabbit_fifo
%% wont' send us a new message since it already sent us m1 for that old delivery-count.
%% In other words, this credit top up simulates in-flight deliveries.
{F8, []} = rabbit_fifo_client:credit(CTag, DC0, 1, false, _Echo1 = true, F7),
{_NoMessages = [],
%% We still expect a credit_reply action due to echo=true
[{credit_reply, CTag, DC1, _Credit1 = 0, _Available1 = 1, _Drain1 = false}],
F9} = process_ra_events(receive_ra_events(), ClusterName, F8),
%% Grant 4 credits and drain.
{F10, []} = rabbit_fifo_client:credit(CTag, DC1, 4, true, _Echo2 = false, F9),
%% rabbit_fifo should advance the delivery-count as much as possible
%% consuming all credits due to drain=true and insufficient messages in the queue.
DC2 = DC1 + 4,
%% We expect to receive m2 which is the only message in the queue.
{[{_, _, _, _, m2}],
%% Even though echo=false, we still expect a credit_reply action due
%% drain=true and insufficient messages in the queue.
[{credit_reply, CTag, DC2, _Credit2 = 0, _Available2 = 0, _Drain2 = true}],
F11} = process_ra_events(receive_ra_events(), ClusterName, F10),
flush(),
%% Enqueue another message.
%% At this point the consumer credit should be all used up due to the drain.
{ok, F12, []} = rabbit_fifo_client:enqueue(ClusterName, m3, F11),
%% assert no deliveries
{_, _, F13} = process_ra_events(receive_ra_events(), ClusterName, F12, [], [],
fun
(D, _) -> error({unexpected_delivery, D})
end),
%% Grant 10 credits and receive the last message.
{F14, []} = rabbit_fifo_client:credit(CTag, DC2, 10, false, _Echo = false, F13),
?assertMatch(
{[{_, _, _, _, m3}],
%% Due to echo=false, we don't expect a credit_reply action.
_NoCreditReplyAction = [],
_F15}, process_ra_events(receive_ra_events(), ClusterName, F14)).
untracked_enqueue(Config) -> untracked_enqueue(Config) ->
ClusterName = ?config(cluster_name, Config), ClusterName = ?config(cluster_name, Config),
ServerId = ?config(node_id, Config), ServerId = ?config(node_id, Config),

View File

@ -19,7 +19,7 @@
-define(WAIT, 5000). -define(WAIT, 5000).
suite() -> suite() ->
[{timetrap, 15 * 60000}]. [{timetrap, 15 * 60_000}].
all() -> all() ->
[ [
@ -1712,11 +1712,6 @@ consume_from_replica(Config) ->
rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_testcase_queue, [Q]). rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_testcase_queue, [Q]).
consume_credit(Config) -> consume_credit(Config) ->
%% Because osiris provides one chunk on every read and we don't want to buffer
%% messages in the broker to avoid memory penalties, the credit value won't
%% be strict - we allow it into the negative values.
%% We can test that after receiving a chunk, no more messages are delivered until
%% the credit goes back to a positive value.
[Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), [Server | _] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename),
Ch = rabbit_ct_client_helpers:open_channel(Config, Server), Ch = rabbit_ct_client_helpers:open_channel(Config, Server),
@ -1736,40 +1731,55 @@ consume_credit(Config) ->
qos(Ch1, Credit, false), qos(Ch1, Credit, false),
subscribe(Ch1, Q, false, 0), subscribe(Ch1, Q, false, 0),
%% Receive everything %% We expect to receive exactly 2 messages.
DeliveryTags = receive_batch(), DTag1 = receive {#'basic.deliver'{delivery_tag = Tag1}, _} -> Tag1
after 5000 -> ct:fail({missing_delivery, ?LINE})
%% We receive at least the given credit as we know there are 100 messages in the queue end,
?assert(length(DeliveryTags) >= Credit), _DTag2 = receive {#'basic.deliver'{delivery_tag = Tag2}, _} -> Tag2
after 5000 -> ct:fail({missing_delivery, ?LINE})
%% Let's ack as many messages as we can while avoiding a positive credit for new deliveries end,
{ToAck, Pending} = lists:split(length(DeliveryTags) - Credit, DeliveryTags), receive {#'basic.deliver'{}, _} -> ct:fail({unexpected_delivery, ?LINE})
after 100 -> ok
[ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DeliveryTag,
multiple = false})
|| DeliveryTag <- ToAck],
%% Nothing here, this is good
receive
{#'basic.deliver'{}, _} ->
exit(unexpected_delivery)
after 1000 ->
ok
end, end,
%% Let's ack one more, we should receive a new chunk %% When we ack the 1st message, we should receive exactly 1 more message
ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = hd(Pending), ok = amqp_channel:cast(Ch1, #'basic.ack'{delivery_tag = DTag1,
multiple = false}), multiple = false}),
DTag3 = receive {#'basic.deliver'{delivery_tag = Tag3}, _} -> Tag3
%% Yeah, here is the new chunk! after 5000 -> ct:fail({missing_delivery, ?LINE})
receive end,
{#'basic.deliver'{}, _} -> receive {#'basic.deliver'{}, _} ->
ok ct:fail({unexpected_delivery, ?LINE})
after 5000 -> after 100 -> ok
exit(timeout)
end, end,
%% Whenever we ack 2 messages, we should receive exactly 2 more messages.
ok = consume_credit0(Ch1, DTag3),
rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_testcase_queue, [Q]). rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_testcase_queue, [Q]).
consume_credit0(_Ch, DTag)
when DTag > 50 ->
%% sufficiently tested
ok;
consume_credit0(Ch, DTagPrev) ->
%% Ack 2 messages.
ok = amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = DTagPrev,
multiple = true}),
%% Receive 1st message.
receive {#'basic.deliver'{}, _} -> ok
after 5000 -> ct:fail({missing_delivery, ?LINE})
end,
%% Receive 2nd message.
DTag = receive {#'basic.deliver'{delivery_tag = T}, _} -> T
after 5000 -> ct:fail({missing_delivery, ?LINE})
end,
%% We shouldn't receive more messages given that AMQP 0.9.1 prefetch count is 2.
receive {#'basic.deliver'{}, _} -> ct:fail({unexpected_delivery, ?LINE})
after 10 -> ok
end,
consume_credit0(Ch, DTag).
consume_credit_out_of_order_ack(Config) -> consume_credit_out_of_order_ack(Config) ->
%% Like consume_credit but acknowledging the messages out of order. %% Like consume_credit but acknowledging the messages out of order.
%% We want to ensure it doesn't behave like multiple, that is if we have %% We want to ensure it doesn't behave like multiple, that is if we have

View File

@ -25,6 +25,7 @@ groups() ->
{classic_queue, [], [ {classic_queue, [], [
all_messages_go_to_one_consumer, all_messages_go_to_one_consumer,
fallback_to_another_consumer_when_first_one_is_cancelled, fallback_to_another_consumer_when_first_one_is_cancelled,
fallback_to_another_consumer_when_first_one_is_cancelled_qos1,
fallback_to_another_consumer_when_exclusive_consumer_channel_is_cancelled, fallback_to_another_consumer_when_exclusive_consumer_channel_is_cancelled,
fallback_to_another_consumer_when_first_one_is_cancelled_manual_acks, fallback_to_another_consumer_when_first_one_is_cancelled_manual_acks,
amqp_exclusive_consume_fails_on_exclusive_consumer_queue amqp_exclusive_consume_fails_on_exclusive_consumer_queue
@ -32,6 +33,7 @@ groups() ->
{quorum_queue, [], [ {quorum_queue, [], [
all_messages_go_to_one_consumer, all_messages_go_to_one_consumer,
fallback_to_another_consumer_when_first_one_is_cancelled, fallback_to_another_consumer_when_first_one_is_cancelled,
fallback_to_another_consumer_when_first_one_is_cancelled_qos1,
fallback_to_another_consumer_when_exclusive_consumer_channel_is_cancelled, fallback_to_another_consumer_when_exclusive_consumer_channel_is_cancelled,
fallback_to_another_consumer_when_first_one_is_cancelled_manual_acks, fallback_to_another_consumer_when_first_one_is_cancelled_manual_acks,
basic_get_is_unsupported basic_get_is_unsupported
@ -165,6 +167,49 @@ fallback_to_another_consumer_when_first_one_is_cancelled(Config) ->
amqp_connection:close(C), amqp_connection:close(C),
ok. ok.
fallback_to_another_consumer_when_first_one_is_cancelled_qos1(Config) ->
{C, Ch} = connection_and_channel(Config),
Q = queue_declare(Ch, Config),
?assertEqual(#'basic.qos_ok'{},
amqp_channel:call(Ch, #'basic.qos'{prefetch_count = 1})),
CTag1 = <<"tag1">>,
CTag2 = <<"tag2">>,
amqp_channel:subscribe(Ch, #'basic.consume'{queue = Q,
consumer_tag = CTag1}, self()),
receive #'basic.consume_ok'{consumer_tag = CTag1} -> ok
after 5000 -> ct:fail(timeout_ctag1)
end,
amqp_channel:subscribe(Ch, #'basic.consume'{queue = Q,
consumer_tag = CTag2}, self()),
receive #'basic.consume_ok'{consumer_tag = CTag2} -> ok
after 5000 -> ct:fail(timeout_ctag2)
end,
Publish = #'basic.publish'{exchange = <<>>, routing_key = Q},
amqp_channel:cast(Ch, Publish, #amqp_msg{payload = <<"m1">>}),
amqp_channel:cast(Ch, Publish, #amqp_msg{payload = <<"m2">>}),
DTag1 = receive {#'basic.deliver'{consumer_tag = CTag1,
delivery_tag = DTag},
#amqp_msg{payload = <<"m1">>}} -> DTag
after 5000 -> ct:fail(timeout_m1)
end,
#'basic.cancel_ok'{consumer_tag = CTag1} = amqp_channel:call(Ch, #'basic.cancel'{consumer_tag = CTag1}),
receive #'basic.cancel_ok'{consumer_tag = CTag1} -> ok
after 5000 -> ct:fail(missing_cancel)
end,
amqp_channel:cast(Ch, #'basic.ack'{delivery_tag = DTag1}),
receive {#'basic.deliver'{consumer_tag = CTag2},
#amqp_msg{payload = <<"m2">>}} -> ok;
Unexpected -> ct:fail({unexpected, Unexpected})
after 5000 -> ct:fail(timeout_m2)
end,
amqp_connection:close(C).
fallback_to_another_consumer_when_first_one_is_cancelled_manual_acks(Config) -> fallback_to_another_consumer_when_first_one_is_cancelled_manual_acks(Config) ->
%% Let's ensure that although the consumer is cancelled we still keep the unacked %% Let's ensure that although the consumer is cancelled we still keep the unacked
%% messages and accept acknowledgments on them. %% messages and accept acknowledgments on them.
@ -292,7 +337,7 @@ queue_declare(Channel, Config) ->
consume({Parent, State, 0}) -> consume({Parent, State, 0}) ->
Parent ! {consumer_done, State}; Parent ! {consumer_done, State};
consume({Parent, {MessagesPerConsumer, MessageCount}, CountDown}) -> consume({Parent, {MessagesPerConsumer, MessageCount}, CountDown} = Arg) ->
receive receive
#'basic.consume_ok'{consumer_tag = CTag} -> #'basic.consume_ok'{consumer_tag = CTag} ->
consume({Parent, {maps:put(CTag, 0, MessagesPerConsumer), MessageCount}, CountDown}); consume({Parent, {maps:put(CTag, 0, MessagesPerConsumer), MessageCount}, CountDown});
@ -307,9 +352,9 @@ consume({Parent, {MessagesPerConsumer, MessageCount}, CountDown}) ->
consume({Parent, NewState, CountDown - 1}); consume({Parent, NewState, CountDown - 1});
#'basic.cancel_ok'{consumer_tag = CTag} -> #'basic.cancel_ok'{consumer_tag = CTag} ->
Parent ! {cancel_ok, CTag}, Parent ! {cancel_ok, CTag},
consume({Parent, {MessagesPerConsumer, MessageCount}, CountDown}); consume(Arg);
_ -> _ ->
consume({Parent, {MessagesPerConsumer, MessageCount}, CountDown}) consume(Arg)
after ?TIMEOUT -> after ?TIMEOUT ->
Parent ! {consumer_timeout, {MessagesPerConsumer, MessageCount}}, Parent ! {consumer_timeout, {MessagesPerConsumer, MessageCount}},
flush(), flush(),

View File

@ -8,11 +8,10 @@
-module(unit_access_control_SUITE). -module(unit_access_control_SUITE).
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/file.hrl").
-include_lib("amqp_client/include/amqp_client.hrl"). -include_lib("amqp_client/include/amqp_client.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-compile(export_all). -compile([export_all, nowarn_export_all]).
all() -> all() ->
[ [
@ -24,7 +23,7 @@ groups() ->
[ [
{parallel_tests, [parallel], [ {parallel_tests, [parallel], [
password_hashing, password_hashing,
unsupported_connection_refusal version_negotiation
]}, ]},
{sequential_tests, [], [ {sequential_tests, [], [
login_with_credentials_but_no_password, login_with_credentials_but_no_password,
@ -278,20 +277,37 @@ auth_backend_internal_expand_topic_permission(_Config) ->
), ),
ok. ok.
unsupported_connection_refusal(Config) -> %% Test AMQP 1.0 §2.2
passed = rabbit_ct_broker_helpers:rpc(Config, 0, version_negotiation(Config) ->
?MODULE, unsupported_connection_refusal1, [Config]). ok = rabbit_ct_broker_helpers:rpc(Config, ?MODULE, version_negotiation1, [Config]).
unsupported_connection_refusal1(Config) -> version_negotiation1(Config) ->
H = ?config(rmq_hostname, Config), H = ?config(rmq_hostname, Config),
P = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), P = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp),
[passed = test_unsupported_connection_refusal(H, P, V) ||
V <- [<<"AMQP",9,9,9,9>>, <<"AMQP",0,1,0,0>>, <<"XXXX",0,0,9,1>>]],
passed.
test_unsupported_connection_refusal(H, P, Header) -> [?assertEqual(<<"AMQP",0,1,0,0>>, version_negotiation2(H, P, Vsn)) ||
Vsn <- [<<"AMQP",0,1,0,0>>,
<<"AMQP",0,1,0,1>>,
<<"AMQP",0,1,1,0>>,
<<"AMQP",0,9,1,0>>,
<<"AMQP",0,0,8,0>>,
<<"XXXX",0,1,0,0>>,
<<"XXXX",0,0,9,1>>]],
[?assertEqual(<<"AMQP",3,1,0,0>>, version_negotiation2(H, P, Vsn)) ||
Vsn <- [<<"AMQP",1,1,0,0>>,
<<"AMQP",4,1,0,0>>,
<<"AMQP",9,1,0,0>>]],
[?assertEqual(<<"AMQP",0,0,9,1>>, version_negotiation2(H, P, Vsn)) ||
Vsn <- [<<"AMQP",0,0,9,2>>,
<<"AMQP",0,0,10,0>>,
<<"AMQP",0,0,10,1>>]],
ok.
version_negotiation2(H, P, Header) ->
{ok, C} = gen_tcp:connect(H, P, [binary, {active, false}]), {ok, C} = gen_tcp:connect(H, P, [binary, {active, false}]),
ok = gen_tcp:send(C, Header), ok = gen_tcp:send(C, Header),
{ok, <<"AMQP",0,0,9,1>>} = gen_tcp:recv(C, 8, 100), {ok, ServerVersion} = gen_tcp:recv(C, 8, 100),
ok = gen_tcp:close(C), ok = gen_tcp:close(C),
passed. ServerVersion.

View File

@ -73,6 +73,7 @@ def all_beam_files(name = "all_beam_files"):
"src/rabbit_queue_collector.erl", "src/rabbit_queue_collector.erl",
"src/rabbit_registry.erl", "src/rabbit_registry.erl",
"src/rabbit_resource_monitor_misc.erl", "src/rabbit_resource_monitor_misc.erl",
"src/rabbit_routing_parser.erl",
"src/rabbit_runtime.erl", "src/rabbit_runtime.erl",
"src/rabbit_runtime_parameter.erl", "src/rabbit_runtime_parameter.erl",
"src/rabbit_semver.erl", "src/rabbit_semver.erl",
@ -168,6 +169,7 @@ def all_test_beam_files(name = "all_test_beam_files"):
"src/rabbit_queue_collector.erl", "src/rabbit_queue_collector.erl",
"src/rabbit_registry.erl", "src/rabbit_registry.erl",
"src/rabbit_resource_monitor_misc.erl", "src/rabbit_resource_monitor_misc.erl",
"src/rabbit_routing_parser.erl",
"src/rabbit_runtime.erl", "src/rabbit_runtime.erl",
"src/rabbit_runtime_parameter.erl", "src/rabbit_runtime_parameter.erl",
"src/rabbit_semver.erl", "src/rabbit_semver.erl",
@ -260,6 +262,7 @@ def all_srcs(name = "all_srcs"):
"src/rabbit_registry.erl", "src/rabbit_registry.erl",
"src/rabbit_registry_class.erl", "src/rabbit_registry_class.erl",
"src/rabbit_resource_monitor_misc.erl", "src/rabbit_resource_monitor_misc.erl",
"src/rabbit_routing_parser.erl",
"src/rabbit_runtime.erl", "src/rabbit_runtime.erl",
"src/rabbit_runtime_parameter.erl", "src/rabbit_runtime_parameter.erl",
"src/rabbit_semver.erl", "src/rabbit_semver.erl",

View File

@ -8,7 +8,7 @@
-include("resource.hrl"). -include("resource.hrl").
%% Passed around most places %% Passed around most places
-record(user, {username, -record(user, {username :: rabbit_types:option(rabbit_types:username()),
tags, tags,
authz_backends}). %% List of {Module, AuthUserImpl} pairs authz_backends}). %% List of {Module, AuthUserImpl} pairs
@ -254,7 +254,7 @@
%% Max message size is hard limited to 512 MiB. %% Max message size is hard limited to 512 MiB.
%% If user configures a greater rabbit.max_message_size, %% If user configures a greater rabbit.max_message_size,
%% this value is used instead. %% this value is used instead.
-define(MAX_MSG_SIZE, 536870912). -define(MAX_MSG_SIZE, 536_870_912).
-define(store_proc_name(N), rabbit_misc:store_proc_name(?MODULE, N)). -define(store_proc_name(N), rabbit_misc:store_proc_name(?MODULE, N)).

Some files were not shown because too many files have changed in this diff Show More