MENU

実例で使えるタイムアウト処理の基礎

目次

タイムアウト処理の実装

Javaにおけるタイムアウト処理は、システムの安定性と信頼性を確保する上で重要な役割を果たす。処理が予期せず長時間実行され続けることを防ぎ、システムリソースを適切に管理するための機能である。

Thread.sleepを使用したタイムアウト処理

Thread.sleepを用いたタイムアウト処理は、最も基本的な実装方法の一つである。以下に具体的な実装例を記す。

public void processWithTimeout(Runnable task, long timeoutMillis) throws Exception {
    Thread taskThread = new Thread(() -> {
        try {
            task.run();
        } catch (Exception e) {
            // 元の例外を保持するためのフィールドに格納
            exceptionHolder.set(e);
        }
    });

    taskThread.start();

    try {
        // 指定時間だけ待機
        taskThread.join(timeoutMillis);

        // タイムアウト判定
        if (taskThread.isAlive()) {
            taskThread.interrupt();
            throw new TimeoutException("処理がタイムアウトしました");
        }

        // タスク実行中に発生した例外を確認
        if (exceptionHolder.get() != null) {
            throw exceptionHolder.get();
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw e;
    }
}

この実装では、別スレッドで実行される処理に対して時間制限を設けている。join()メソッドにより、指定時間経過後も処理が完了していない場合はタイムアウトとして扱う。また、タスク実行中に発生した例外は適切に保持され、呼び出し元に伝播する。

java.util.Timerを使用したタイムアウト処理

Timerクラスを使用することで、より柔軟なタイムアウト制御が可能となる。

public class TimerBasedTimeout {
    private final Timer timer = new Timer(true); // デーモンスレッドとして実行

    public void executeWithTimeout(Runnable task, long timeoutMillis) throws TimeoutException {
        TimerTask timeoutTask = new TimerTask() {
            @Override
            public void run() {
                // タイムアウト時の処理
                Thread.currentThread().interrupt();
            }
        };

        timer.schedule(timeoutTask, timeoutMillis);
        try {
            task.run();
            // 正常終了時はタイムアウトタスクをキャンセル
            timeoutTask.cancel();
        } catch (Exception e) {
            throw new TimeoutException("処理がタイムアウトしました");
        } finally {
            timeoutTask.cancel();
        }
    }
}

Timerを利用する利点は、タイムアウト時の処理をより細かく制御できる点にある。また、複数のタイムアウト処理を一元管理することも可能である。

ScheduledExecutorServiceによるタイムアウト処理

ScheduledExecutorServiceは、Timerよりも高度なスケジューリング機能を提供する。

public class ScheduledTimeoutExecutor {
    private final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(1);

    public <T> T executeWithTimeout(Callable<T> task, long timeout, TimeUnit unit) 
        throws Exception {
        Future<T> future = scheduler.submit(task);
        try {
            return future.get(timeout, unit);
        } catch (TimeoutException e) {
            future.cancel(true);
            throw e;
        } finally {
            // タスクのクリーンアップ
            if (!future.isDone()) {
                future.cancel(true);
            }
        }
    }
}

ScheduledExecutorServiceは、ThreadPoolExecutorの機能を拡張したものであり、定期的なタスク実行やタイムアウト処理を効率的に管理できる。また、スレッドプールを使用することで、システムリソースの効率的な利用が可能となる。

この実装方法の特筆すべき点は、タスクのキャンセル処理が確実に行われること、また例外処理が適切に組み込まれていることである。これにより、リソースリークを防ぎつつ、安全なタイムアウト処理を実現している。

ネットワーク処理でのタイムアウト実装

基本的なタイムアウト処理の実装を踏まえ、実践的なネットワーク処理におけるタイムアウト実装について解説する。ネットワーク処理では、外部要因による遅延や接続不能状態に対する適切な制御が不可欠である。

HTTPコネクションのタイムアウト設定

HTTPコネクションにおけるタイムアウト設定は、接続時間、読み取り時間、および応答待ち時間の3つの要素で構成される。以下に、HttpURLConnectionを使用した実装例を記す。

public String fetchWithTimeout(String urlString) throws IOException {
    URL url = new URL(urlString);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();

    try {
        // 接続タイムアウトを5秒に設定
        connection.setConnectTimeout(5000);
        // 読み取りタイムアウトを10秒に設定
        connection.setReadTimeout(10000);

        // レスポンスの文字セットを取得
        String charset = Optional.ofNullable(connection.getContentType())
                .flatMap(contentType -> {
                    String[] values = contentType.split(";");
                    for (String value : values) {
                        if (value.trim().toLowerCase().startsWith("charset=")) {
                            return Optional.of(value.substring(8).trim());
                        }
                    }
                    return Optional.empty();
                })
                .orElse("UTF-8"); // デフォルトはUTF-8

        // 入力ストリームの取得
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(connection.getInputStream(), charset))) {
            StringBuilder response = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            return response.toString();
        }
    } finally {
        // 接続のクリーンアップ
        connection.disconnect();
    }
}

