5

First of all to prevent mark question as duplicate by guys who don't like read to the end I have read Producer-Consumer Logging service with Unreliable way to shutdown question. But it is not fully answer the question and answer contradicts the book text.

In book provided following code:

public class LogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread logger;
    private static final int CAPACITY = 1000;

    public LogWriter(Writer writer) {
        this.queue = new LinkedBlockingQueue<String>(CAPACITY);
        this.logger = new LoggerThread(writer);
    }

    public void start() {
        logger.start();
    }

    public void log(String msg) throws InterruptedException {
        queue.put(msg);
    }

    private class LoggerThread extends Thread {
        private final PrintWriter writer;

        public LoggerThread(Writer writer) {
            this.writer = new PrintWriter(writer, true); // autoflush
        }

        public void run() {
            try {
                while (true)
                    writer.println(queue.take());
            } catch (InterruptedException ignored) {
            } finally {
                writer.close();
            }
        }
    }
}

Now we should to understand how to stop this process. We should stop logging but should not skip already commited messages.

Author researches approach:

public void log(String msg) throws InterruptedException {
     if(!shutdownRequested)
           queue.put(msg);
     else
           throw new IllegalArgumentException("logger is shut down");
 }

and comment it like this:

Another approach to shutting down LogWriter would be to set a “shutdown requested” flag to prevent further messages from being submitted, as shown in Listing 7.14. The con- sumer could then drain the queue upon being notified that shutdown has been requested, writing out any pending messages and unblocking any producers blocked in log . However, this approach has race conditions that make it unreliable. The implementation of log is a check-then-act sequence: producers could observe that the service has not yet been shut down but still queue messages after the shutdown, again with the risk that the producer might get blocked in log and never become unblocked. There are tricks that reduce the likelihood of this (like having the consumer wait several seconds before declaring the queue drained), but these do not change the fundamental problem, merely the likelihood that it will cause a fail- ure.

The phrase build enough hard for me.

I understand that

 if(!shutdownRequested)
           queue.put(msg);

is not atomic and message can be added to the queue after shutdowning. Yes, it is not very accurate but I don't see problem. queue just will be drain and when queue will be empty we can stop LoggerThread. Especially I don't understand why producers can be blocked.

Author didn't provide full code thus I cannot understand all details. I believe that this book was read by community majority and this example has detailed explanation.

Please explain with full code example.

Community
  • 1
  • 1
gstackoverflow
  • 36,709
  • 117
  • 359
  • 710

1 Answers1

5

The first thing to understand is that when a shutdown is requested, the producer needs to stop accepting any more requests and the consumer (LoggerThread in this case) needs to drain the queue. The code you present in the question demonstrates only one side of the story; the producer rejecting any further requests when shutdownRequested is true. After this example, the author proceeds to say :

The consumer could then drain the queue upon being notified that shutdown has been requested, writing out any pending messages and unblocking any producers blocked in log

First and foremost, queue.take in LoggerThread as shown in your question will block infinitely for new messages to be available in the queue; however, if we want to shutdown the LoggerThread(gracefully), we need to make sure that the shutdown code in LoggerThread gets a chance to execute when shutdownRequested is true rather than infinitely being blocked by queue.take.

When the author says that the consumer can drain the queue, what he means is that the LogWritter can check shutdownRequested and if it is true, it can call the non blocking drainTo method to drain the current contents of the queue in a separate collection instead of calling queue.take (or call a similar non blocking method instead). Alternatey, if shutdownRequested is false, LogWriter can proceed to call queue.take as usual.

The real problem with this approach is the way the log method (being called by producers) is implemented. Since it is not atomic, it is possible that multiple threads can miss the setting of shutdownRequested to true. What happens if the number of threads that miss this update is greater than the CAPACITY of the queue. Let's take a look at the log method again. (Added curly braces for the sake of explanation) :

