Unexpected exceptionsedit

When a client call throws an exception that the IConnction can not handle, this exception will bubble out the client as an UnexpectedElasticsearchClientException, regardless whether the client is configured to throw or not. An IConnection is in charge of knowning what exceptions it can recover from or not. The default IConnection that is based on WebRequest can and will recover from WebExceptions but others will be grounds for immediately exiting the pipeline.

var audit = new Auditor(() => Framework.Cluster
    .Nodes(10)
    .ClientCalls(r => r.SucceedAlways())
    .ClientCalls(r => r.OnPort(9201).FailAlways(new Exception("boom!")))
    .StaticConnectionPool()
    .Settings(s => s.DisablePing())
);
audit = await audit.TraceCall(
    new ClientCall {
        { AuditEvent.HealthyResponse, 9200 },
    }
);
audit = await audit.TraceUnexpectedException(
    new ClientCall {
        { AuditEvent.BadResponse, 9201 },
    },
    (e) =>
    {
        e.FailureReason.Should().Be(PipelineFailure.Unexpected);
        e.InnerException.Should().NotBeNull();
        e.InnerException.Message.Should().Be("boom!");
    }
);
e.FailureReason.Should().Be(PipelineFailure.Unexpected);
e.InnerException.Should().NotBeNull();
e.InnerException.Message.Should().Be("boom!");

Sometimes an unexpected exception happens further down in the pipeline, this is why we wrap them inside an UnexpectedElasticsearchClientException so that information about where in the pipeline the unexpected exception is not lost, here a call to 9200 fails using a webexception. It then falls over to 9201 which throws an hard exception from within IConnection. We assert that we can still see the audit trail for the whole coordinated request.

var audit = new Auditor(() => Framework.Cluster
    .Nodes(10)
#if DOTNETCORE
    .ClientCalls(r => r.OnPort(9200).FailAlways(new System.Net.Http.HttpRequestException("recover")))
#else
    .ClientCalls(r => r.OnPort(9200).FailAlways(new WebException("recover")))
#endif
    .ClientCalls(r => r.OnPort(9201).FailAlways(new Exception("boom!")))
    .StaticConnectionPool()
    .Settings(s => s.DisablePing())
);

audit = await audit.TraceUnexpectedException(
    new ClientCall {
        { AuditEvent.BadResponse, 9200 },
        { AuditEvent.BadResponse, 9201 },
    },
    (e) =>
    {
        e.FailureReason.Should().Be(PipelineFailure.Unexpected);
        e.InnerException.Should().NotBeNull();
        e.InnerException.Message.Should().Be("boom!");
    }
);

e.FailureReason.Should().Be(PipelineFailure.Unexpected);

e.InnerException.Should().NotBeNull();

e.InnerException.Message.Should().Be("boom!");

An unexpected hard exception on ping and sniff is something we do try to revover from and failover. Here pinging nodes on first use is enabled and 9200 throws on ping, we still fallover to 9201’s ping succeeds. However the client call on 9201 throws a hard exception we can not recover from

var audit = new Auditor(() => Framework.Cluster
    .Nodes(10)
    .Ping(r => r.OnPort(9200).FailAlways(new Exception("ping exception")))
    .Ping(r => r.OnPort(9201).SucceedAlways())
    .ClientCalls(r => r.OnPort(9201).FailAlways(new Exception("boom!")))
    .StaticConnectionPool()
    .AllDefaults()
);
audit = await audit.TraceUnexpectedException(
    new ClientCall {
        { AuditEvent.PingFailure, 9200 },
        { AuditEvent.PingSuccess, 9201 },
        { AuditEvent.BadResponse, 9201 },
    },
    (e) =>
    {
        e.FailureReason.Should().Be(PipelineFailure.Unexpected);
e.InnerException.Should().NotBeNull();
        e.InnerException.Message.Should().Be("boom!");
e.SeenExceptions.Should().NotBeEmpty();
        var pipelineException = e.SeenExceptions.First();
        pipelineException.FailureReason.Should().Be(PipelineFailure.PingFailure);
        pipelineException.InnerException.Message.Should().Be("ping exception");
var pingException = e.AuditTrail.First(a => a.Event == AuditEvent.PingFailure).Exception;
        pingException.Should().NotBeNull();
        pingException.Message.Should().Be("ping exception");

    }
);