setConnectTimeoutメソッドはサーバーへの接続確立までの待機時間を制御し、setReadTimeoutメソッドはデータの読み取り操作のタイムアウトを制御する。これにより、ネットワークの不具合や過負荷状態から適切に保護することが可能となる。

ソケット通信でのタイムアウト制御

低レベルのソケット通信においても、適切なタイムアウト制御が必要である。

public class SocketTimeoutExample {
    public void connectWithTimeout(String host, int port) throws IOException {
        Socket socket = new Socket();

        try {
            // 接続タイムアウトを設定
            socket.connect(new InetSocketAddress(host, port), 5000);

            // データ送受信のタイムアウトを設定
            socket.setSoTimeout(3000);

            // 入出力ストリームの取得
            InputStream in = socket.getInputStream();
            OutputStream out = socket.getOutputStream();

            // データの送受信処理
            byte[] buffer = new byte[1024];
            int bytesRead = in.read(buffer); // 最大3秒待機
        } finally {
            socket.close();
        }
    }
}

ソケット通信では、connect()メソッドの第二引数でタイムアウトを指定し、setSoTimeout()で読み取り操作のタイムアウトを設定する。これにより、ネットワークの遅延や障害に対して堅牢な通信処理を実現できる。

データベース接続のタイムアウト管理

データベース接続におけるタイムアウト設定は、接続プールの管理とも密接に関連する。

public class DatabaseTimeoutManager {
    public Connection getConnectionWithTimeout() throws SQLException {
        Properties props = new Properties();

        // 接続タイムアウトを30秒に設定
        props.setProperty("connectTimeout", "30");
        // クエリタイムアウトを60秒に設定
        props.setProperty("socketTimeout", "60");

        return DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/mydb",
            props
        );
    }

    public void executeQueryWithTimeout(Connection conn, String sql) 
            throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            // クエリのタイムアウトを20秒に設定
            stmt.setQueryTimeout(20);
            ResultSet rs = stmt.executeQuery(sql);
            // 結果の処理
        }
    }
}

データベース接続では、接続確立時のタイムアウトに加え、クエリ実行時のタイムアウトも重要である。setQueryTimeout()メソッドにより、長時間実行されるクエリによるシステムの停滞を防ぐことができる。また、コネクションプールを使用する場合は、プールからの接続取得時のタイムアウトも考慮する必要がある。

タイムアウト処理のテスト方法

実装したタイムアウト処理の信頼性を確保するためには、適切なテスト方法の選択と実施が不可欠である。以下に、効果的なテスト手法について解説する。

JUnitを使用したタイムアウトのテスト

JUnitフレームワークは、タイムアウト処理のテストに必要な機能を提供している。以下に具体的な実装例を記す。