public void log(String msg) throws InterruptedException {
     if(!shutdownRequested) {//A. 1001 threads see shutdownRequested as false and pass the if condition.

           //B. At this point, shutdownRequested is set to true by client code
           //C. Meanwhile, the LoggerThread which is the consumer sees that shutdownRequested is true and calls 
           //queue.drainTo to drain all existing messages in the queue instead of `queue.take`. 
           //D. Producers insert the new message into the queue.    
           queue.put(msg);//Step E
     } else
           throw new IllegalArgumentException("logger is shut down");
     }
}

As shown in Step E, it is possible for multiple producer threads to call put while the LoggerThread finished draining the queue and exited the w. There should be no issues till the 1000th thread calls put. The real issue is when the 1001th thread calls put. It will block as the queue capacity is only 1000 and the LoggerThread may no longer be alive or subscribed to the queue.take method.

Chetan Kinger
  • 15,069
  • 6
  • 45
  • 82
  • It seems pretty farfetched to expect 1000 threads to be waiting between the same two lines of code. Though I realize it's still technically incorrect if it's theoretically possible. – shmosel Feb 21 '17 at 18:09
  • 1
    I'm interested to see if you can demonstrate how the shutdown process would actually drain the queue *and all blocked producers*. From my testing, `drainTo()` only drains already queued items. – shmosel Feb 21 '17 at 18:13
  • @shmosel I agree. It's a bit far fetched but I think you misunderstood my answer. 1000 threads are not waiting for `put`. It's the 1001th thread that is waiting. For `put` to block, you need 1000 elements in the queue and a block on 1001th attempt to `put` a new element. All this while ensuring that the consumer does not make space for the 1001th element which can only be possible if 1001 threads attempt to put a new element in the queue **after** the consumer has finished draining the queue and is now breaking out of the while loop. I don't see any other way to explain that paragraph! Do you? – Chetan Kinger Feb 21 '17 at 18:58
  • I didn't mean waiting in the sense of blocking. I meant in the sense of descheduling. Unless we're talking about a machine that can support 1000 simultaneously active threads, which seems even less likely. – shmosel Feb 21 '17 at 19:01
  • @shmosel Yes. Technically speaking, 1000 is a really high number. For the sake of technical correctness, if we replace 1000 with 4 in my answer and the question, then it starts making more sense realistically. Unfortunately, the author chose 1000 as the queue capacity which makes it difficult to prove a point. Do you see any other explanation? This is the best I could come up with. – Chetan Kinger Feb 21 '17 at 19:05
  • A lower capacity would generally indicate a lower producer output, which would make it just as unlikely. No, I don't have a better explanation myself. That and my second comment is why I didn't offer an answer. – shmosel Feb 21 '17 at 19:10
  • 1
    I'm asking about the method of "unblocking any producers blocked in `log`". That is, producers that may have been blocked before shutdown. – shmosel Feb 21 '17 at 19:24
  • @shmosel I have made an assumption that there are no producers blocked when `drainTo` was called. That said, `drainTo` does remove elements from the queue so I believe space availability should unblock the blocked producers anyway? – Chetan Kinger Feb 21 '17 at 19:27
  • @CKing drainTo is not mandatory here I think. per element take can theoretically lead to the result – gstackoverflow Feb 21 '17 at 19:31
  • @gstackoverflow That's right. I did mention in my answer that `LoggerThread` either calls `drainTo` or a similar non blocking call. Thanks for the accept. – Chetan Kinger Feb 21 '17 at 19:33
  • 1
    The book clearly does account for blocked producers, as I quoted. Simply draining the queue or using some other non-blocking iteration won't process waiting items, which seems to be the intent. And if there are more than 1000 blocked producers it may not even unblock them all. – shmosel Feb 21 '17 at 19:45
  • @shmosel Draining the queue creates space in the queue. Producers only block if there is no space. Once there is space, they unblock regardless of what you use to clear the space. But since I was talking about a scenario where there are no blocked producers before the 1001 threads arrive, I wouldn't want to continue this discussion here. Let's proceed to discuss in chat if you like. – Chetan Kinger Feb 21 '17 at 20:03