rascal 17.0.2 → 18.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ ## 18.0.1
4
+
5
+ - Removed console.log when the channel pool destroyed a channel
6
+ - Add streams example
7
+
8
+ ## 18.0.0
9
+
10
+ - Fixes https://github.com/onebeyond/rascal/issues/227 by requiring special characters to be URL encoded.
11
+ - Consolidated broker and management url configuration logic
12
+
3
13
  ## 17.0.2
4
14
 
5
15
  - Update guidesmiths references to onebeyond.
package/README.md CHANGED
@@ -47,9 +47,9 @@ Rascal extends the existing [RabbitMQ Concepts](https://www.rabbitmq.com/tutoria
47
47
 
48
48
  A **publication** is a named configuration for publishing a message, including the destination queue or exchange, routing configuration, encryption profile and reliability guarantees, message options, etc. A **subscription** is a named configuration for consuming messages, including the source queue, encryption profile, content encoding, delivery options (e.g. acknowledgement handling and prefetch), etc. These must be [configured](#configuration) and supplied when creating the Rascal broker. After the broker has been created the subscriptions and publications can be retrieved from the broker and used to publish and consume messages.
49
49
 
50
- ### Breaking Changes in Rascal@14
50
+ ### Breaking Changes
51
51
 
52
- Rascal@14 waits for inflight messages to be acknowledged before closing subscriber channels. Prior to this version Rascal just waited an arbitrary amount of time. If your application does not acknowledge a message for some reason (quite likely in tests) calling `subscription.cancel`, `broker.unsubscribeAll`, `broker.bounce`, `broker.shutdown` or `broker.nuke` will wait indefinitely. You can specify a `closeTimeout` in your subscription config, however if this is exceeded the `subscription.cancel` and `broker.unsubscribeAll` methods will yield an error via callback or rejection, while the `broker.bounce`, `broker.shutdown` and `broker.nuke` methods will emit an error event, but attempt to continue. In both cases the error will have a code of `ETIMEDOUT`.
52
+ Please refer to the [Change Log](https://github.com/onebeyond/rascal/blob/master/CHANGELOG.md)
53
53
 
54
54
  ### Special Note
55
55
 
@@ -318,6 +318,7 @@ The simplest way to specify a connection is with a url
318
318
  }
319
319
  }
320
320
  ```
321
+ As of Rascal v18.0.0 you must URL encode special characters appearing in the username, password and vhost, e.g. `amqp://guest:secr%23t@broker.example.com:5672/v1?heartbeat=10`
321
322
 
322
323
  Alternatively you can specify the individual connection details
323
324
 
@@ -345,6 +346,8 @@ Alternatively you can specify the individual connection details
345
346
  }
346
347
  ```
347
348
 
349
+ Special characters do not need to be encoded when specified in this form.
350
+
348
351
  Any attributes you add to the "options" sub document will be converted to query parameters. Any attributes you add in the "socketOptions" sub document will be passed directly to amqplib's connect method (which hands them off to `net` or `tls`. Providing you merge your configuration with the default configuration `rascal.withDefaultConfig(config)` you need only specify the attributes you want to override
349
352
 
350
353
  ```json
@@ -459,7 +462,7 @@ The AMQP protocol doesn't support assertion or checking of vhosts, so Rascal use
459
462
  }
460
463
  ```
461
464
 
462
- Rascal uses [superagent](https://github.com/visionmedia/superagent) under the hood. URL configuration is supported.
465
+ Rascal uses [superagent](https://github.com/visionmedia/superagent) under the hood. URL configuration is also supported.
463
466
 
464
467
  ```json
