dotnet / Docker.DotNet

:whale: .NET (C#) Client Library for Docker API
https://www.nuget.org/packages/Docker.DotNet/
MIT License
2.26k stars 382 forks source link

Exception when simultaneous read/write to stdin/stdout #448

Open aplocher opened 4 years ago

aplocher commented 4 years ago

Output of dotnet --info:

.NET Core SDK (reflecting any global.json):
 Version:   3.1.101
 Commit:    b377529961

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.18363
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.1.101\

Host (useful for support):
  Version: 3.1.1
  Commit:  a1388f194c

.NET Core SDKs installed:
  2.1.202 [C:\Program Files\dotnet\sdk]
  2.1.504 [C:\Program Files\dotnet\sdk]
  2.1.509 [C:\Program Files\dotnet\sdk]
  2.1.802 [C:\Program Files\dotnet\sdk]
  3.0.100 [C:\Program Files\dotnet\sdk]
  3.1.100 [C:\Program Files\dotnet\sdk]
  3.1.101 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.15 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.14 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.15 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.1.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.15 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.1.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 3.1.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

What version of Docker.DotNet?:

Tested with latest master and v3.125.2

Steps to reproduce the issue:

var client = new DockerClientConfiguration(new Uri("https://docker-server:2376")).CreateClient();

// Create Container
var response = await client.Containers.CreateContainerAsync(new CreateContainerParameters
{
    Image = "jbarlow83/ocrmypdf:latest",
    AttachStdin = true,
    AttachStdout = true,
    AttachStderr = false,
    StdinOnce = true,
    OpenStdin = true,
    HostConfig = new HostConfig
    {
        AutoRemove = true,
        CPUCount = 1
    },
    Cmd = new string[] { "--redo-ocr", "-", "-" }
});

var inputFile = @"C:\temp\test.pdf";
var outputFile = @"C:\temp\test-output.pdf";

var attachParams = new ContainerAttachParameters
{
    Stderr = false,
    Stdin = true,
    Stdout = true,
    Stream = true
};

// Attach Container
using (var stream = await client.Containers.AttachContainerAsync(response.ID, false, attachParams))
{
    var inputBytes = File.ReadAllBytes(inputFile);

    // Start Container
    await client.Containers.StartContainerAsync(response.ID, new ContainerStartParameters());

    var outputStream = new MemoryStream();

    // Write bytes to STDIN
    var taskWrite = Task.Run(async () =>
    {
        await stream.WriteAsync(inputBytes, 0, inputBytes.Length, CancellationToken.None);
    });

    // Read bytes from STDOUT to MemoryStream
    var taskRead = Task.Run(async () =>
    {
        // Copied from MultiplexedStream.CopyOutputToAsync method:
        var buffer = new byte[81920];

        for (; ; )
        {
            // Exception is thrown here after "await WaitContainerAsync" below:
            var result = await stream.ReadOutputAsync(buffer, 0, buffer.Length, default(CancellationToken)).ConfigureAwait(false);

            if (result.EOF)
            {
                return;
            }

            await outputStream.WriteAsync(buffer, 0, result.Count, default(CancellationToken)).ConfigureAwait(false);
        }
        // End CopyOutputToAsync
    });

    await taskWrite;
    stream.CloseWrite();
    await client.Containers.WaitContainerAsync(response.ID);
    await taskRead;

    // Write MemoryStream to file
    if (File.Exists(outputFile))
        File.Delete(outputFile);

    outputStream.Position = 0;
    using (var filestream = File.Create(outputFile))
    {
        await outputStream.CopyToAsync(filestream);
    }

What actually happened?:

When I provide a large(ish) input, the following exception will occur on ReadOutputAsync

System.IO.IOException: 'Unable to read data from the transport connection: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.'

   at System.Net.Security._SslStream.EndRead(IAsyncResult asyncResult)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncTrimPromise`1.Complete(TInstance thisRef, Func`3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization)
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Docker.DotNet.MultiplexedStream.<ReadOutputAsync>d__12.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()

What did you expect to happen?:

I expected the read to succeed, like it does with smaller files.

Additional information:

This docker instance takes a PDF file through STDIN and produces another PDF file (OCR'ed) to STDOUT.

This docker client command is exactly what I'm trying to replicate with Docker.DotNet but am unable to (with LARGER PDFs, small files work fine):

type c:\temp\test.pdf | docker --tlsverify -H "docker-server:2376" run -i -a stdin -a stdout --cpus 1 jbarlow83/ocrmypdf --redo-ocr - - > c:\temp\test-output.pdf

I've used various combinations of the code from issue #223 but he seemed to solve his issue (which was different from mine).

I also thought the Pull-Request from Issue #388 might be helpful, but when I use Peek I get an exception "_stream isn't a peekable stream"

This works without an exception with an 8mb test file, but another 20mb test file fails with this exception. Both files work fine from the docker CLI.

Basically, I'm wondering if I'm doing something wrong (and if so, how can I fix it?) - or is this an issue with Docker.DotNet? Thank you!

aplocher commented 4 years ago

I modified the code to use the "Exec" stuff instead, and I'm having the same exact problem.

IDockerClient client = new DockerClientConfiguration(new Uri("https://docker-server:2376"), credentials).CreateClient();

// Create Container
var response = await client.Containers.CreateContainerAsync(new CreateContainerParameters
{
    Image = "jbarlow83/ocrmypdf:latest",
    AttachStdin = true,
    AttachStdout = true,
    AttachStderr = false,
    OpenStdin = true,
    HostConfig = new HostConfig
    {
        CPUCount = 4
    },
    Entrypoint = new string[] { "bash" }
});

await client.Containers.StartContainerAsync(response.ID, new ContainerStartParameters());

var execContainer = await client.Exec.ExecCreateContainerAsync(response.ID, new ContainerExecCreateParameters
{
    AttachStdin = true,
    AttachStdout = true,
    AttachStderr = false,
    Detach = false,
    Privileged = true,
    Cmd = new string[] { "/usr/local/bin/ocrmypdf", "--redo-ocr", "-", "-" }
});

var stream = await client.Exec.StartAndAttachContainerExecAsync(execContainer.ID, true);

var inputFile = @"C:\temp\test.pdf";
var outputFile = @"C:\temp\test-2.pdf";

var inputBytes = File.ReadAllBytes(inputFile);
var outputStream = new MemoryStream();

// Write bytes to STDIN
var writeTask = Task.Run(async () =>
{
    await stream.WriteAsync(inputBytes, 0, inputBytes.Length, CancellationToken.None);
    stream.CloseWrite();
});

// Read bytes from STDOUT to MemoryStream
var readTask = Task.Run(async () =>
{
    //var buffer = new byte[81920];
    var buffer = ArrayPool<byte>.Shared.Rent(81920);

    try
    {
        // Copied from MultiplexedStream.CopyOutputToAsync method:
        for (; ; )
        {
            // Exception is thrown here after "await WaitContainerAsync" below:
            var result = await stream.ReadOutputAsync(buffer, 0, buffer.Length, default(CancellationToken)).ConfigureAwait(false);

            if (result.EOF)
                return;

            if (result.Count == 0)
                return;

            await outputStream.WriteAsync(buffer, 0, result.Count, default(CancellationToken)).ConfigureAwait(false);
        }
        // End CopyOutputToAsync
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer);
    }
});

await readTask;
await writeTask;

// Write MemoryStream to file
if (File.Exists(outputFile))
    File.Delete(outputFile);

outputStream.Position = 0;
using (var filestream = File.Create(outputFile))
{
    await outputStream.CopyToAsync(filestream);
}
xplicit commented 4 years ago

I have similar issue. When output stream is not populated for a long period of time, I've got an exception:

Unhandled exception. System.AggregateException: One or more errors occurred. (Unable to read data from the transport connection: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond..)
 ---> System.IO.IOException: Unable to read data from the transport connection: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond..
 ---> System.Net.Sockets.SocketException (10060): A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Security.SslStream.<FillBufferAsync>g__InternalFillBufferAsync|215_0[TReadAdapter](TReadAdapter adap, ValueTask`1 task, Int32 min, Int32 initial)
   at System.Net.Security.SslStream.ReadAsyncInternal[TReadAdapter](TReadAdapter adapter, Memory`1 buffer)
   at Docker.DotNet.MultiplexedStream.ReadOutputAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
   at Docker.DotNet.MultiplexedStream.CopyOutputToAsync(Stream stdin, Stream stdout, Stream stderr, CancellationToken cancellationToken)

Code slightly differs:

            var container = await client.Containers.CreateContainerAsync(new CreateContainerParameters()
            {
                AttachStdin = true,
                AttachStdout = true,
                AttachStderr = true,
                Image = image,
                Cmd = new List<string> {"/bin/bash","-c", command},
                OpenStdin = true,
                StdinOnce = true
            });

            var multiplexedStream = await client.Containers.AttachContainerAsync(container.ID, false,
                new ContainerAttachParameters() {Stderr = false, Stdin = true, Stdout = true, Stream = true});

            var buffer = Encoding.UTF8.GetBytes(input);
            await multiplexedStream.WriteAsync(buffer, 0, buffer.Length, default);
            multiplexedStream.CloseWrite();

            var outputStream = new MemoryStream();
            var stdErrStream = new MemoryStream();
            var copyTask = multiplexedStream.CopyOutputToAsync(null, outputStream, stdErrStream, default);

            var started = await client.Containers.StartContainerAsync(container.ID, new ContainerStartParameters(), default);

            await copyTask;
xplicit commented 4 years ago

Also I found that timeout occures if container does not respond for a roughly 10 seconds. For example if I use command="sleep 8 && echo test" everything works fine. If I use "sleep 9 && echo test" I always got timeout. What is interesting that timeout ocurres not after 10 seconds, but roughly after 2 minutes.

        var container = await client.Containers.CreateContainerAsync(new CreateContainerParameters()
        {
            AttachStdin = true,
            AttachStdout = true,
            AttachStderr = true,
            Image = image,
            Cmd = new List<string> {"/bin/bash","-c", command},
            OpenStdin = true,
            StdinOnce = true
        });
xplicit commented 4 years ago

What I found that stdout must be populated at least once every 10 seconds, otherwise you've got timeout exception. For example

command = ""sleep 3 && echo t1 && sleep 10 && echo t2 && sleep 10 && echo t3 && sleep 10 && echo t4 "

Throws exception on first sleep 10, while

"sleep 3 && echo t1 && sleep 3 && echo t2 && sleep 3 && echo t3 && sleep 3 && echo t4 " 

Works fine.

I did not get who is responsible for breaking connection every 10 seconds it could be docker api server as well as underlying NetworkStream/SslStream/BufferedStream/HttpClient

xplicit commented 4 years ago

I resolved my issue by rewriting from scratch ContainerAttachAsync/CopyOutputToAsync, Now it works correctly, no 10-seconds timeouts anymore.

aplocher commented 4 years ago

@xplicit Is that something you wouldn't mind sharing? I would love to revisit this.

My current "work-around" is to not use Docker.DotNet and instead use Process.Start with the "docker" cli

Hardly ideal! 😆

Thanks!

fbozkurtt commented 3 years ago

I resolved my issue by rewriting from scratch ContainerAttachAsync/CopyOutputToAsync, Now it works correctly, no 10-seconds timeouts anymore.

Care to share your code please? I'm having difficulties with this Exec thing.

matt-cassinelli commented 8 months ago

Did anyone find a solution for this other than using docker cli?