CodeGym /Cursos /JAVA 25 SELF /Manejo de errores en E/S asíncrona, cancelación de operac...

Manejo de errores en E/S asíncrona, cancelación de operaciones

JAVA 25 SELF
Nivel 56, Lección 3
Disponible

1. Manejo de errores en operaciones asíncronas

En el código síncrono todo es sencillo: si no se encuentra el archivo o no hay permisos, capturas la excepción al instante en try-catch. En el código asíncrono, especialmente cuando usas callbacks (CompletionHandler), el error puede ocurrir después de que tu método haya terminado — en algún lugar de las entrañas del pool de hilos. Si no se maneja correctamente, el programa puede comportarse de forma impredecible: desde «pérdidas silenciosas» de datos hasta la caída de toda la aplicación.

¿Cómo se propagan los errores a CompletionHandler?

La interfaz CompletionHandler<V, A> tiene dos métodos:

  • completed(V result, A attachment) — se invoca si la operación terminó correctamente.
  • failed(Throwable exc, A attachment) — se invoca si ocurrió un error.

He aquí un ejemplo de uso:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Leídos correctamente " + result + " bytes");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Error al leer el archivo: " + exc.getMessage());
                    // Se puede registrar en logs, notificar al usuario, propagar el error
                }
            });
        } catch (IOException ex) {
            System.out.println("Error al abrir el archivo: " + ex.getMessage());
        }

        // Damos tiempo a que la operación asíncrona termine (en aplicaciones reales usa CountDownLatch u otros mecanismos)
        Thread.sleep(500);
    }
}

¿Qué está pasando aquí?

  • Si el archivo no existe, se llamará al método failed con la excepción correspondiente (NoSuchFileException).
  • Si la operación terminó con éxito, se ejecutará completed.

Ejemplos de errores típicos

  • Archivo no encontrado: NoSuchFileException
  • Acceso denegado: AccessDeniedException
  • Error de lectura/escritura: diferentes subclases de IOException
  • Problemas con el búfer: BufferOverflowException, BufferUnderflowException

Registro (logging) e información al usuario

Un error en un callback asíncrono no es motivo de pánico, pero tampoco de hacer como si nada hubiera pasado. Una buena práctica es registrar el error (por ejemplo, mediante Logger) y, si es relevante para el usuario, mostrar un mensaje o invocar un manejador en la UI.

Ejemplo de logging:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Error en la operación asíncrona: " + exc);
    exc.printStackTrace();
}

En código de producción utiliza registros adecuados (por ejemplo, java.util.logging o Log4j), no System.err.

2. Cancelación de operaciones asíncronas

¿Cuándo puede ser necesaria la cancelación de una operación?

A veces es necesario detener una tarea asíncrona en mitad de su ejecución. Por ejemplo, el usuario se arrepiente y pulsa «Cancelar» durante la descarga de un archivo. O se cierra la ventana de la aplicación y la operación ya no tiene sentido. También puede que al finalizar la aplicación simplemente haya que liberar los recursos de forma ordenada.

Para estos casos, la E/S asíncrona en Java admite la cancelación a través de la interfaz Future. Con ella puedes interrumpir una tarea en ejecución en cualquier momento y no malgastar recursos.

¿Cómo cancelar una operación con Future?

El método read o write de AsynchronousFileChannel devuelve un objeto Future<Integer>. Este objeto dispone del método cancel(boolean mayInterruptIfRunning).

Ejemplo: cancelación de una lectura asíncrona

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Esperamos un poco y luego cancelamos la operación
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("¡La operación de lectura se ha cancelado!");
            } else {
                System.out.println("No se pudo cancelar la operación (quizá ya haya finalizado)");
            }
        }
    }
}

Aspectos importantes:

  • La cancelación solo funciona para operaciones que aún no han terminado.
  • Si la operación ya ha finalizado, no se podrá cancelar.
  • Tras cancelar, si intentas invocar get() sobre ese Future, se lanzará la excepción CancellationException.

¿Cuándo ya no es posible cancelar la operación?

Si la tarea ya ha terminado — sea con éxito o con error — ya no es posible detenerla.

