How I can send additional header when configuring RabbitMQ queue? - masstransit

I want to use Single Active Consumer feature of RabbitMQ as described here: https://www.rabbitmq.com/consumers.html#single-active-consumer.
Single active consumer can be enabled when declaring a queue, with the x-single-active-consumer argument set to true, e.g. with the Java client:
Channel ch = ...;
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-single-active-consumer", true);
ch.queueDeclare("my-queue", false, false, false, arguments);
I couldn't find a way to add custom header at the moment of queue declaration.
I am configuring my endpoints like this:
busConfigurator.UsingRabbitMq((context, rabbitConfig) =>
{
// ...
rabbitConfig.ReceiveEndpoint(endpointName, (IReceiveEndpointConfigurator a) =>
{
a.ConfigureConsumer(context, consumerType);
// a.ConfigureSend(...)
// a.ConfigureReceive(...)
});
});
With ConfigureSend and ConfigureReceive I can add/read headers to the message, but I need to add x-single-active-consumer when queue is created in rabbit.

I found the issue, I was casting endpoint configurator to IReceiveEndpointConfigurator, while it should be IRabbitMqReceiveEndpointConfigurator (which is default type)
Correct code:
busConfigurator.UsingRabbitMq((context, rabbitConfig) =>
{
// ...
rabbitConfig.ReceiveEndpoint(endpointName, (a) =>
{
a.SingleActiveConsumer = true;
a.ConfigureConsumer(context, consumerType);
});
});

Related

Configure DeadLetter queue for send endpoint

I am trying to configure a Producer to send a message to a Consumer that has a deadletter queue configured. The Producer is using a SendEndpoint (Or rather the request/response pattern), but I get an exception from RabbitMQ.
I have the following consumer:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMassTransit(x =>
{
x.AddConsumer<SomeMessageRequestConsumer>();
x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(busConfig =>
{
busConfig.Host(new Uri("rabbitmq://rabbit#localhost"), "/", hostConfigurator =>
{
hostConfigurator.Password("Guest");
hostConfigurator.Username("Guest");
});
busConfig.ReceiveEndpoint(nameof(SomeMessage), x =>
{
x.ConfigureConsumer<SomeMessageRequestConsumer>(provider);
x.Durable = false;
x.ConfigureConsumeTopology = false;
x.BindDeadLetterQueue("SomeMessageDeadLetter", "SomeMessageDeadLetter", null);
});
}));
});
services.AddMassTransitHostedService();
}
I have the following Producer:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IReplyToClientFactory, ReplyToClientFactory>();
services.AddMassTransit(x =>
{
x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(busConfig =>
{
busConfig.Host(new Uri("rabbitmq://rabbit#localhost"), "/", hostConfigurator =>
{
hostConfigurator.Password("Guest");
hostConfigurator.Username("Guest");
});
}));
});
services.AddMassTransitHostedService();
}
In the Producer project I have a controller that send the message like so:
public ProducerController(IReplyToClientFactory clientFactory)
{
this.clientFactory = clientFactory;
}
[HttpPost]
public async Task<IActionResult> Post(CancellationToken cancellationToken)
{
var serviceAddress = new Uri($"queue:{nameof(SomeMessage)}?durable=false");
var client = this.clientFactory.GetFactory().CreateRequestClient<SomeMessage>(serviceAddress);
var (successResponse, failResponse) = await client.GetResponse<SomeMessageSuccessResponse, SomeMessageFailResponse>(new SomeMessage()
{
Text = "Hello",
}, cancellationToken, TimeSpan.FromSeconds(5));
return Ok();
}
I get the following error on RabbitMQ :
operation queue.declare caused a channel exception precondition_failed: inequivalent arg 'x-dead-letter-exchange' for queue 'SomeMessage' in vhost '/': received none but current is the value 'SomeMessageDeadLetter' of type 'longstr'
I have tried to configure the deadletter on the Publish, Send and Message Topologies but with no success. Is what I am trying to do possible or am I chasing the wind here?
You could change the destination address from a queue to an exchange, to decouple your producer from the consumer queue configuration. To send to the exchange, changed your address format to:
$"exchange:{nameof(SomeMessage)}"
That way, you don't need to know the queue configuration to send the request.