465
468
  {
@@ -750,6 +753,62 @@ To define a queue with extensions such as `x-queue-type` add arguments to the op
750
753
 
751
754
  Refer to the [amqplib](https://www.squaremobius.net/amqp.node/channel_api.html) documentation for further queue options.
752
755
 
756
+ #### streams
757
+
758
+ Rascal supports [RabbitMQ Streams](https://www.rabbitmq.com/docs/streams) via x-queue-type argument, i.e.
759
+
760
+ ```json
761
+ {
762
+ "queues": {
763
+ "q1": {
764
+ "options": {
765
+ "arguments": {
766
+ "x-queue-type": "stream"
767
+ }
768
+ }
769
+ }
770
+ }
771
+ }
772
+ ```
773
+
774
+ The [Stream Plugin](https://www.rabbitmq.com/docs/stream) and associated binary protocol extension are not supported.
775
+
776
+ Streams are **not** a replacement for regular messaging - instead they are best suited for when you can tolerate occasional message loss and need for higher throughput, such as sampling web based analytics.
777
+
778
+ When working with streams you need to think carefully about [data retention](https://www.rabbitmq.com/docs/streams#retention). Unless you specify retention configuration, messages will never be deleted and eventually you will run out of space. Conversely, if you automatically delete messages based on queue size or age, they may be lost without ever being read.
779
+
780
+ You also need to think about how you will [track the consumer offset](https://www.rabbitmq.com/blog/2021/09/13/rabbitmq-streams-offset-tracking). Typically you will need to store this in a database after successfully processing the message and use it to tell the broker where to resume from after your application restarts. For example...
781
+
782
+ ```js
783
+ const initialOffset = (await loadOffset('/my-queue')) || 'first';
784
+
785
+ const overrides = {
786
+ options: {
787
+ arguments: {
788
+ 'x-stream-offset': initialOffset
789
+ }
790
+ }
791
+ };
792
+
793
+ const subscription = await broker.subscribe('/my-queue', overrides);
794
+
795
+ subscription.on('message', async (message, content, ackOrNack) => {
796
+ const currentOffset = message.properties.headers['x-stream-offset'];
797
+ try {
798
+ await handleMessage(content);
799
+ await updateOffset('/my-queue', currentOffset);
800
+ } catch (err) {
801
+ await handleError('/my-queue', currentOffset, err);
802
+ } finally {
803
+ ackOrNack(); // Streams do not support nack so do not pass the error argument
804
+ }
805
+ });
806
+ ```
807
+
808
+ However, if your application is offline for too long, and messages are still being published to the stream, it may not be able to resume from where you left off, since those messages may have been deleted. Furthremore, if your application consumes messages concurrently, you need to think about how you will recover should one fail. If you naively override the previouly saved offset, you may be replacing a higher/later offset with an lower/older one, causing in your application to restart from the wrong point. Finally, you also need to decide what to do if the message cannot be processed. You cannot simply replay the message since you are working with a stream, rather than a classic queue. You could cancel the subscription and resume from the current offset, but this will lead to duplicates if you have been consuming messages concurrently. Alternatively you could republish the failures to a dead letter queue and process them separately.
809
+
810
+ For the above reasons, we only recommend considering streams when you genuinely need the extra throughput.
811
+
753
812
  #### bindings
754
813
 
755
814
  You can bind exchanges to exchanges, or exchanges to queues.
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "streams",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 1,
5
+ "requires": true,
6
+ "dependencies": {
7
+ "random-readable": {
8
+ "version": "1.0.1",
9
+ "resolved": "https://registry.npmjs.org/random-readable/-/random-readable-1.0.1.tgz",
10
+ "integrity": "sha512-Y++VltLA4yRsvFDAPbODh9hMw7cfkng+c/S+44ob6xGt0itLr8s6VhANl7kY7igEv3igPgzdc+T8EhBjQWjd9g=="
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "streams",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "random-readable": "^1.0.1"
13
+ }
14
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "../../lib/config/schema.json",
3
+ "vhosts": {
4
+ "/": {
5
+ "publicationChannelPools": {
6
+ "regularPool": {
7
+ "max": 10,
8
+ "min": 10,
9
+ "evictionRunIntervalMillis": 1000,
10
+ "idleTimeoutMillis": 5000,
11
+ "autostart": true
12
+ }
13
+ },
14
+ "connection": {
15
+ "socketOptions": {
16
+ "timeout": 1000
17
+ }
18
+ },
19
+ "queues": {
20
+ "demo_stream": {
21
+ "options": {
22
+ "arguments": {
23
+ "x-queue-type": "stream",
24
+ "x-max-length-bytes": 10485760
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "publications": {
30
+ "demo_pub": {
31
+ "queue": "demo_stream",
32
+ "confirm": false
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,38 @@
1
+ const Rascal = require('../..');
2
+ const config = require('./publisher-config');
3
+ const random = require('random-readable');
4
+ const max = parseInt(process.argv[2], 10) || Infinity;
5
+
6
+ Rascal.Broker.create(Rascal.withDefaultConfig(config), (err, broker) => {
7
+ if (err) throw err;
8
+
9
+ broker.on('error', console.error);
10
+
11
+ let count = 0;
12
+
13
+ const stream = random
14
+ .createRandomStream()
15
+ .on('error', console.error)
16
+ .on('data', (data) => {
17
+ broker.publish('demo_pub', data, (err, publication) => {
18
+ if (err) throw err;
19
+ else if (count >= max) stream.destroy();
20
+ else count++;
21
+ publication.on('error', console.error);
22
+ });
23
+ })
24
+ .on('close', () => {
25
+ console.log(`Published ${count} messages`)
26
+ broker.shutdown();
27
+ }); ;
28
+
29
+ broker.on('busy', (details) => {
30
+ console.log(Date.now(), `Pausing vhost: ${details.vhost} (mode: ${details.mode}, queue: ${details.queue}, size: ${details.size}, borrowed: ${details.borrowed}, available: ${details.available})`);
31
+ stream.pause();
32
+ });
33
+
34
+ broker.on('ready', (details) => {
35
+ console.log(Date.now(), `Resuming vhost: ${details.vhost} (mode: ${details.mode}, queue: ${details.queue}, size: ${details.size}, borrowed: ${details.borrowed}, available: ${details.available})`);
36
+ stream.resume();
37
+ });
38
+ });
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../lib/config/schema.json",
3
+ "vhosts": {
4
+ "/": {
5
+ "connection": {
6
+ "socketOptions": {
7
+ "timeout": 1000
8
+ }
9
+ },
10
+ "queues": {
11
+ "demo_stream": {
12
+ "options": {
13
+ "arguments": {
14
+ "x-queue-type": "stream",
15
+ "x-max-length-bytes": 10485760
16
+ }
17
+ }
18
+ }
19
+ },
20
+ "subscriptions": {
21
+ "demo_sub": {
22
+ "queue": "demo_stream",
23
+ "prefetch": 250
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,25 @@
1
+ const Rascal = require('../..');
2
+ const config = require('./subscriber-config');
3
+ const offset = parseInt(process.argv[2], 10) || 'first';
4
+
5
+ Rascal.Broker.create(Rascal.withDefaultConfig(config), (err, broker) => {
6
+ if (err) throw err;
7
+
8
+ broker.on('error', console.error);
9
+
10
+ const overrides = {
11
+ options: {
12
+ arguments: {
13
+ 'x-stream-offset': offset
14
+ }
15
+ }
16
+ };
17
+
18
+ broker.subscribe('demo_sub', overrides, (err, subscription) => {
19
+ if (err) throw err;
20
+ subscription.on('message', (message, content, ackOrNack) => {
21
+ console.log(`Received message: ${message.properties.headers['x-stream-offset']}`)
22
+ ackOrNack();
23
+ });
24
+ });
25
+ });
package/lib/amqp/Vhost.js CHANGED
@@ -201,8 +201,7 @@ function Vhost(vhostConfig, components) {
201
201
  createChannelWhenInitialised(options.confirm, (err, channel) => {
202
202
  if (err) return deferRejection(reject, err);
203
203
  if (!channel) return deferRejection(reject, new Error('Vhost is shutting down'));
204
- const destroyChannel = _.once((err) => {
205
- console.log('Destroying channel', err);
204
+ const destroyChannel = _.once(() => {
206
205
  debug('Destroying %s channel: %s for vhost: %s due to error or close event', mode, channel._rascal_id, vhostConfig.name);
207
206
  channel._rascal_closed = true;
208
207
  if (pool.isBorrowedResource(channel)) {
@@ -91,7 +91,7 @@ module.exports = _.curry((rascalConfig, next) => {
91
91
  } = new URL(connectionString);
92
92
  const options = Array.from(searchParams).reduce((attributes, entry) => ({ ...attributes, [entry[0]]: entry[1] }), {});
93
93
  return {
94
- protocol, hostname, port, user, password, vhost, options,
94
+ protocol, hostname: decodeURIComponent(hostname), port, user: decodeURIComponent(user), password: decodeURIComponent(password), vhost: decodeURIComponent(vhost), options,
95
95
  };
96
96
  }
97
97
 
@@ -105,10 +105,14 @@ module.exports = _.curry((rascalConfig, next) => {
105
105
  }
106
106
 
107
107
  function configureManagementConnection(vhostConfig, vhostName, connection) {
108
- _.defaultsDeep(connection.management, { hostname: connection.hostname });
109
- const auth = connection.management.auth || getAuth(connection.management.user, connection.management.password) || getAuth(connection.user, connection.password);
110
- connection.management.url = connection.management.url || url.format({ ...connection.management, auth });
111
- connection.management.loggableUrl = connection.management.url.replace(/:[^:]*?@/, ':***@');
108
+ connection.management = _.isString(connection.management) ? { url: connection.management } : connection.management;
109
+ const attributesFromUrl = parseConnectionUrl(connection.management.url);
110
+ const attributesFromConfig = getConnectionAttributes(connection.management);
111
+ const defaults = { user: connection.user, password: connection.password, hostname: connection.hostname };
112
+
113
+ const connectionAttributes = _.defaultsDeep({ options: null }, attributesFromUrl, attributesFromConfig, defaults);
114
+ setConnectionAttributes(connection.management, connectionAttributes);
115
+ setConnectionUrls(connection.management);
112
116
  }
113
117
 
114
118
  function setConnectionAttributes(connection, attributes, defaults) {
@@ -120,7 +124,6 @@ module.exports = _.curry((rascalConfig, next) => {
120
124
  const auth = getAuth(connection.user, connection.password);
121
125
  const pathname = connection.vhost === '/' ? '' : connection.vhost;
122
126
  const query = connection.options;
123
-
124
127
  connection.url = url.format({
125
128
  slashes: true, ...connection, auth, pathname, query,
126
129
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rascal",
3
- "version": "17.0.2",
3
+ "version": "18.0.1",
4
4
  "description": "A config driven wrapper for amqplib supporting multi-host connections, automatic error recovery, redelivery flood protection, transparent encryption / decryption, channel pooling and publication timeouts",
5
5
  "main": "index.js",
6
6
  "dependencies": {