FTP 8.5: OutOfMemory subiendo ficheros con un cliente .NET

 

 

Recuerdo un caso en el que un cliente tenía un OutOfMemory subiendo ficheros de más de 1GB a un FTP usando las clases de .NET. Recordemos que el límite de memoria virtual que tiene un proceso de 64 bit es de 8 TB o 128 TB en Windows 8.1/10/2012 R2 (Memory Limits for Windows and Windows Server Releases)

Técnicamente es posible tener ese OutOfMemory puesto que podríamos crear una aplicación que consuma memoria hasta llegar a ese límite de memoria. Pero, ¿cómo es posible tener ese OutOfMemory si subimos un fichero de 1.4 GB y nos quedan más de 120 TB libres en el proceso?

Usando este código (de la MSDN) , veremos cómo es posible conseguir la excepción subiendo el fichero:

 

            string user = "";

            string pass = "";

            string file = "";

            string ftpUri = "";

            try

            { 

                if (args.Length == 0)

                {

                    Console.WriteLine("Enter username: ");

                    user = Console.ReadLine();

                    Console.WriteLine("Enter password: ");

                    pass = Console.ReadLine();

                    Console.WriteLine("Enter file name: ");

                    file = Console.ReadLine();

                    Console.WriteLine("Enter FTP URi: ");

                    ftpUri = Console.ReadLine();

                }

                else

                {

                    user = args[0];

                    pass = args[1];

    file = args[2];

                    ftpUri = args[3];

                }

                 FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUri);

                request.Method = WebRequestMethods.Ftp.UploadFile;              

                request.Credentials = newNetworkCredential(user, pass);                               

                StreamReader sourceStream = newStreamReader(file);

                Console.ReadLine(); 

                byte[] fileContents = Encoding.UTF8.GetBytes(sourceStream.ReadToEnd());

                sourceStream.Close();

                request.ContentLength = fileContents.Length; 

                Stream requestStream = request.GetRequestStream();

                requestStream.Write(fileContents, 0, fileContents.Length);

                requestStream.Close(); 

                FtpWebResponse response = (FtpWebResponse)request.GetResponse(); 

                Console.WriteLine("Upload File Complete, status {0}", response.StatusDescription);

                response.Close();

            }

            catch(Exception ex)

            {

                Console.WriteLine("An exception has occurred: {0}", ex.Message);

            }

Como decía, ejecutando este código, tendremos una OutOfMemoryException, concretamente cuando intentemos convertir el Stream del fichero que queremos subir a un array de bytes:

 

                byte[] fileContents = Encoding.UTF8.GetBytes(sourceStream.ReadToEnd());

 

Si hemos adjuntado un depurador, veremos algo como esto:

 

# Call Site
00 KERNELBASE!RaiseException
01 clr!DllCanUnloadNowInternal
02 clr!TranslateSecurityAttributes
03 clr!NGenCreateNGenWorker
04 mscorlib_ni!System.Text.StringBuilder.ToString()
05 FTPClient!FTPCLient.Main(System.String[])

 

Lo que ocurre es que estamos convirtiendo un fichero muy grande en el array de bytes directamente. Lo recomendado sería usar un buffer para evitar este comportamiento: Subir ficheros con buffer

También podemos usar los objetos muy grandes de .NET (como menciono en este otro artículo: OutOfMemoryException al manejar StringBuilder en un proceso de 64 bitshttps://blogs.msdn.com/b/desarrolloweb/archive/2016/02/17/stringbuilder-outofmemory.aspx

Espero que os sirva.

-- José Ortega Gutiérrez