Además, no todas las implementaciones son capaces de interrumpir operaciones a nivel del sistema operativo. Por ejemplo, con algunos sistemas de archivos la «cancelación» será meramente simbólica: la operación seguirá ejecutándose, pero simplemente ignorarás su resultado.

3. Práctica: manejo de errores y cancelación

Ejemplo 1: manejo del error al leer un archivo inexistente

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("La operación finalizó correctamente");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Error al leer el archivo: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("Error al abrir el archivo: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

¿Qué veremos en la consola?

Error al abrir el archivo: no_such_file.txt

o, si el error se produce al leer y no al abrir:

Error al leer el archivo: NoSuchFileException - no_such_file.txt

Ejemplo 2: cancelación de una operación larga y finalización correcta

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Cancelamos la operación después de 50 ms (para experimentar)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("¡La operación de lectura ha sido cancelada!");
            } else {
                System.out.println("No se pudo cancelar la operación (probablemente ya haya finalizado)");
            }

            try {
                // Intentamos obtener el resultado (lanzará CancellationException)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("Se capturó CancellationException: la operación realmente está cancelada.");
            }
        }
    }
}

4. Buenas prácticas: cómo hacerlo bien

Libera los recursos incluso ante errores

Usa try-with-resources para cerrar los canales automáticamente:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

Si utilizas CompletionHandler, no olvides cerrar el canal cuando terminen todas las operaciones. Esto es especialmente importante si realizas varias operaciones asíncronas consecutivas.

No bloquees la UI/el hilo principal

Las operaciones asíncronas están para no bloquear el hilo principal. No llames a future.get() en el hilo de la UI; de lo contrario, se pierde el propósito de la asincronía.

Registra todos los errores

En CompletionHandler implementa siempre el método failed y registra (o propaga) todas las excepciones.

Comprueba la finalización de todas las operaciones antes de cerrar la aplicación

Si la aplicación termina antes de que lo haga la operación, el resultado puede perderse. En demostraciones por consola a veces hay que hacer Thread.sleep(500), pero en aplicaciones reales utiliza CountDownLatch, CompletableFuture u otros mecanismos de sincronización.

No te olvides de la cancelación

Si una operación ya no es necesaria (por ejemplo, el usuario cerró la ventana), cancélala mediante Future.cancel. Ahorrarás recursos y mejorarás la respuesta de la aplicación.

5. Errores habituales al manejar errores y cancelaciones en la E/S asíncrona

Error n.º 1: ignorar el método failed en CompletionHandler.
Si no implementas el manejo de errores, tu aplicación puede comportarse de forma impredecible: los errores «se perderán» y el usuario no sabrá por qué no ocurre nada.

Error n.º 2: no cerrar el canal tras finalizar las operaciones.
Si olvidas cerrar AsynchronousFileChannel, obtendrás fugas de recursos y, posiblemente, bloqueo del archivo en el SO.

Error n.º 3: esperar el resultado de una operación asíncrona en el hilo principal.
Si llamas a future.get() en el hilo de la UI, la interfaz se «congela» y toda la asincronía pierde su sentido.

Error n.º 4: intentar cancelar una operación ya finalizada.
Si llamas a cancel() demasiado tarde — cuando la operación ya terminó — la cancelación no surtirá efecto. No es crítico, pero puede confundir durante la depuración.

Error n.º 5: no comprobar el resultado de la cancelación.
Llamas a cancel(), pero no compruebas el valor devuelto ni manejas CancellationException al invocar get(): la aplicación puede fallar o comportarse de forma extraña.

Error n.º 6: no liberar recursos en caso de error o cancelación.
Si no cierras el canal tras un error o una cancelación, pueden producirse fugas o bloqueos de archivos.

1
Tarea
JAVA 25 SELF, nivel 56, lección 3
Bloqueada
Cambio de canal: cancelación de la descarga del vídeo 🎬
Cambio de canal: cancelación de la descarga del vídeo 🎬
1
Tarea
JAVA 25 SELF, nivel 56, lección 3
Bloqueada
Supervisión de registros del sistema: comprobación paralela y cancelación de emergencia 🚨
Supervisión de registros del sistema: comprobación paralela y cancelación de emergencia 🚨
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION