diff --git a/README.md b/README.md index f584edc61..90d487b91 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ A programming idiom is a means of expressing a recurring construct in one or mor * [Execute Around](#execute-around) * [Double Checked Locking](#double-checked-locking) - +* [Poison Pill](#poison-pill) ## Abstract Factory [↑](#list-of-design-patterns) @@ -475,6 +475,13 @@ A programming idiom is a means of expressing a recurring construct in one or mor **Real world examples:** * [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) prototype inheritance +## Poison Pill [↑](#list-of-design-patterns) +**Intent:** Poison Pill is known predefined data item that allows to provide graceful shutdown for separate distributed consumption process. + +![alt text](https://github.com/iluwatar/java-design-patterns/blob/master/poison-pill/etc/poison-pill.png "Poison Pill") + +**Applicability:** Use the Poison Pill idiom when +* need to send signal from one thread/process to another to terminate # Frequently asked questions diff --git a/poison-pill/etc/poison-pill.png b/poison-pill/etc/poison-pill.png new file mode 100644 index 000000000..bfca5848e Binary files /dev/null and b/poison-pill/etc/poison-pill.png differ diff --git a/poison-pill/etc/poison-pill.ucls b/poison-pill/etc/poison-pill.ucls new file mode 100644 index 000000000..6d20b1974 --- /dev/null +++ b/poison-pill/etc/poison-pill.ucls @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/poison-pill/pom.xml b/poison-pill/pom.xml new file mode 100644 index 000000000..1d9d4c38a --- /dev/null +++ b/poison-pill/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.0-SNAPSHOT + + poison-pill + + + junit + junit + test + + + diff --git a/poison-pill/src/main/java/com/iluwatar/App.java b/poison-pill/src/main/java/com/iluwatar/App.java new file mode 100644 index 000000000..22de1dd28 --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/App.java @@ -0,0 +1,37 @@ +package com.iluwatar; + +/** + * One of possible approaches to terminate Producer-Consumer pattern is using PoisonPill idiom. + * If you use PoisonPill as termination signal then Producer is responsible to notify Consumer that exchange is over + * and reject any further messages. Consumer receiving PoisonPill will stop to read messages from queue. + * You also must ensure that PoisonPill will be last message that will be read from queue (if you have + * prioritized queue than this can be tricky). + * In simple cases as PoisonPill can be used just null-reference, but holding unique separate shared + * object-marker (with name "Poison" or "PoisonPill") is more clear and self describing. + */ +public class App { + + public static void main(String[] args) { + MessageQueue queue = new SimpleMessageQueue(10000); + + final Producer producer = new Producer("PRODUCER_1", queue); + final Consumer consumer = new Consumer("CONSUMER_1", queue); + + new Thread() { + @Override + public void run() { + consumer.consume(); + } + }.start(); + + new Thread() { + @Override + public void run() { + producer.send("hand shake"); + producer.send("some very important information"); + producer.send("bye!"); + producer.stop(); + } + }.start(); + } +} diff --git a/poison-pill/src/main/java/com/iluwatar/Consumer.java b/poison-pill/src/main/java/com/iluwatar/Consumer.java new file mode 100644 index 000000000..499971b6c --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/Consumer.java @@ -0,0 +1,38 @@ +package com.iluwatar; + +import com.iluwatar.Message.Headers; + +/** + * Class responsible for receiving and handling submitted to the queue messages + */ +public class Consumer { + + private final MQSubscribePoint queue; + private final String name; + + public Consumer(String name, MQSubscribePoint queue) { + this.name = name; + this.queue = queue; + } + + public void consume() { + while (true) { + Message msg; + try { + msg = queue.take(); + if (msg == Message.POISON_PILL) { + System.out.println(String.format("Consumer %s receive request to terminate.", name)); + break; + } + } catch (InterruptedException e) { + // allow thread to exit + System.err.println(e); + return; + } + + String sender = msg.getHeader(Headers.SENDER); + String body = msg.getBody(); + System.out.println(String.format("Message [%s] from [%s] received by [%s]", body, sender, name)); + } + } +} diff --git a/poison-pill/src/main/java/com/iluwatar/MQPublishPoint.java b/poison-pill/src/main/java/com/iluwatar/MQPublishPoint.java new file mode 100644 index 000000000..6cc830e71 --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/MQPublishPoint.java @@ -0,0 +1,9 @@ +package com.iluwatar; + +/** + * Endpoint to publish {@link Message} to queue + */ +public interface MQPublishPoint { + + public void put(Message msg) throws InterruptedException; +} diff --git a/poison-pill/src/main/java/com/iluwatar/MQSubscribePoint.java b/poison-pill/src/main/java/com/iluwatar/MQSubscribePoint.java new file mode 100644 index 000000000..5e15ba7b9 --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/MQSubscribePoint.java @@ -0,0 +1,9 @@ +package com.iluwatar; + +/** + * Endpoint to retrieve {@link Message} from queue + */ +public interface MQSubscribePoint { + + public Message take() throws InterruptedException; +} diff --git a/poison-pill/src/main/java/com/iluwatar/Message.java b/poison-pill/src/main/java/com/iluwatar/Message.java new file mode 100644 index 000000000..e9a67fe59 --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/Message.java @@ -0,0 +1,52 @@ +package com.iluwatar; + +import java.util.Map; + +/** + * Interface that implements the Message pattern and represents an inbound or outbound message as part of an {@link Producer}-{@link Consumer} exchange. + */ +public interface Message { + + public static final Message POISON_PILL = new Message() { + + @Override + public void addHeader(Headers header, String value) { + throw poison(); + } + + @Override + public String getHeader(Headers header) { + throw poison(); + } + + @Override + public Map getHeaders() { + throw poison(); + } + + @Override + public void setBody(String body) { + throw poison(); + } + + @Override + public String getBody() { + throw poison(); + } + + private RuntimeException poison() { + return new UnsupportedOperationException("Poison"); + } + + }; + + public enum Headers { + DATE, SENDER + } + + public void addHeader(Headers header, String value); + public String getHeader(Headers header); + public Map getHeaders(); + public void setBody(String body); + public String getBody(); +} diff --git a/poison-pill/src/main/java/com/iluwatar/MessageQueue.java b/poison-pill/src/main/java/com/iluwatar/MessageQueue.java new file mode 100644 index 000000000..03ced489b --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/MessageQueue.java @@ -0,0 +1,8 @@ +package com.iluwatar; + +/** + * Represents abstraction of channel (or pipe) that bounds {@link Producer} and {@link Consumer} + */ +public interface MessageQueue extends MQPublishPoint, MQSubscribePoint { + +} diff --git a/poison-pill/src/main/java/com/iluwatar/Producer.java b/poison-pill/src/main/java/com/iluwatar/Producer.java new file mode 100644 index 000000000..89fb75277 --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/Producer.java @@ -0,0 +1,48 @@ +package com.iluwatar; + +import java.util.Date; + +import com.iluwatar.Message.Headers; + +/** + * Class responsible for producing unit of work that can be expressed as message and submitted to queue + */ +public class Producer { + + private final MQPublishPoint queue; + private final String name; + private boolean isStopped; + + public Producer(String name, MQPublishPoint queue) { + this.name = name; + this.queue = queue; + this.isStopped = false; + } + + public void send(String body) { + if (isStopped) { + throw new IllegalStateException(String.format("Producer %s was stopped and fail to deliver requested message [%s].", body, name)); + } + Message msg = new SimpleMessage(); + msg.addHeader(Headers.DATE, new Date().toString()); + msg.addHeader(Headers.SENDER, name); + msg.setBody(body); + + try { + queue.put(msg); + } catch (InterruptedException e) { + // allow thread to exit + System.err.println(e); + } + } + + public void stop() { + isStopped = true; + try { + queue.put(Message.POISON_PILL); + } catch (InterruptedException e) { + // allow thread to exit + System.err.println(e); + } + } +} diff --git a/poison-pill/src/main/java/com/iluwatar/SimpleMessage.java b/poison-pill/src/main/java/com/iluwatar/SimpleMessage.java new file mode 100644 index 000000000..27b8db08b --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/SimpleMessage.java @@ -0,0 +1,39 @@ +package com.iluwatar; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * {@link Message} basic implementation + */ +public class SimpleMessage implements Message { + + private Map headers = new HashMap<>(); + private String body; + + @Override + public void addHeader(Headers header, String value) { + headers.put(header, value); + } + + @Override + public String getHeader(Headers header) { + return headers.get(header); + } + + @Override + public Map getHeaders() { + return Collections.unmodifiableMap(headers); + } + + @Override + public void setBody(String body) { + this.body = body; + } + + @Override + public String getBody() { + return body; + } +} diff --git a/poison-pill/src/main/java/com/iluwatar/SimpleMessageQueue.java b/poison-pill/src/main/java/com/iluwatar/SimpleMessageQueue.java new file mode 100644 index 000000000..12d519ce2 --- /dev/null +++ b/poison-pill/src/main/java/com/iluwatar/SimpleMessageQueue.java @@ -0,0 +1,27 @@ +package com.iluwatar; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** + * Bounded blocking queue wrapper + */ +public class SimpleMessageQueue implements MessageQueue { + + private final BlockingQueue queue; + + public SimpleMessageQueue(int bound) { + queue = new ArrayBlockingQueue(bound); + } + + @Override + public void put(Message msg) throws InterruptedException { + queue.put(msg); + } + + @Override + public Message take() throws InterruptedException { + return queue.take(); + } + +} diff --git a/poison-pill/src/test/java/com/iluwatar/AppTest.java b/poison-pill/src/test/java/com/iluwatar/AppTest.java new file mode 100644 index 000000000..6db5ad214 --- /dev/null +++ b/poison-pill/src/test/java/com/iluwatar/AppTest.java @@ -0,0 +1,12 @@ +package com.iluwatar; + +import org.junit.Test; + +public class AppTest { + + @Test + public void test() { + String[] args = {}; + App.main(args); + } +} diff --git a/pom.xml b/pom.xml index d81be42c9..d05182607 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ execute-around property intercepting-filter + poison-pill