rsocket-js routing fireAndForget to Spring Boot #MessageMapping

As I understand RSocket-JS supports routing messages using encodeCompositeMetadata and encodeRoute, however, I cannot get the server to accept a fireAndForget message. The server constantly logs the following message:
o.s.m.r.a.support.RSocketMessageHandler : No handler for fireAndForget to ''
This is the server mapping I am trying to trigger:
#Controller
public class MockController {
private static final Logger LOGGER = LoggerFactory.getLogger(MockController.class);
#MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(MockData mockData) {
LOGGER.info("fireAndForget: {}", mockData);
return Mono.empty();
}
}
This is the TypeScript code that's trying to make the connection:
client.connect().subscribe({
onComplete: socket => {
console.log("Connected to socket!")
socket.fireAndForget({
data: { someData: "Hello world!" },
metadata: encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute("fire-and-forget")]])
});
},
onError: error => console.error(error),
onSubscribe: cancel => {/* call cancel() to abort */ }
});
I've also tried adding the route in other ways (metadata: String.fromCharCode('route'.length)+'route') I found on the internet, but none seem to work.
What do I need to do to format the route in a way that the Spring Boot server recognizes it and can route the message correctly?
Binary only communication when using CompositeMetadata
Please make sure that you have configured your ClientTransport with binary codecs as follows:
new RSocketWebSocketClient(
{
url: 'ws://<host>:<port>'
},
BufferEncoders,
),
Having Binary encoders you will be able to properly send your routes using composite metadata.
Also, please make sure that you have configured metadataMimeType as:
...
const metadataMimeType = MESSAGE_RSOCKET_COMPOSITE_METADATA.string; // message/x.rsocket.composite-metadata.v0
new RSocketClient<Buffer, Buffer>({
setup: {
...
metadataMimeType,
},
transport: new RSocketWebSocketClient(
{
url: 'ws://<host>:<port>',
},
BufferEncoders,
),
});
Note, once you enabled BufferEncoders your JSONSeriallizer will not work and you would need to encode your JSON to binary yours selves ( I suggest doing that since in the future versions we will remove support of Serializers concept completely). Therefore, your request has to be adjusted as it is in the following example:
client.connect().subscribe({
onComplete: socket => {
console.log("Connected to socket!")
socket.fireAndForget({
data: Buffer.from(JSON.stringify({ someData: "Hello world!" })),
metadata: encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute("fire-and-forget")]])
});
},
onError: error => console.error(error),
onSubscribe: cancel => {/* call cancel() to abort */ }
});
Use #Payload annotation for your payload at spring backend
Also, to handle any data from the client and to let Spring know that the specified parameter argument is your incoming request data, you have to annotate it with the #Payload annotation:
#Controller
public class MockController {
private static final Logger LOGGER = LoggerFactory.getLogger(MockController.class);
#MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(#Payload MockData mockData) {
LOGGER.info("fireAndForget: {}", mockData);
return Mono.empty();
}
}

Add identifier to websocket

I am using the Node.js ws library, to listen to events in user accounts on a 3rd party API. For each user, I open a websocket to listen to the events in the user's account.
Turns out, the 3rd-party API doesn't provide a userID for each event, so if I have 10 websocket connections to user-accounts, I cannot determine which account an event came from.
I have access to a unique userId prior to starting each of my connections.
Is there a way to append or wrap the websocket connection with the userId identifier, to each connection I make, such that when I receive an event, I can access the custom identifier, and subsequently know which user's account the event came from?
The code below is a mix of real code, and pseudocode (i.e customSocket)
const ws = new WebSocket('wss://thirdparty-api.com/accounts', {
port: 8080,
});
ws.send(
JSON.stringify({
action: 'authenticate',
data: {
oauth_token: access_token,
},
})
);
// wrap and attach data here (pseudocode at top-level)
customSocket.add({userId,
ws.send(
JSON.stringify({
action: 'listen',
data: {
streams: ['action_updates'],
},
})
)
})
// listen for wrapper data here, pseudocode at top level
customSocket.emit((customData) {
ws.on('message', function incoming(data) {
console.log('incoming -> data', data.toString());
})
console.log('emit -> customData', customData);
})
Looking at the socket.io library, the namespace feature may solve for this, but I can't determine if that's true or not. Below is an example in their documentation:
// your application has multiple tenants so you want to dynamically create one namespace per tenant
const workspaces = io.of(/^\/\w+$/);
workspaces.on('connection', socket => {
const workspace = socket.nsp;
workspace.emit('hello');
});
// this middleware will be assigned to each namespace
workspaces.use((socket, next) => {
// ensure the user has access to the workspace
next();
});
I found a solution to this which is fairly simple. First create a message handler function:
const eventHandler = (uid, msg) => {
console.log(`${uid} did ${msg}`);
};
Then, when you create the websocket for the given user, wrap the .on event with the handler:
const createSocketForUser = (uid, eventHandler) => {
const socket = new WebSocket(/* ... */);
socket.onmessage = (msg) => {
eventHandler(uid, msg)
};
return socket;
}