public class TimeoutTest {
    @Test(timeout = 1000)
    public void testProcessTimeout() {
        // タイムアウトが発生すべき処理
        TimeoutProcessor processor = new TimeoutProcessor();

        assertThrows(TimeoutException.class, () -> {
            processor.executeWithTimeout(() -> {
                // 意図的に遅延を発生させる処理
                Thread.sleep(2000);
            }, 1000);
        });
    }

    @Test
    public void testProcessCompletion() throws Exception {
        TimeoutProcessor processor = new TimeoutProcessor();

        // 正常に完了すべき処理
        AtomicBoolean executed = new AtomicBoolean(false);
        processor.executeWithTimeout(() -> {
            Thread.sleep(500);
            executed.set(true);
        }, 1000);

        assertTrue(executed.get(), "処理が正常に完了していない");
    }
}

JUnitのtimeoutアノテーションを使用することで、テストメソッド自体にタイムアウトを設定できる。これにて、テストの実行時間を制御し、無限ループなどの異常を検出することが可能となる。

モックを活用したタイムアウトテスト

外部依存を持つタイムアウト処理のテストには、モックオブジェクトの活用が有効である。

public class NetworkTimeoutTest {
    @Mock
    private HttpClient httpClient;

    @Test
    public void testHttpTimeout() throws Exception {
        // モックの設定
        HttpResponse<String> mockResponse = mock(HttpResponse.class);
        when(mockResponse.body()).thenReturn("Response Data");
        
        when(httpClient.send(any(), any(HttpResponse.BodyHandler.class)))
            .thenAnswer(invocation -> {
                Thread.sleep(2000); // 意図的な遅延
                return mockResponse;
            });

        NetworkProcessor processor = new NetworkProcessor(httpClient);

        // タイムアウトの検証
        assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
            assertThrows(TimeoutException.class, () -> {
                processor.fetchData("http://example.com");
            });
        });
    }
}

モックを使用することで、ネットワーク遅延やサーバー応答の遅延を再現可能なテストケースとして実装できる。実際のHTTPレスポンスをモック化することで、より現実的なテストシナリオを構築し、タイムアウト処理の正確な検証が可能となる。

実践的なテストシナリオの作成

実運用を想定した総合的なテストシナリオを作成することで、タイムアウト処理の信頼性を向上させることができる。

public class IntegrationTimeoutTest {
    private static final int TIMEOUT_THRESHOLD = 5000;

    @Test
    public void testConcurrentTimeouts() throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        CountDownLatch latch = new CountDownLatch(3);

        // 複数の同時実行処理のテスト
        Future<?> task1 = executor.submit(() -> {
            try {
                testDatabaseTimeout();
            } finally {
                latch.countDown();
            }
        });

        Future<?> task2 = executor.submit(() -> {
            try {
                testNetworkTimeout();
            } finally {
                latch.countDown();
            }
        });

        Future<?> task3 = executor.submit(() -> {
            try {
                testFileOperationTimeout();
            } finally {
                latch.countDown();
            }
        });

        // すべての処理の完了を待機
        assertTrue(latch.await(TIMEOUT_THRESHOLD, TimeUnit.MILLISECONDS),
            "タイムアウト処理が正常に機能していない");
    }

    private void testDatabaseTimeout() {
        // データベース操作のタイムアウトテスト実装
    }

    private void testNetworkTimeout() {
        // ネットワーク操作のタイムアウトテスト実装
    }

    private void testFileOperationTimeout() {
        // ファイル操作のタイムアウトテスト実装
    }
}

この実装例では、複数の処理を同時に実行し、それぞれのタイムアウト処理が適切に機能することを検証している。CountDownLatchを使用することで、すべての処理の完了を確実に待機し、テストの信頼性を確保している。

タイムアウト処理のテスト方法

実装したタイムアウト処理の信頼性を確保するためには、適切なテスト方法の選択と実施が不可欠である。以下に、効果的なテスト手法について解説する。

