diff --git a/.all-contributorsrc b/.all-contributorsrc index df0e170f068f..ccc740df15e1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3773,6 +3773,33 @@ "contributions": [ "code" ] + }, + { + "login": "SandhyaDevadiga", + "name": "Sandhya", + "avatar_url": "https://avatars.githubusercontent.com/u/128058717?v=4", + "profile": "https://github.com/SandhyaDevadiga", + "contributions": [ + "code" + ] + }, + { + "login": "devikae", + "name": "dev-ikae", + "avatar_url": "https://avatars.githubusercontent.com/u/98100047?v=4", + "profile": "https://github.com/devikae", + "contributions": [ + "code" + ] + }, + { + "login": "zanarellidev", + "name": "zanarelli", + "avatar_url": "https://avatars.githubusercontent.com/u/268068569?v=4", + "profile": "https://github.com/zanarellidev", + "contributions": [ + "translation" + ] } ], "contributorsPerLine": 6, diff --git a/README.md b/README.md index f0e461c0fb76..eb821177e73e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns) [![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![All Contributors](https://img.shields.io/badge/all_contributors-414-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-417-orange.svg?style=flat-square)](#contributors-)
@@ -613,6 +613,11 @@ This project is licensed under the terms of the MIT license. Anvesh Srivastava
Anvesh Srivastava

💻 Mukul Howale
Mukul Howale

💻 + + Sandhya
Sandhya

💻 + dev-ikae
dev-ikae

💻 + zanarelli
zanarelli

🌍 + diff --git a/fork-join/README.md b/fork-join/README.md new file mode 100644 index 000000000000..b94af1de02ac --- /dev/null +++ b/fork-join/README.md @@ -0,0 +1,161 @@ +--- +title: "Fork/Join Pattern in Java: Parallel Divide-and-Conquer Processing" +shortTitle: Fork/Join +description: "Learn the Fork/Join design pattern in Java with real-world examples, class diagrams, and code samples. Understand how to split large tasks into parallel subtasks for improved performance." +category: Concurrency +language: en +tag: + - Performance + - Scalability + - Concurrency +--- + +## Also known as + +* Divide and Conquer Parallelism +* Work-Stealing Parallelism + +## Intent of Fork/Join Design Pattern + +The Fork/Join pattern recursively splits a large task into independent subtasks (fork), +processes them in parallel across multiple threads, and combines their results (join) to +produce a final outcome. It maximizes CPU utilization for computationally intensive problems. + +## Detailed Explanation of Fork/Join Pattern with Real-World Examples + +Real-world example + +> Imagine a large warehouse that needs to count all its inventory items across 100 aisles. +> Instead of one person counting every aisle sequentially, the manager divides the warehouse +> into sections and assigns a team of workers to count each section simultaneously. Once +> every section is counted, the manager collects all partial counts and sums them into the +> total inventory. This is the Fork/Join pattern: split the work, do it in parallel, merge +> the results. + +In plain words + +> Fork/Join splits a big problem into smaller pieces, solves each piece in parallel on +> separate threads, then combines all results back together. + +## Programmatic Example of Fork/Join Pattern in Java + +We demonstrate the pattern by computing the sum of a large array in parallel using Java's +built-in `ForkJoinPool` and `RecursiveTask`. + +The `SumTask` is a recursive task that splits the array when it's too large: + +```java +public class SumTask extends RecursiveTask { + + private static final int THRESHOLD = 1000; + private final long[] numbers; + private final int start; + private final int end; + + @Override + protected Long compute() { + int length = end - start; + + if (length <= THRESHOLD) { + // Base case: sum directly + long sum = 0; + for (int i = start; i < end; i++) { + sum += numbers[i]; + } + return sum; + } + + // Fork: split into two halves + int mid = start + length / 2; + SumTask leftTask = new SumTask(numbers, start, mid); + SumTask rightTask = new SumTask(numbers, mid, end); + + leftTask.fork(); // run left half asynchronously + long rightResult = rightTask.compute(); // compute right half here + long leftResult = leftTask.join(); // wait for left half + + // Join: combine results + return leftResult + rightResult; + } +} +``` + +The `ForkJoinSumCalculator` provides a clean API: + +```java +public class ForkJoinSumCalculator { + + private final ForkJoinPool pool; + + public ForkJoinSumCalculator() { + this.pool = ForkJoinPool.commonPool(); + } + + public long calculateSum(long[] numbers) { + SumTask task = new SumTask(numbers, 0, numbers.length); + return pool.invoke(task); + } +} +``` + +Running the example in `App`: + +```java +long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray(); +ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); +long result = calculator.calculateSum(numbers); +System.out.println("Fork/Join sum: " + result); +``` + +Program output: + +``` +Fork/Join sum: 50000005000000 +Expected sum: 50000005000000 +Correct: true +Time taken: 45 ms +Available processors: 8 +``` + +## When to Use the Fork/Join Pattern in Java + +* When you have a large, CPU-intensive task that can be divided into independent subtasks. +* When the subtasks are roughly the same size and don't depend on each other. +* When you want to utilize multiple CPU cores without manually managing threads. +* When the problem naturally fits a divide-and-conquer strategy (e.g., sorting, searching, + numerical computation). + +## When NOT to Use Fork/Join + +* For I/O-bound tasks (network calls, file reads) — use virtual threads or async I/O instead. +* When subtasks are too small — the overhead of forking exceeds the benefit. +* When tasks have dependencies on each other and cannot run independently. + +## Benefits and Trade-offs of Fork/Join Pattern + +Benefits: + +* Maximizes CPU utilization through work-stealing algorithm. +* Scales automatically with the number of available processors. +* Built into Java's standard library (`java.util.concurrent`) — no external dependencies. +* Clean recursive decomposition makes the code readable and maintainable. + +Trade-offs: + +* Overhead from task creation and thread management for very small problems. +* Requires tasks to be independent — shared mutable state introduces bugs. +* Choosing an appropriate threshold requires tuning for optimal performance. +* Debugging parallel code is inherently harder than sequential code. + +## Related Java Design Patterns + +* [Divide and Conquer](https://java-design-patterns.com/patterns/divide-and-conquer/): + Fork/Join is the parallel execution variant of the classic divide-and-conquer strategy. +* [Thread Pool](https://java-design-patterns.com/patterns/thread-pool/): Fork/Join uses a + specialized pool with work-stealing semantics. + +## References + +* [Java Documentation for ForkJoinPool](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ForkJoinPool.html) +* [Java Concurrency in Practice — Brian Goetz](https://amzn.to/4aRMruW) +* [Java Documentation for RecursiveTask](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/RecursiveTask.html) diff --git a/fork-join/pom.xml b/fork-join/pom.xml new file mode 100644 index 000000000000..23958727728a --- /dev/null +++ b/fork-join/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + fork-join + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.forkjoin.App + + + + + + + + + + diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/App.java b/fork-join/src/main/java/com/iluwatar/forkjoin/App.java new file mode 100644 index 000000000000..a53bdcec6de7 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/App.java @@ -0,0 +1,43 @@ +package com.iluwatar.forkjoin; + +import java.util.stream.LongStream; + +/** + * The Fork/Join pattern is a concurrency design pattern that splits a large task into smaller + * subtasks (fork), processes them in parallel, and then combines the results (join). + * + *

In Java, this pattern is implemented using {@link java.util.concurrent.ForkJoinPool} and + * {@link java.util.concurrent.RecursiveTask}. Worker threads in the pool use a work-stealing + * algorithm — idle threads take tasks from busy threads — maximizing CPU utilization. + * + *

In this example, we demonstrate the pattern by computing the sum of a large array in parallel. + * The array is recursively split in half until each piece is small enough to sum directly, then + * results are combined back up. + */ +public final class App { + + private App() {} + + /** + * @param args command line arguments, not used + */ + public static void main(String[] args) { + // Create an array of 10 million numbers: [1, 2, 3, ..., 10_000_000] + long[] numbers = LongStream.rangeClosed(1, 10_000_000).toArray(); + + // Calculate sum using Fork/Join + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + long startTime = System.currentTimeMillis(); + long result = calculator.calculateSum(numbers); + long endTime = System.currentTimeMillis(); + + // The expected sum of 1 to N is N*(N+1)/2 + long expected = 10_000_000L * 10_000_001L / 2; + + System.out.println("Fork/Join sum: " + result); + System.out.println("Expected sum: " + expected); + System.out.println("Correct: " + (result == expected)); + System.out.println("Time taken: " + (endTime - startTime) + " ms"); + System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors()); + } +} diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java b/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java new file mode 100644 index 000000000000..0e9dff32cda8 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/ForkJoinSumCalculator.java @@ -0,0 +1,44 @@ +package com.iluwatar.forkjoin; + +import java.util.concurrent.ForkJoinPool; + +/** + * ForkJoinSumCalculator provides a convenient API to sum an array of numbers using the Fork/Join + * framework. It creates a {@link ForkJoinPool}, submits a {@link SumTask}, and returns the computed + * sum. + * + *

The pool manages a set of worker threads that process subtasks in parallel. Idle threads can + * "steal" work from busy threads, maximizing CPU utilization. + */ +public class ForkJoinSumCalculator { + + private final ForkJoinPool pool; + + /** Creates a calculator using the common ForkJoinPool (uses all available CPU cores). */ + public ForkJoinSumCalculator() { + this.pool = ForkJoinPool.commonPool(); + } + + /** + * Creates a calculator with a specific number of threads. + * + * @param parallelism the number of worker threads to use + */ + public ForkJoinSumCalculator(int parallelism) { + this.pool = new ForkJoinPool(parallelism); + } + + /** + * Calculates the sum of all elements in the array using Fork/Join parallelism. + * + * @param numbers the array of numbers to sum + * @return the total sum of all elements + */ + public long calculateSum(long[] numbers) { + if (numbers == null || numbers.length == 0) { + return 0; + } + SumTask task = new SumTask(numbers, 0, numbers.length); + return pool.invoke(task); + } +} diff --git a/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java b/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java new file mode 100644 index 000000000000..acbd5f0516b7 --- /dev/null +++ b/fork-join/src/main/java/com/iluwatar/forkjoin/SumTask.java @@ -0,0 +1,93 @@ +package com.iluwatar.forkjoin; + +import java.util.concurrent.RecursiveTask; + +/** + * SumTask demonstrates the Fork/Join pattern by recursively splitting an array summation problem + * into smaller subtasks until each subtask is small enough to compute directly. + * + *

How it works: If the portion of the array is smaller than THRESHOLD, sum it in a simple loop. + * Otherwise, split the array in half, fork one half to run in parallel, compute the other half in + * the current thread, and then join the results. This approach utilizes multiple CPU cores to + * perform the summation significantly faster than a single-threaded loop for large arrays. + */ +public class SumTask extends RecursiveTask { + + /** + * If the number of elements to process is at or below this threshold, the task computes the sum + * directly instead of splitting further. + */ + private static final int THRESHOLD = 1000; + + private final long[] numbers; + private final int start; + private final int end; + + /** + * Creates a task to sum elements of the given array from index {@code start} (inclusive) to index + * {@code end} (exclusive). + * + * @param numbers the array of numbers to sum + * @param start the starting index (inclusive) + * @param end the ending index (exclusive) + */ + public SumTask(long[] numbers, int start, int end) { + if (start > end) { + throw new IllegalArgumentException( + "start (" + start + ") must not be greater than end (" + end + ")"); + } + this.numbers = numbers; + this.start = start; + this.end = end; + } + + /** + * The main computation method. This is where the fork/join magic happens. + * + * @return the sum of elements from start to end + */ + @Override + protected Long compute() { + int length = end - start; + + // BASE CASE: if the chunk is small enough, just sum directly + if (length <= THRESHOLD) { + return computeDirectly(); + } + + // FORK: split the task into two halves + int mid = start + length / 2; + + // Create subtask for the left half + SumTask leftTask = new SumTask(numbers, start, mid); + + // Create subtask for the right half + SumTask rightTask = new SumTask(numbers, mid, end); + + // Fork the left task — it will run in a separate thread + leftTask.fork(); + + // Compute the right task in the current thread (no need to fork both) + long rightResult = rightTask.compute(); + + // JOIN: wait for the left task to finish and get its result + long leftResult = leftTask.join(); + + // Combine the results from both halves + return leftResult + rightResult; + } + + /** + * Computes the sum directly using a simple loop. This is used when the chunk size is at or below + * the threshold — no further splitting needed. + * + * @return the sum of elements in the range [start, end) + */ + private long computeDirectly() { + long sum = 0; + for (int i = start; i < end; i++) { + sum += numbers[i]; + } + return sum; + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java new file mode 100644 index 000000000000..c66f8610cc55 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/AppTest.java @@ -0,0 +1,11 @@ +package com.iluwatar.forkjoin; + +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void shouldExecuteWithoutException() { + App.main(new String[] {}); + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java new file mode 100644 index 000000000000..6af56dfc38e1 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/ForkJoinSumCalculatorTest.java @@ -0,0 +1,56 @@ +package com.iluwatar.forkjoin; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.LongStream; +import org.junit.jupiter.api.Test; + +class ForkJoinSumCalculatorTest { + + @Test + void shouldReturnZeroForNullArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + + assertEquals(0L, calculator.calculateSum(null)); + } + + @Test + void shouldReturnZeroForEmptyArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + + assertEquals(0L, calculator.calculateSum(new long[0])); + } + + @Test + void shouldCalculateSumOfSmallArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + long[] numbers = {10, 20, 30, 40, 50}; + + long result = calculator.calculateSum(numbers); + + assertEquals(150L, result); + } + + @Test + void shouldCalculateSumOfLargeArray() { + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(); + long[] numbers = LongStream.rangeClosed(1, 100_000).toArray(); + + long result = calculator.calculateSum(numbers); + + long expected = 100_000L * 100_001L / 2; + assertEquals(expected, result); + } + + @Test + void shouldWorkWithCustomParallelism() { + // Use only 2 threads + ForkJoinSumCalculator calculator = new ForkJoinSumCalculator(2); + long[] numbers = LongStream.rangeClosed(1, 50_000).toArray(); + + long result = calculator.calculateSum(numbers); + + long expected = 50_000L * 50_001L / 2; + assertEquals(expected, result); + } +} diff --git a/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java b/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java new file mode 100644 index 000000000000..03df6a6bc268 --- /dev/null +++ b/fork-join/src/test/java/com/iluwatar/forkjoin/SumTaskTest.java @@ -0,0 +1,85 @@ +package com.iluwatar.forkjoin; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ForkJoinPool; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Test; + +class SumTaskTest { + + @Test + void shouldSumSmallArrayDirectly() { + // Array smaller than threshold — should compute without forking + long[] numbers = {1, 2, 3, 4, 5}; + SumTask task = new SumTask(numbers, 0, numbers.length); + + long result = ForkJoinPool.commonPool().invoke(task); + + assertEquals(15L, result); + } + + @Test + void shouldSumLargeArrayUsingForkJoin() { + // Array larger than threshold — will fork into subtasks + long[] numbers = LongStream.rangeClosed(1, 10_000).toArray(); + SumTask task = new SumTask(numbers, 0, numbers.length); + + long result = ForkJoinPool.commonPool().invoke(task); + + // Sum of 1 to N = N*(N+1)/2 + long expected = 10_000L * 10_001L / 2; + assertEquals(expected, result); + } + + @Test + void shouldSumPartialRange() { + // Sum only a portion of the array (indices 2 to 5) + long[] numbers = {10, 20, 30, 40, 50, 60}; + SumTask task = new SumTask(numbers, 2, 5); + + long result = ForkJoinPool.commonPool().invoke(task); + + // 30 + 40 + 50 = 120 + assertEquals(120L, result); + } + + @Test + void shouldReturnZeroForEmptyRange() { + long[] numbers = {1, 2, 3}; + SumTask task = new SumTask(numbers, 1, 1); // start == end, empty range + + long result = ForkJoinPool.commonPool().invoke(task); + + assertEquals(0L, result); + } + + @Test + void shouldHandleSingleElement() { + long[] numbers = {42}; + SumTask task = new SumTask(numbers, 0, 1); + + long result = ForkJoinPool.commonPool().invoke(task); + + assertEquals(42L, result); + } + + @Test + void shouldProduceCorrectResultForMillionElements() { + long[] numbers = LongStream.rangeClosed(1, 1_000_000).toArray(); + SumTask task = new SumTask(numbers, 0, numbers.length); + + long result = ForkJoinPool.commonPool().invoke(task); + + long expected = 1_000_000L * 1_000_001L / 2; + assertEquals(expected, result); + } + + @Test + void shouldThrowExceptionWhenStartGreaterThanEnd() { + long[] numbers = {1, 2, 3, 4, 5}; + + assertThrows(IllegalArgumentException.class, () -> new SumTask(numbers, 4, 2)); + } +} diff --git a/localization/pt/delegation/README.md b/localization/pt/delegation/README.md new file mode 100644 index 000000000000..9f424895891d --- /dev/null +++ b/localization/pt/delegation/README.md @@ -0,0 +1,157 @@ +--- +title: "Delegation Pattern in Java: Mastering Efficient Task Assignment" +shortTitle: Delegation +description: "Explore the Delegation Design Pattern in Java with real-world examples, class diagrams, and its benefits. Learn how to enhance your code flexibility and reuse." +category: Behavioral +language: pt +tag: + - Decoupling + - Delegation + - Object composition +--- + +## Também conhecido como + +* Helper +* Surrogate + +## Propósito + +Permitir que um objeto delegue a responsabilidade por uma tarefa a outro objeto auxiliar. + +## Explicação + +Exemplo do mundo real + +> Em um restaurante, o chef principal delega tarefas aos sous-chefs: um cuida das grelhas, outro das saladas e um terceiro é responsável pelas sobremesas. Cada sous-chef é especializado em sua área, permitindo que o chef principal se concentre na gestão geral da cozinha. Isso reflete o padrão Delegation, no qual um objeto principal delega tarefas específicas a objetos auxiliares, cada um especialista em seu domínio. + +Em outras palavras + +> Delegation é um padrão de design em que um objeto repassa uma tarefa a um objeto auxiliar. + +De acordo com a Wikipédia + +> Em programação orientada a objetos, delegação se refere a avaliar um membro (propriedade ou método) de um objeto (o receptor) no contexto de outro objeto original (o remetente). A delegação pode ser feita explicitamente, passando o objeto remetente para o objeto receptor, o que pode ser feito em qualquer linguagem orientada a objetos; ou implicitamente, pelas regras de busca de membros da linguagem, o que exige suporte da linguagem para esse recurso. + +Diagrama de sequência + +![Delegation sequence diagram](../../../delegation/etc/delegation-sequence-diagram.png) + +## Exemplo Programático + +Vamos considerar um exemplo de impressão. + +Temos uma interface `Printer` e três implementações: `CanonPrinter`, `EpsonPrinter` e `HpPrinter`. + +```java +public interface Printer { + void print(final String message); +} + +@Slf4j +public class CanonPrinter implements Printer { + @Override + public void print(String message) { + LOGGER.info("Canon Printer : {}", message); + } +} + +@Slf4j +public class EpsonPrinter implements Printer { + @Override + public void print(String message) { + LOGGER.info("Epson Printer : {}", message); + } +} + +@Slf4j +public class HpPrinter implements Printer { + @Override + public void print(String message) { + LOGGER.info("HP Printer : {}", message); + } +} +``` + +O `PrinterController` pode ser usado como um `Printer`, delegando qualquer trabalho tratado por essa interface a um objeto que a implemente. + +```java +public class PrinterController implements Printer { + + private final Printer printer; + + public PrinterController(Printer printer) { + this.printer = printer; + } + + @Override + public void print(String message) { + printer.print(message); + } +} +``` + +No código cliente, os controladores de impressora podem imprimir mensagens de formas diferentes, dependendo do objeto para o qual delegam esse trabalho. + +```java +public class App { + + private static final String MESSAGE_TO_PRINT = "hello world"; + + public static void main(String[] args) { + var hpPrinterController = new PrinterController(new HpPrinter()); + var canonPrinterController = new PrinterController(new CanonPrinter()); + var epsonPrinterController = new PrinterController(new EpsonPrinter()); + + hpPrinterController.print(MESSAGE_TO_PRINT); + canonPrinterController.print(MESSAGE_TO_PRINT); + epsonPrinterController.print(MESSAGE_TO_PRINT); + } +} +``` + +Saída do programa: + +``` +HP Printer:hello world +Canon Printer:hello world +Epson Printer:hello world +``` + +## Quando usar o padrão Delegation + +* Quando você deseja passar a responsabilidade de uma classe para outra sem usar herança. +* Para obter reutilização baseada em composição em vez de herança. +* Quando você precisa usar diversas classes auxiliares intercambiáveis em tempo de execução. + +## Aplicações do mundo real do padrão Delegation + +* O pacote java.awt.event do Java, no qual listeners são frequentemente usados para tratar eventos. +* Classes wrapper do Java Collections Framework (java.util.Collections), que delegam a outros objetos de coleção. +* No Spring Framework, a delegação é usada extensivamente no container IoC, no qual beans delegam tarefas a outros beans. + +## Benefícios e desafios do padrão Delegation + +Benefícios: + +* Reduz a criação de subclasses: os objetos podem delegar operações a objetos diferentes e alterá-los em tempo de execução, reduzindo a necessidade de criar subclasses. +* Incentiva a reutilização: a delegação promove a reutilização do código do objeto auxiliar. +* Aumenta a flexibilidade: ao delegar tarefas a objetos auxiliares, é possível alterar o comportamento das suas classes em tempo de execução. + +Desafios: + +* Sobrecarga em tempo de execução: a delegação pode introduzir camadas adicionais de indireção, o que pode resultar em pequenos custos de desempenho. +* Complexidade: o design pode se tornar mais complicado, pois envolve classes e interfaces adicionais para gerenciar a delegação. + +## Padrões relacionados + +* [Composite](https://java-design-patterns.com/patterns/composite/): a delegação pode ser usada dentro de um padrão composite para delegar comportamentos específicos de componentes a componentes filhos. +* [Strategy](https://java-design-patterns.com/patterns/strategy/): a delegação é frequentemente usada no padrão strategy, no qual um objeto de contexto delega tarefas a um objeto de estratégia. +* https://java-design-patterns.com/patterns/proxy/: o padrão proxy é uma forma de delegação em que um objeto proxy controla o acesso a outro objeto, ao qual delega o trabalho. + +## Referências e Créditos + +* [Effective Java](https://amzn.to/4aGE7gX) +* [Head First Design Patterns](https://amzn.to/3J9tuaB) +* [Refactoring: Improving the Design of Existing Code](https://amzn.to/3VOcRsw) +* [Delegate Pattern: Wikipedia ](https://en.wikipedia.org/wiki/Delegation_pattern) diff --git a/localization/pt/money/README.md b/localization/pt/money/README.md new file mode 100644 index 000000000000..278924fb1315 --- /dev/null +++ b/localization/pt/money/README.md @@ -0,0 +1,131 @@ +--- +title: "Money Pattern in Java: Encapsulating Monetary Values with Currency Consistency" +shortTitle: Money +description: "Learn how the Money design pattern in Java ensures currency safety, precision handling, and maintainable financial operations. Explore examples, applicability, and benefits of the pattern." +category: Structural +language: pt +tag: + - Business + - Domain + - Encapsulation + - Immutable +--- + +## Também conhecido como + +* Monetary Value Object + +## Propósito + +Encapsular valores monetários e sua moeda associada em um objeto específico do domínio. + +## Explicação + +Exemplo do mundo real + +> Imagine um sistema de vale-presente on-line, no qual cada vale-presente mantém um saldo específico em uma determinada moeda. Em vez de usar apenas um valor de ponto flutuante para o saldo, o sistema usa um objeto Money para rastrear o valor e a moeda com precisão. Sempre que alguém usa o vale-presente, o saldo é atualizado com cálculos precisos que evitam erros de arredondamento de ponto flutuante, garantindo que a lógica de domínio permaneça consistente e correta. + +Em outras palavras + +> O padrão Money encapsula tanto um valor quanto sua moeda, garantindo que as operações financeiras sejam precisas, consistentes e fáceis de manter. + +De acordo com a Wikipédia + +> O padrão de design Money encapsula um valor monetário e sua moeda, permitindo operações aritméticas e conversões seguras, ao mesmo tempo em que preserva a precisão e a consistência nos cálculos financeiros. + +Mapa mental + +![Money Pattern Mind Map](../../../money/etc/money-mind-map.png) + +Fluxograma + +![Money Pattern Flowchart](../../../money/etc/money-flowchart.png) + +## Exemplo Programático + +Neste exemplo, criamos uma classe `Money` para demonstrar como valores monetários podem ser encapsulados junto com sua moeda. Essa abordagem ajuda a evitar imprecisões de ponto flutuante, garante que as operações aritméticas sejam tratadas de forma consistente e fornece uma maneira clara e centrada no domínio de trabalhar com dinheiro. + +```java +@AllArgsConstructor +@Getter +public class Money { + private double amount; + private String currency; + + public Money(double amnt, String curr) { + this.amount = amnt; + this.currency = curr; + } + + private double roundToTwoDecimals(double value) { + return Math.round(value * 100.0) / 100.0; + } + + public void addMoney(Money moneyToBeAdded) throws CannotAddTwoCurrienciesException { + if (!moneyToBeAdded.getCurrency().equals(this.currency)) { + throw new CannotAddTwoCurrienciesException("You are trying to add two different currencies"); + } + this.amount = roundToTwoDecimals(this.amount + moneyToBeAdded.getAmount()); + } + + public void subtractMoney(Money moneyToBeSubtracted) throws CannotSubtractException { + if (!moneyToBeSubtracted.getCurrency().equals(this.currency)) { + throw new CannotSubtractException("You are trying to subtract two different currencies"); + } else if (moneyToBeSubtracted.getAmount() > this.amount) { + throw new CannotSubtractException("The amount you are trying to subtract is larger than the amount you have"); + } + this.amount = roundToTwoDecimals(this.amount - moneyToBeSubtracted.getAmount()); + } + + public void multiply(int factor) { + if (factor < 0) { + throw new IllegalArgumentException("Factor must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * factor); + } + + public void exchangeCurrency(String currencyToChangeTo, double exchangeRate) { + if (exchangeRate < 0) { + throw new IllegalArgumentException("Exchange rate must be non-negative"); + } + this.amount = roundToTwoDecimals(this.amount * exchangeRate); + this.currency = currencyToChangeTo; + } +} +``` + +Ao encapsular toda a lógica relacionada a dinheiro em uma única classe, reduzimos o risco de misturar moedas diferentes, melhoramos a clareza do código-base e facilitamos futuras modificações, como adicionar novas moedas ou refinar as regras de arredondamento. Esse padrão fortalece o modelo de domínio ao tratar o dinheiro como um conceito distinto, e não apenas como mais um valor numérico. + +## Quando usar o padrão Money + +* Quando cálculos financeiros ou manipulações de dinheiro fazem parte da lógica de negócio +* Quando é necessário um tratamento preciso de valores monetários para evitar imprecisões de ponto flutuante +* Quando princípios de domain-driven design e tipagem forte são desejados + +## Aplicações do mundo real do padrão Money + +* A biblioteca JSR 354 (Java Money and Currency) em Java +* Modelos de domínio personalizados em sistemas de e-commerce e contabilidade + +## Benefícios e desafios do padrão Money + +Benefícios + +* Fornece uma representação única e type-safe para valores monetários e moeda +* Incentiva o encapsulamento de operações relacionadas, como adição, subtração e formatação +* Evita erros de ponto flutuante ao usar inteiros ou bibliotecas decimais especializadas + +Desafios + +* Requer classes e infraestrutura adicionais para lidar com conversões e formatação de moeda +* Pode introduzir sobrecarga de desempenho ao realizar um grande número de operações monetárias + +## Padrões relacionados + +* [Value Object](https://java-design-patterns.com/patterns/value-object/): Money é tipicamente um exemplo clássico de value object em domain-driven design. + +## Referências e Créditos + +* [Domain-Driven Design: Tackling Complexity in the Heart of Software](https://amzn.to/3wlDrze) +* [Implementing Domain-Driven Design](https://amzn.to/4dmBjrB) +* [Patterns of Enterprise Application Architecture](https://amzn.to/3WfKBPR) diff --git a/localization/pt/polling-publisher/README.md b/localization/pt/polling-publisher/README.md new file mode 100644 index 000000000000..d9cf98a550b3 --- /dev/null +++ b/localization/pt/polling-publisher/README.md @@ -0,0 +1,119 @@ +--- +title: "Polling Publisher-Subscriber Pattern in Java: Mastering Asynchronous Messaging Elegantly" +shortTitle: Polling Pub/Sub +description: "Learn how to implement a Polling Publisher-Subscriber system in Java using Spring Boot and Kafka. Explore the architecture, real-world analogies, and benefits of asynchronous communication with clean code examples." +category: Architectural +language: pt +tag: + - Spring Boot + - Kafka + - Microservices + - Asynchronous Messaging + - Decoupling +--- + +## Também conhecido como + +* Event-Driven Architecture +* Asynchronous Pub/Sub Pattern +* Message Queue-Based Polling System + +## Propósito + +O padrão Polling Publisher-Subscriber desacopla os produtores de dados dos consumidores ao permitir uma comunicação assíncrona e orientada a mensagens. Um serviço consulta periodicamente (poll) uma fonte de dados e publica mensagens em um message broker (por exemplo, Kafka), que são então consumidas por um ou mais serviços assinantes. + +## Explicação + +### Exemplo do mundo real + +> Uma agência de notícias consulta constantemente as últimas atualizações. Assim que recebe novas informações, ela as publica em diferentes veículos (TV, jornais, aplicativos). Cada veículo consome e exibe as atualizações de forma independente. + +### Em outras palavras + +> Um serviço verifica regularmente se há atualizações (polling) e envia mensagens para o Kafka. Outro serviço escuta o Kafka e processa as mensagens de forma assíncrona. + +### De acordo com a Wikipédia + +> Este padrão se assemelha muito ao [modelo Publish–subscribe](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern), no qual as mensagens são enviadas pelos publicadores e recebidas pelos assinantes sem que eles se conheçam. + +### Fluxo da arquitetura + +``` ++------------+ +--------+ +-------------+ +| Publisher | ---> | Kafka | ---> | Subscriber | ++------------+ +--------+ +-------------+ +``` + +## Exemplo Programático (Spring Boot + Kafka) + +### Serviço Publisher + +- Usa o `@Scheduled` do Spring para consultar dados periodicamente. +- Publica dados em um tópico do Kafka. +- Opcionalmente expõe uma API REST para publicação manual de dados. + +```java +@Scheduled(fixedRate = 5000) +public void pollAndPublish() { + String data = pollingService.getLatestData(); + kafkaTemplate.send("updates-topic", data); +} +``` + +### Serviço Subscriber + +- Escuta um tópico do Kafka usando `@KafkaListener`. +- Processa as mensagens de forma assíncrona. + +```java +@KafkaListener(topics = "updates-topic") +public void processUpdate(String message) { + log.info("Received update: {}", message); + updateProcessor.handle(message); +} +``` + +## Quando usar o padrão Polling Publisher-Subscriber + +Use esse padrão quando: + +* Não é possível o produtor enviar dados em tempo real (push). +* Deseja-se um baixo acoplamento entre produtores e consumidores. +* É necessário processamento de eventos assíncrono e escalável. +* Você está construindo uma arquitetura de microsserviços orientada a eventos. + +## Aplicações do mundo real + +* Painéis de relatórios em tempo real +* Agregadores de health check para sistemas distribuídos +* Processamento de telemetria de IoT +* Sistemas de notificação e alerta + +## Benefícios e desafios do padrão Polling Pub/Sub + +### Benefícios + +* Baixo acoplamento entre serviços +* Arquitetura assíncrona e escalável +* Tolerante a falhas, com persistência de mensagens no Kafka +* Fácil de estender com novos consumidores ou publicadores + +### Desafios + +* O polling introduz latência entre a geração e o consumo dos dados +* Requer o gerenciamento e a configuração do Kafka (ou de outro broker) +* Implantação e infraestrutura ligeiramente mais complexas + +## Padrões relacionados + +* [Observer Pattern](https://java-design-patterns.com/patterns/observer/) +* [Mediator Pattern](https://java-design-patterns.com/patterns/mediator/) +* [Message Queue Pattern](https://java-design-patterns.com/patterns/event-queue/) + +## Referências e Créditos + +* [Apache Kafka Documentation](https://kafka.apache.org/documentation/) +* [Spring Kafka Documentation](https://docs.spring.io/spring-kafka) +* [Spring Scheduled Tasks](https://www.baeldung.com/spring-scheduled-tasks) +* [Spring Kafka Tutorial – Baeldung](https://www.baeldung.com/spring-kafka) +* Inspired by: [iluwatar/java-design-patterns](https://github.com/iluwatar/java-design-patterns) diff --git a/microservices-distributed-tracing/pom.xml b/microservices-distributed-tracing/pom.xml index c7dddf0fa0b6..fa9e288879f1 100644 --- a/microservices-distributed-tracing/pom.xml +++ b/microservices-distributed-tracing/pom.xml @@ -52,7 +52,7 @@ io.zipkin.reporter2 zipkin-reporter-brave - 3.5.0 + 3.5.3 org.junit.jupiter diff --git a/pom.xml b/pom.xml index 46cec4ee61c4..4ed64e643219 100644 --- a/pom.xml +++ b/pom.xml @@ -139,6 +139,7 @@ fluent-interface flux flyweight + fork-join front-controller function-composition game-loop @@ -252,6 +253,7 @@ view-helper virtual-proxy visitor + write-ahead-log backpressure actor-model rate-limiting-pattern diff --git a/write-ahead-log/README.md b/write-ahead-log/README.md new file mode 100644 index 000000000000..45caab13cadf --- /dev/null +++ b/write-ahead-log/README.md @@ -0,0 +1,217 @@ +--- +title: "Write-Ahead Log (WAL) Pattern in Java: Ensuring Data Durability and Crash Recovery" +shortTitle: Write-Ahead Log +description: "Learn about the Write-Ahead Log (WAL) design pattern in Java. Discover how append-only logging guarantees data durability and fast crash recovery in database engines and distributed systems." +category: Data Access +language: en +tag: + - Data access + - Storage + - Fault tolerance + - Transactions + - Performance +--- + +## Also known as + +* Append-Only Log +* Redo Log +* Journaling + +## Intent of Write-Ahead Log Pattern + +The Write-Ahead Log (WAL) design pattern ensures data durability and system recoverability in database engines, distributed consensus protocols, and transactional systems. It enforces a strict order of operations where any state mutation (e.g., insert, update, delete) must be written sequentially to an append-only log file on stable storage (disk) before it is applied to the main database state or in-memory storage structures. + +## Detailed Explanation of Write-Ahead Log Pattern with Real-World Examples + +Real-world example + +> Imagine an accountant managing a company's ledger. Before modifying the main financial summary balance sheets, the accountant immediately records every incoming transaction line-by-line into a sequential physical logbook. If power cuts out mid-day or the summary balance sheets are damaged, the accountant can re-open the physical logbook, replay every recorded entry from the beginning, and perfectly recalculate the final financial state. + +In plain words + +> Write-Ahead Log guarantees that no state mutation is lost during sudden system crashes by writing changes to a fast append-only disk log file before updating the in-memory store. + +Wikipedia says + +> In computer science, write-ahead logging (WAL) is a family of techniques for providing atomicity and durability (two of the ACID properties) in database systems. In a system using WAL, all modifications are written to a log before they are applied. Usually both redo and undo information are stored in the log. + +Class Diagram + +```mermaid +classDiagram + class OperationType { + <> + SET + DELETE + CHECKPOINT + } + + class LogEntry { + -long sequenceNumber + -OperationType type + -String key + -String value + +toLogString() String + +fromLogString(String line)$ LogEntry + } + + class WriteAheadLog { + -File logFile + -AtomicLong sequenceNumberCounter + +append(OperationType type, String key, String value) LogEntry + +readAll() List~LogEntry~ + +clear() void + } + + class DatabaseStore { + -WriteAheadLog wal + -Map~String, String~ memTable + +put(String key, String value) void + +delete(String key) void + +get(String key) String + +checkpoint() void + +simulateCrash() void + +recover() void + } + + DatabaseStore --> WriteAheadLog + WriteAheadLog --> LogEntry + LogEntry --> OperationType +``` + +## Programmatic Example of Write-Ahead Log Pattern in Java + +The `WriteAheadLog` class manages append-only sequential writes to disk: + +```java +public class WriteAheadLog { + private final File logFile; + private final AtomicLong sequenceNumberCounter = new AtomicLong(0); + + public synchronized LogEntry append(OperationType type, String key, String value) throws IOException { + long nextSeq = sequenceNumberCounter.incrementAndGet(); + LogEntry entry = new LogEntry(nextSeq, type, key, value); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true))) { + writer.write(entry.toLogString()); + writer.newLine(); + writer.flush(); + } + return entry; + } +} +``` + +The `DatabaseStore` class coordinates writing to the log before modifying its in-memory `MemTable`: + +```java +public class DatabaseStore { + private final WriteAheadLog wal; + private final Map memTable = new HashMap<>(); + + public synchronized void put(String key, String value) throws IOException { + wal.append(OperationType.SET, key, value); + memTable.put(key, value); + } + + public synchronized void delete(String key) throws IOException { + wal.append(OperationType.DELETE, key, null); + memTable.remove(key); + } + + public synchronized void recover() { + memTable.clear(); + List entries = wal.readAll(); + for (LogEntry entry : entries) { + if (entry.getType() == OperationType.SET) { + memTable.put(entry.getKey(), entry.getValue()); + } else if (entry.getType() == OperationType.DELETE) { + memTable.remove(entry.getKey()); + } + } + } +} +``` + +The `App` class demonstrates initialization, writes, crash simulation, and WAL recovery: + +```java +@Slf4j +public class App { + public static void main(String[] args) { + try { + File logFile = File.createTempFile("wal_demo", ".log"); + WriteAheadLog wal = new WriteAheadLog(logFile); + DatabaseStore store = new DatabaseStore(wal); + + store.put("user:101", "Alice"); + store.put("user:102", "Bob"); + store.delete("user:103"); + + // Simulating system crash where in-memory state is lost + store.simulateCrash(); + + // System reboot & recovery from WAL log replay + store.recover(); + + LOGGER.info("MemTable post recovery: {}", store.getMemTableSnapshot()); + } catch (IOException e) { + LOGGER.error("Error running WAL demo", e); + } + } +} +``` + +Program output: + +```text +15:45:00.100 [main] INFO com.iluwatar.writeaheadlog.App -- === 1. Initializing Storage Engine with WAL === +15:45:00.105 [main] INFO com.iluwatar.writeaheadlog.WriteAheadLog -- WAL Entry appended & flushed to disk: LogEntry(sequenceNumber=1, type=SET, key=user:101, value=Alice) +15:45:00.106 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- Applied SET operation to MemTable: user:101 = Alice +15:45:00.107 [main] INFO com.iluwatar.writeaheadlog.App -- === 3. Simulating Unexpected System Crash === +15:45:00.108 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- !!! SIMULATED SYSTEM CRASH: In-memory MemTable has been wiped !!! +15:45:00.109 [main] INFO com.iluwatar.writeaheadlog.App -- === 4. System Restart & Recovery from WAL === +15:45:00.110 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- Starting recovery process from WAL... +15:45:00.112 [main] INFO com.iluwatar.writeaheadlog.DatabaseStore -- Recovery completed. Replayed 5 log entries into MemTable. +15:45:00.113 [main] INFO com.iluwatar.writeaheadlog.App -- MemTable snapshot post recovery: {user:101=Alice, user:102=Bob Smith} +``` + +## When to Use the Write-Ahead Log Pattern in Java + +* Building storage engines or key-value data stores requiring ACID durability guarantees. +* Implementing fault-tolerant distributed consensus protocols (e.g., Raft, Paxos). +* System architectures where random disk I/O is expensive, allowing sequential append-only writes for maximum throughput. +* Message brokers or event streams requiring replayability after failure. + +## Real-World Applications of Write-Ahead Log Pattern in Java + +* **PostgreSQL / MySQL (InnoDB):** Uses WAL / Redo Log for crash recovery and replication. +* **SQLite:** Write-Ahead Logging mode for concurrency and atomic commits. +* **Apache Cassandra / RocksDB:** Appends mutations to CommitLog / WAL before MemTable updates. +* **Apache Kafka / Raft:** Log replication across distributed nodes for consensus and state machine replication. + +## Benefits and Trade-offs of Write-Ahead Log Pattern + +Benefits: + +* **High Performance:** Sequential disk writes are significantly faster than random disk updates (e.g., updating B-Trees directly). +* **Durability & Fault Tolerance:** Guarantees no committed transaction is lost during sudden system crashes. +* **Simplicity of Recovery:** Replaying ordered log records deterministically restores the exact last-known state. + +Trade-offs: + +* **Storage Overhead:** Log files grow over time, requiring periodic checkpointing and log truncation. +* **Recovery Time:** Large log files without checkpoints can lead to slow startup/recovery times. + +## Related Java Design Patterns + +* [Event Sourcing](https://java-design-patterns.com/patterns/event-sourcing/): Captures state mutations as a sequence of events, similar to log replay. +* [Command](https://java-design-patterns.com/patterns/command/): Encapsulates requests as objects, which can be serialized into WAL entries. +* [Memento](https://java-design-patterns.com/patterns/memento/): Stores state snapshots (checkpoints) to truncate logs. + +## References and Credits + +* [Designing Data-Intensive Applications (Martin Kleppmann)](https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/) +* [PostgreSQL Documentation: Write-Ahead Logging (WAL)](https://www.postgresql.org/docs/current/wal-intro.html) +* [Raft Consensus Algorithm Paper](https://raft.github.io/) diff --git a/write-ahead-log/pom.xml b/write-ahead-log/pom.xml new file mode 100644 index 000000000000..adb766953a47 --- /dev/null +++ b/write-ahead-log/pom.xml @@ -0,0 +1,87 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + write-ahead-log + 1.26.0-SNAPSHOT + write-ahead-log + http://maven.apache.org + + UTF-8 + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.projectlombok + lombok + ${lombok.version} + provided + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.writeaheadlog.App + + + + + + + + + diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java new file mode 100644 index 000000000000..e8e9fae2f763 --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/App.java @@ -0,0 +1,82 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import java.io.File; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; + +/** + * Main application class demonstrating the Write-Ahead Log (WAL) design pattern. + * + *

The WAL pattern guarantees durability by ensuring every mutation (SET, DELETE) is written to a + * persistent append-only log file on disk BEFORE updating in-memory state. If the system crashes + * unexpectedly, replaying log entries from the WAL file restores state. + */ +@Slf4j +public class App { + + /** + * Application entry point. + * + * @param args command line arguments + */ + public static void main(String[] args) { + try { + File logFile = File.createTempFile("wal_demo", ".log"); + logFile.deleteOnExit(); + + LOGGER.info( + "=== 1. Initializing Storage Engine with WAL at {} ===", logFile.getAbsolutePath()); + WriteAheadLog wal = new WriteAheadLog(logFile); + DatabaseStore store = new DatabaseStore(wal); + + LOGGER.info("=== 2. Performing Data Operations (Write-Ahead Logging) ==="); + store.put("user:101", "Alice"); + store.put("user:102", "Bob"); + store.put("user:103", "Charlie"); + store.put("user:102", "Bob Smith"); + store.delete("user:103"); + store.checkpoint(); + + LOGGER.info("MemTable snapshot before crash: {}", store.getMemTableSnapshot()); + + LOGGER.info("=== 3. Simulating Unexpected System Crash ==="); + store.simulateCrash(); + LOGGER.info("MemTable snapshot after crash: {}", store.getMemTableSnapshot()); + + LOGGER.info("=== 4. System Restart & Recovery from WAL ==="); + store.recover(); + LOGGER.info("MemTable snapshot post recovery: {}", store.getMemTableSnapshot()); + + if (logFile.exists()) { + logFile.delete(); + } + } catch (IOException e) { + LOGGER.error("An error occurred during WAL demonstration: {}", e.getMessage(), e); + } + } +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java new file mode 100644 index 000000000000..dd6d94fce7aa --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/DatabaseStore.java @@ -0,0 +1,131 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Storage engine demonstrating Write-Ahead Log (WAL) pattern. All mutations are logged and flushed + * to persistent WAL storage before modifying the in-memory MemTable. + */ +@Slf4j +public class DatabaseStore { + + @Getter private final WriteAheadLog wal; + private final Map memTable = new HashMap<>(); + + /** + * Constructs DatabaseStore with the specified WriteAheadLog. + * + * @param wal persistent write-ahead log manager + */ + public DatabaseStore(WriteAheadLog wal) { + this.wal = wal; + } + + /** + * Stores a key-value pair. First writes to WAL on disk, then updates the in-memory MemTable. + * + * @param key target entry key + * @param value target entry value + * @throws IOException if writing to WAL fails + */ + public synchronized void put(String key, String value) throws IOException { + wal.append(OperationType.SET, key, value); + memTable.put(key, value); + LOGGER.info("Applied SET operation to MemTable: {} = {}", key, value); + } + + /** + * Removes a key-value pair. First writes DELETE operation to WAL on disk, then updates the + * in-memory MemTable. + * + * @param key target entry key to delete + * @throws IOException if writing to WAL fails + */ + public synchronized void delete(String key) throws IOException { + wal.append(OperationType.DELETE, key, null); + memTable.remove(key); + LOGGER.info("Applied DELETE operation to MemTable: {}", key); + } + + /** + * Retrieves value associated with key from the in-memory MemTable. + * + * @param key key to lookup + * @return value or null if non-existent + */ + public synchronized String get(String key) { + return memTable.get(key); + } + + /** + * Returns an unmodifiable view of current in-memory MemTable state. + * + * @return unmodifiable map of stored data + */ + public synchronized Map getMemTableSnapshot() { + return Collections.unmodifiableMap(new HashMap<>(memTable)); + } + + /** + * Writes a CHECKPOINT log record. + * + * @throws IOException if writing to WAL fails + */ + public synchronized void checkpoint() throws IOException { + wal.append(OperationType.CHECKPOINT, null, null); + LOGGER.info("Checkpoint written to WAL."); + } + + /** Simulates a system crash or power outage where in-memory state is wiped. */ + public synchronized void simulateCrash() { + memTable.clear(); + LOGGER.info("!!! SIMULATED SYSTEM CRASH: In-memory MemTable has been wiped !!!"); + } + + /** Replays log entries from persistent Write-Ahead Log to fully recover in-memory state. */ + public synchronized void recover() { + LOGGER.info("Starting recovery process from WAL..."); + memTable.clear(); + List entries = wal.readAll(); + + for (LogEntry entry : entries) { + if (entry.getType() == OperationType.SET) { + memTable.put(entry.getKey(), entry.getValue()); + } else if (entry.getType() == OperationType.DELETE) { + memTable.remove(entry.getKey()); + } + } + LOGGER.info("Recovery completed. Replayed {} log entries into MemTable.", entries.size()); + } +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java new file mode 100644 index 000000000000..7ae212c8337c --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/LogEntry.java @@ -0,0 +1,83 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** Represents a single record entry in the Write-Ahead Log. */ +@Getter +@AllArgsConstructor +@EqualsAndHashCode +@ToString +public class LogEntry { + + private static final String DELIMITER = "|"; + + private final long sequenceNumber; + private final OperationType type; + private final String key; + private final String value; + + /** + * Serializes the LogEntry to a delimited string format suitable for append-only logging. + * + * @return delimited log line representation + */ + public String toLogString() { + return sequenceNumber + + DELIMITER + + type + + DELIMITER + + (key != null ? key : "") + + DELIMITER + + (value != null ? value : ""); + } + + /** + * Deserializes a delimited string line into a LogEntry instance. + * + * @param line serialized log line + * @return parsed LogEntry + * @throws IllegalArgumentException if the log line format is invalid + */ + public static LogEntry fromLogString(String line) { + if (line == null || line.isBlank()) { + throw new IllegalArgumentException("Log line cannot be null or blank"); + } + String[] parts = line.split("\\|", -1); + if (parts.length < 4) { + throw new IllegalArgumentException("Invalid log line format: " + line); + } + long sequenceNumber = Long.parseLong(parts[0]); + OperationType type = OperationType.valueOf(parts[1]); + String key = parts[2].isEmpty() ? null : parts[2]; + String value = parts[3].isEmpty() ? null : parts[3]; + return new LogEntry(sequenceNumber, type, key, value); + } +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java new file mode 100644 index 000000000000..33e06111bdc4 --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/OperationType.java @@ -0,0 +1,33 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +/** Enumeration representing the type of operations logged in the Write-Ahead Log. */ +public enum OperationType { + SET, + DELETE, + CHECKPOINT +} diff --git a/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java new file mode 100644 index 000000000000..4ea2044ff2fc --- /dev/null +++ b/write-ahead-log/src/main/java/com/iluwatar/writeaheadlog/WriteAheadLog.java @@ -0,0 +1,134 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Manages sequential append-only logging to persistent disk storage. Ensures state changes are + * flushed to stable storage before in-memory updates. + */ +@Slf4j +public class WriteAheadLog { + + @Getter private final File logFile; + private final AtomicLong sequenceNumberCounter = new AtomicLong(0); + + /** + * Initializes WriteAheadLog with target log file. If log file exists, calculates the initial + * sequence number from existing entries. + * + * @param logFile file to use for append-only log entries + */ + public WriteAheadLog(File logFile) { + this.logFile = logFile; + initSequenceNumber(); + } + + private void initSequenceNumber() { + if (logFile.exists()) { + List existingEntries = readAll(); + if (!existingEntries.isEmpty()) { + long maxSeq = existingEntries.get(existingEntries.size() - 1).getSequenceNumber(); + sequenceNumberCounter.set(maxSeq); + } + } + } + + /** + * Appends an entry to the log file and flushes to ensure persistence. + * + * @param type operation type (SET, DELETE, CHECKPOINT) + * @param key operation target key + * @param value operation target value + * @return recorded LogEntry + * @throws IOException if writing to persistent storage fails + */ + public synchronized LogEntry append(OperationType type, String key, String value) + throws IOException { + long nextSeq = sequenceNumberCounter.incrementAndGet(); + LogEntry entry = new LogEntry(nextSeq, type, key, value); + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(logFile, true))) { + writer.write(entry.toLogString()); + writer.newLine(); + writer.flush(); + } + LOGGER.info("WAL Entry appended & flushed to disk: {}", entry); + return entry; + } + + /** + * Reads all log entries sequentially from the persistent log file. + * + * @return list of parsed LogEntries in sequential order + */ + public synchronized List readAll() { + List entries = new ArrayList<>(); + if (!logFile.exists()) { + return entries; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isBlank()) { + entries.add(LogEntry.fromLogString(line)); + } + } + } catch (IOException e) { + LOGGER.error("Failed to read log entries from WAL file: {}", logFile.getAbsolutePath(), e); + } + return entries; + } + + /** + * Clears the log file and resets the sequence number counter. Typically invoked after a + * successful checkpoint. + */ + public synchronized void clear() { + if (logFile.exists()) { + try { + Files.delete(logFile.toPath()); + sequenceNumberCounter.set(0); + LOGGER.info("WAL log cleared successfully."); + } catch (IOException e) { + LOGGER.error("Failed to clear WAL file: {}", logFile.getAbsolutePath(), e); + } + } + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java new file mode 100644 index 000000000000..b3bdbe3360ba --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/AppTest.java @@ -0,0 +1,38 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void testAppMainExecutesWithoutExceptions() { + assertDoesNotThrow(() -> App.main(new String[0])); + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java new file mode 100644 index 000000000000..08288547348b --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/DatabaseStoreTest.java @@ -0,0 +1,96 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DatabaseStoreTest { + + private File tempFile; + private WriteAheadLog wal; + private DatabaseStore store; + + @BeforeEach + void setUp() throws IOException { + tempFile = File.createTempFile("db_store_test", ".log"); + wal = new WriteAheadLog(tempFile); + store = new DatabaseStore(wal); + } + + @AfterEach + void tearDown() { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + + @Test + void testPutAndGet() throws IOException { + store.put("key1", "val1"); + assertEquals("val1", store.get("key1")); + } + + @Test + void testDelete() throws IOException { + store.put("key1", "val1"); + assertEquals("val1", store.get("key1")); + + store.delete("key1"); + assertNull(store.get("key1")); + } + + @Test + void testSimulateCrashAndRecovery() throws IOException { + store.put("key1", "val1"); + store.put("key2", "val2"); + store.put("key1", "val1_updated"); + store.delete("key2"); + store.checkpoint(); + + Map beforeCrashSnapshot = store.getMemTableSnapshot(); + assertEquals("val1_updated", beforeCrashSnapshot.get("key1")); + assertNull(beforeCrashSnapshot.get("key2")); + + store.simulateCrash(); + assertNull(store.get("key1")); + assertNull(store.get("key2")); + assertTrue(store.getMemTableSnapshot().isEmpty()); + + store.recover(); + Map recoveredSnapshot = store.getMemTableSnapshot(); + assertEquals("val1_updated", recoveredSnapshot.get("key1")); + assertNull(recoveredSnapshot.get("key2")); + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java new file mode 100644 index 000000000000..ea031e04c8a0 --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/LogEntryTest.java @@ -0,0 +1,80 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class LogEntryTest { + + @Test + void testToLogStringAndFromLogString() { + LogEntry entry = new LogEntry(1, OperationType.SET, "key1", "val1"); + String logString = entry.toLogString(); + assertEquals("1|SET|key1|val1", logString); + + LogEntry parsed = LogEntry.fromLogString(logString); + assertEquals(entry, parsed); + assertEquals(1, parsed.getSequenceNumber()); + assertEquals(OperationType.SET, parsed.getType()); + assertEquals("key1", parsed.getKey()); + assertEquals("val1", parsed.getValue()); + } + + @Test + void testDeleteLogEntrySerialization() { + LogEntry entry = new LogEntry(2, OperationType.DELETE, "key2", null); + String logString = entry.toLogString(); + assertEquals("2|DELETE|key2|", logString); + + LogEntry parsed = LogEntry.fromLogString(logString); + assertEquals(entry, parsed); + assertEquals("key2", parsed.getKey()); + assertNull(parsed.getValue()); + } + + @Test + void testInvalidLogStringThrowsException() { + assertThrows(IllegalArgumentException.class, () -> LogEntry.fromLogString(null)); + assertThrows(IllegalArgumentException.class, () -> LogEntry.fromLogString(" ")); + assertThrows(IllegalArgumentException.class, () -> LogEntry.fromLogString("1|SET|key1")); + } + + @Test + void testEqualsAndHashCode() { + LogEntry entry1 = new LogEntry(1, OperationType.SET, "k", "v"); + LogEntry entry2 = new LogEntry(1, OperationType.SET, "k", "v"); + LogEntry entry3 = new LogEntry(2, OperationType.SET, "k", "v"); + + assertEquals(entry1, entry2); + assertEquals(entry1.hashCode(), entry2.hashCode()); + assertNotEquals(entry1, entry3); + } +} diff --git a/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java new file mode 100644 index 000000000000..8f0410414e3c --- /dev/null +++ b/write-ahead-log/src/test/java/com/iluwatar/writeaheadlog/WriteAheadLogTest.java @@ -0,0 +1,134 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.iluwatar.writeaheadlog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class WriteAheadLogTest { + + private File tempFile; + private WriteAheadLog wal; + + @BeforeEach + void setUp() throws IOException { + tempFile = File.createTempFile("wal_test", ".log"); + wal = new WriteAheadLog(tempFile); + } + + @AfterEach + void tearDown() { + if (tempFile != null && tempFile.exists()) { + if (tempFile.isDirectory()) { + File[] files = tempFile.listFiles(); + if (files != null) { + for (File f : files) { + f.delete(); + } + } + } + tempFile.delete(); + } + } + + @Test + void testAppendAndReadAll() throws IOException { + LogEntry e1 = wal.append(OperationType.SET, "k1", "v1"); + LogEntry e2 = wal.append(OperationType.SET, "k2", "v2"); + LogEntry e3 = wal.append(OperationType.DELETE, "k1", null); + + assertEquals(1, e1.getSequenceNumber()); + assertEquals(2, e2.getSequenceNumber()); + assertEquals(3, e3.getSequenceNumber()); + + List entries = wal.readAll(); + assertEquals(3, entries.size()); + assertEquals(e1, entries.get(0)); + assertEquals(e2, entries.get(1)); + assertEquals(e3, entries.get(2)); + } + + @Test + void testSequenceNumberResumptionOnReopen() throws IOException { + wal.append(OperationType.SET, "k1", "v1"); + wal.append(OperationType.SET, "k2", "v2"); + + WriteAheadLog reopenedWal = new WriteAheadLog(tempFile); + LogEntry newEntry = reopenedWal.append(OperationType.SET, "k3", "v3"); + + assertEquals(3, newEntry.getSequenceNumber()); + } + + @Test + void testClear() throws IOException { + wal.append(OperationType.SET, "k1", "v1"); + assertTrue(tempFile.exists()); + + wal.clear(); + assertFalse(tempFile.exists()); + + List entries = wal.readAll(); + assertNotNull(entries); + assertTrue(entries.isEmpty()); + } + + @Test + void testReadAllIOExceptionHandling() throws IOException { + File dir = Files.createTempDirectory("wal_dir_test").toFile(); + WriteAheadLog dirWal = new WriteAheadLog(dir); + + List entries = dirWal.readAll(); + assertNotNull(entries); + assertTrue(entries.isEmpty()); + + dir.delete(); + } + + @Test + void testClearIOExceptionHandling() throws IOException { + File dir = Files.createTempDirectory("wal_nonempty_dir_test").toFile(); + File child = new File(dir, "child.txt"); + child.createNewFile(); + + WriteAheadLog dirWal = new WriteAheadLog(dir); + dirWal.clear(); + + assertTrue(dir.exists()); + + child.delete(); + dir.delete(); + } +}