akkadotnet / Alpakka

Akka Streams Connectors - Alpakka

Home Page:https://alpakka.getakka.net/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

AMQP Sink does not recover after RabbitMQ server restart

vasily-kirichenko opened this issue · comments

Run the following code

let system = ActorSystem.Create("test", ConfigurationFactory.Default())
    let mat = ActorMaterializer.Create(system, ActorMaterializerSettings.Create(system).WithSupervisionStrategy(Deciders.ResumingDecider))
    
    let connSettings = 
        AmqpConnectionDetails
            .Create("localhost", 5672)
            .WithAutomaticRecoveryEnabled(true)
            .WithNetworkRecoveryInterval(TimeSpan.FromSeconds 1.0)
            
    let queueName = "my-queue"
    let queueDeclaration = QueueDeclaration.Create(queueName).WithDurable(true).WithAutoDelete(false)
    let sink = AmqpSink.CreateSimple(AmqpSinkSettings.Create(connSettings).WithRoutingKey("my-queue").WithDeclarations(queueDeclaration))
    
    Source
        .From(seq { 1..1_000_000 })
        .Select(fun x -> x |> string |> ByteString.FromString)
        .To(sink)
        .Run(mat)
    |> ignore
    
    system.WhenTerminated.Wait()

Everything is OK, messages are being queued at ~20K/second rate.
Now restart RabbitMQ server. The following error is in the log

[ERROR][17-Jan-18 19:06:48][Thread 0012][[akka://test/user/StreamSupervisor-0/Flow-0-1-AmqpSink#1922506265]] Error in stage [AmqpSink]: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host.
Cause: System.IO.IOException: Unable to write data to the transport connection: An existing connection was forcibly closed by the remote host. ---> System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
   at System.Net.Sockets.NetworkStream.Write(Byte[] buffer, Int32 offset, Int32 size)
   --- End of inner exception stack trace ---
   at System.Net.Sockets.NetworkStream.Write(Byte[] buffer, Int32 offset, Int32 size)
   at System.IO.BufferedStream.Flush()
   at RabbitMQ.Client.Impl.SocketFrameHandler.WriteFrameSet(IList`1 frames)
   at RabbitMQ.Client.Impl.Command.TransmitAsFrameSet(Int32 channelNumber, Connection connection)
   at RabbitMQ.Client.Impl.SessionBase.Transmit(Command cmd)
   at RabbitMQ.Client.Impl.ModelBase.ModelSend(MethodBase method, ContentHeaderBase header, Byte[] body)
   at RabbitMQ.Client.Impl.ModelBase.BasicPublish(String exchange, String routingKey, Boolean mandatory, IBasicProperties basicProperties, Byte[] body)
   at Akka.Streams.Amqp.AmqpSinkStage.AmqpSinkStageLogic.<.ctor>b__3_0()
   at Akka.Streams.Implementation.Fusing.GraphInterpreter.ProcessEvent(Connection connection)
   at Akka.Streams.Implementation.Fusing.GraphInterpreter.Execute(Int32 eventLimit)

and the messages are not queued anymore.

I thought that WithAutomaticRecoveryEnabled(true) reconnects automatically, and WithSupervisionStrategy(Deciders.ResumingDecider) resumes the failing stream stage. As a result, I expected messages to continue being queued after server restart (and a single message to be lost).

you need to use RestartSink ,if you want your sink to restart when the connection drops, so the sink will be recreated with new connection to rabbit. Also u can combine them with KillSwitch so you can also gracefully stop.

something like this:

var source = Source.Queue<string>(0, OverflowStrategy.Backpressure);
var restartSink = RestartSink.WithBackoff(() =>
                {
                    Console.WriteLine("Restarting AMQP Sink.");
                    var amqpSink = AmqpSink.CreateSimple(
                        AmqpSinkSettings.Create(connectionSettings)
                            .WithRoutingKey(queueName)
                            .WithDeclarations(queueDeclaration,bindingDeclaration,exchangeDeclaration));
                    return amqpSink;
                }, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), 0.2);

var tupple = source
                    .Select(ByteString.FromString)
                    .ViaMaterialized(KillSwitches.Single<ByteString>(),Keep.Both)
                    .ToMaterialized(restartSink,Keep.Left)
                    .Run(mat);

                var sinkKillSwitch = tupple.Item2;
                var queueSource = tupple.Item1;

                sinkKillSwitch.Shutdown();

@bobanco Excellent, thanks! :) I completely forgot about RestartSink however read about it a week ago.

It works, but it uncover a bug in PostStop:

[ERROR][17-Jan-18 19:46:30][Thread 0003][[akka://test/user/StreamSupervisor-0/Flow-0-0-unknown-operation#1059276845]] Error during PostStop in [AmqpSink]
Cause: System.NullReferenceException: Object reference not set to an instance of an object.
   at Akka.Streams.Amqp.AmqpSinkStage.AmqpSinkStageLogic.PostStop()
   at Akka.Streams.Implementation.Fusing.GraphInterpreter.FinalizeStage(GraphStageLogic logic)

The NRE is fixed in #23