JUnitを使用したタイムアウトのテスト

JUnitフレームワークは、タイムアウト処理のテストに必要な機能を提供している。以下に具体的な実装例を記す。

public class TimeoutTest {
    @Test(timeout = 1000)
    public void testProcessTimeout() {
        // タイムアウトが発生すべき処理
        TimeoutProcessor processor = new TimeoutProcessor();

        assertThrows(TimeoutException.class, () -> {
            processor.executeWithTimeout(() -> {
                // 意図的に遅延を発生させる処理
                Thread.sleep(2000);
            }, 1000);
        });
    }

    @Test
    public void testProcessCompletion() throws Exception {
        TimeoutProcessor processor = new TimeoutProcessor();

        // 正常に完了すべき処理
        AtomicBoolean executed = new AtomicBoolean(false);
        processor.executeWithTimeout(() -> {
            Thread.sleep(500);
            executed.set(true);
        }, 1000);

        assertTrue(executed.get(), "処理が正常に完了していない");
    }
}

JUnitのtimeoutアノテーションを使用することで、テストメソッド自体にタイムアウトを設定できる。これにて、テストの実行時間を制御し、無限ループなどの異常を検出することが可能となる。

モックを活用したタイムアウトテスト

外部依存を持つタイムアウト処理のテストには、モックオブジェクトの活用が有効である。

public class NetworkTimeoutTest {
    @Mock
    private HttpClient httpClient;

    @Test
    public void testHttpTimeout() throws Exception {
        // モックの設定
        when(httpClient.send(any(), any(HttpResponse.BodyHandler.class)))
            .thenAnswer(invocation -> {
                Thread.sleep(2000); // 意図的な遅延
                return null;
            });

        NetworkProcessor processor = new NetworkProcessor(httpClient);

        // タイムアウトの検証
        assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
            assertThrows(TimeoutException.class, () -> {
                processor.fetchData("http://example.com");
            });
        });
    }
}

モックを使用することで、ネットワーク遅延やサーバー応答の遅延を再現可能なテストケースとして実装できる。これにより、実際の外部サービスに依存することなく、タイムアウト処理の動作を検証することが可能となる。

実践的なテストシナリオの作成

実運用を想定した総合的なテストシナリオを作成することで、タイムアウト処理の信頼性を向上させることができる。

public class IntegrationTimeoutTest {
    private static final int TIMEOUT_THRESHOLD = 5000;

    @Test
    public void testConcurrentTimeouts() throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        CountDownLatch latch = new CountDownLatch(3);

        // 複数の同時実行処理のテスト
        Future<?> task1 = executor.submit(() -> {
            try {
                testDatabaseTimeout();
            } finally {
                latch.countDown();
            }
        });

        Future<?> task2 = executor.submit(() -> {
            try {
                testNetworkTimeout();
            } finally {
                latch.countDown();
            }
        });

        Future<?> task3 = executor.submit(() -> {
            try {
                testFileOperationTimeout();
            } finally {
                latch.countDown();
            }
        });

        // すべての処理の完了を待機
        assertTrue(latch.await(TIMEOUT_THRESHOLD, TimeUnit.MILLISECONDS),
            "タイムアウト処理が正常に機能していない");
    }

    private void testDatabaseTimeout() {
        // データベース操作のタイムアウトテスト実装
    }

    private void testNetworkTimeout() {
        // ネットワーク操作のタイムアウトテスト実装
    }

    private void testFileOperationTimeout() {
        // ファイル操作のタイムアウトテスト実装
    }
}

この実装例では、複数の処理を同時に実行し、それぞれのタイムアウト処理が適切に機能することを検証している。CountDownLatchを使用することで、すべての処理の完了を確実に待機し、テストの信頼性を確保している。

以上。

よかったらシェアしてね!
  • URLをコピーしました!
目次