MassTransit sends a durable message when a non durable one is configured

I'm trying to send a message to an queue. The queue exists already and is configured as non durable. Here's my code:
ServiceBus = Bus.Factory.CreateUsingRabbitMq(sbc =>
{
sbc.PurgeOnStartup = true;
sbc.Durable = false;
sbc.Exclusive = false;
sbc.Host(new Uri($"rabbitmq://{RabbitMqHost}"), cfg =>
{
cfg.ConfigureRabbitMq();
});
});
ServiceBus.Request(
new Uri(serviceUri),
new EngineStartingMessage() { Version = ApplicationConfig.SystemVersion },
rCfg =>
{
rCfg.Durable = false;
rCfg.Timeout = new TimeSpan(0, 0, 30);
rCfg.Handle<EngineStartingResponse>(async hContext =>
{
//Response handling
});
});
As you can see Durable is set to false. On ServiceBus.Request I get the following exception:
The AMQP operation was interrupted: AMQP close-reason, initiated by
Peer, code=406, text="PRECONDITION_FAILED - inequivalent arg 'durable'
for exchange 'QUEUENAMEHERE' in vhost '/': received 'true' but current
is 'false'", classId=40, methodId=10, cause=
Any ideas why the message is still sent as durable?
That Durable flag only specifies that the particular request message should not be persisted to disk.
If you want to fix this, add ?durable=false to the serviceUri, to match what's being specified at the receive endpoint which handles the request.

calling a method inside async callback

I am using kafka in my project using kafka-node package...
I have introduced a method and inside it i am trying to use a kafka module for eg:
Meteor.methods
kafka: (topic, message) ->
if(Meteor.isServer)
message = JSON.stringify(message)
kafka = Meteor.npmRequire 'kafka-node'
HighLevelProducer = kafka.HighLevelProducer
Client = kafka.Client
client = new Client
producer = new HighLevelProducer(client)
payloads =[{topic: topic, messages: [message]}]
producer.on 'ready', ->
producer.send payloads, (error,data) ->
if not error
HighLevelConsumer = kafka.HighLevelConsumer
Client = kafka.Client
client = new Client('localhost:2181')
topics = [ { topic: topic } ]
options =
autoCommit: true
fetchMaxWaitMs: 1000
fetchMaxBytes: 1024 * 1024
consumer = new HighLevelConsumer(client, topics, options)
consumer.on 'message',(message) ->
console.log message.value
#Meteor.call 'saveMessage', message.value, (error,data) ->
return
consumer.on 'error', (err) ->
console.log 'error', err
return
producer.on 'error', (err) ->
console.log 'error', err
Everything was fine until i decided to use meteor.call and call a method to save that message..
It gives me this error.
Meteor code must always run within a Fiber. Try wrapping callbacks
that you pass to non-Meteor libraries with Meteor.bindEnvironment
I tried encapsulating it inside Fiber, used Meteor.wrapAsync(), Neither helped,
Please guys can you help me, i am having difficult time solving this issue...
If you're using node style callbacks, you can use Meteor.bindEnvironment around the callback. For example:
let Sockets = new Mongo.Collection('connections');
function createConnection (name) {
check(name, String);
let socket = net.connect(23, '192.168.1.3', Meteor.bindEnvironment(function () {
Sockets.upsert({ name: name }, { $set: { status: 'connected' } });
}));
}

Resources