1
\$\begingroup\$

Shortly, I need to create a Java application with client/server architecture (clients represent some sort of math functions and when they are done computing, server consumes these values and produces a final result). Also, there are should be two types of cancellation, first one allows to stop the server by entering a symbol and the second one shows periodic prompt with 3 options (continue, continue without prompt, cancel). While user sees the prompt, the result of computations shouldn't intervene in console. you can see full description here, named lab1

consumes values produced by clients (here I'm using CompletionService)

private void runFuturesConsumer() {
    new Thread(() -> {
        try {
            while (true) {
                Future<Integer> consumedFuture = completionService.take();
                Integer consumedValue = consumedFuture.get();

                if (Objects.equals(consumedValue, INTERRUPTION_VALUE)) {
                    break;  // clients were interrupted, stop futures consuming
                }           // happens after cancelServerFuture procedure call

                valuesContainer.add(consumedValue);

                if (consumedValue == SHORT_CIRCUIT_CONDITION) {
                    cancelServerFuture();
                    transferResult(true, null);
                    break;
                }

                if (valuesContainer.size() == clientsNumber) {
                    transferResult(false, null);
                    break;
                }
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }).start();
}

transfers result from Server to ApplicationManager (class that handles output/input and cancellation tasks)

private void transferResult(boolean isShortCircuited, String failureReport) {

    // simple lock/sync will produce a deadlock (if computations were completed during the prompt)
    if (outputLock.tryLock()) {
        try {
            if (failureReport != null) {
                serverListener.onFailureReported(failureReport);
            } else if (isShortCircuited) {
                serverListener.onCompletedComputation(SHORT_CIRCUIT_CONDITION, true);
            } else {
                serverListener.onCompletedComputation(valuesContainer.stream()
                        .reduce(1, (accumulator, elem) -> accumulator * elem), false);
            }
        } finally {
            cancelServerFuture();
            outputLock.unlock();
        }
    }
}

will be called when user chooses to stop execution

void stopServer() {
    if (outputLock.tryLock()) {
        transferResult(false, "stopped before the completion");
    }
}

this daemon waits for someone to enter "q" and then stops the server

private void runKeyPressDaemon() {
    Thread thread = new Thread(() -> {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

        String message = null;
        while (true) {
            try {
                message = bufferedReader.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (message != null && message.equals("q")) {
                // it's definitely bad if scheduler decides to reschedule here
                // because output lock happens only inside stop server method
                // (trying to lock right after readLine procedure creates possibility of losing the result)
                server.stopServer();
                break;
            }
        }
    });

    thread.setDaemon(true);
    thread.start();
}

handles second type of cancellation (prompt)

private void runPromptScheduler() {
    scheduledExecutor.scheduleWithFixedDelay(this::displayPrompt, INITIAL_DELAY, SCHEDULER_PERIOD, TimeUnit.SECONDS);
}

private void displayPrompt() {

    promptLock.lock();
    try {
        Scanner scanner = new Scanner(System.in);

        System.out.print("continue(1), continue without prompt(2), cancel(3): ");
        int readValue = scanner.nextInt();

        switch (readValue) {
            case 1:
                handlePrompt(PromptOptions.CONTINUE);
                break;
            case 2:
                handlePrompt(PromptOptions.CONTINUE_WITHOUT_PROMPT);
                break;
            case 3:
                handlePrompt(PromptOptions.CANCEL);
                break;
        }
    } finally {
        promptLock.unlock();
    }
}

private void handlePrompt(PromptOptions promptOptions) {
    switch (promptOptions) {
        case CONTINUE:
            break;
        case CONTINUE_WITHOUT_PROMPT:
            scheduledExecutor.shutdownNow();
            break;
        case CANCEL:
            server.stopServer();
            break;
    }
}

responds for result output

@Override
public void onCompletedComputation(int result, boolean isShortCircuited) {

    promptLock.lock();
    try {
        if (isShortCircuited) {
            System.out.println("result (short-circuit): " + result);
        } else {
            System.out.println("result: " + result);
        }
    } finally {
        scheduledExecutor.shutdownNow();
        promptLock.unlock();
    }
}

@Override
public void onFailureReported(String cause) {

    promptLock.lock();
    try {
        System.out.println("failure caused by: " + cause);
    } finally {
        scheduledExecutor.shutdownNow();
        promptLock.unlock();
    }
}

What I don't like in my solution is that there's kind of race condition, because it's not clear who will acquire the "desired" outputLock. It can be cancellation task (by entering a symbol or prompt) or FuturesConsumer if it consumed all futures (e.g., take a look at a comment from keyPressDaemon method, user can enter "q", but FuturesConsumer could take CPU time and print the result). I can move this outputLock to FuturesConsumer method, but it's not gonna solve the problem. Also, I think that prohibiting result output (when there's an active prompt) by using lock not the best solution. Thanks in advance for any recommendations and help.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

A few small-ish things the I noticed (I should stop starting my answers with some sentence like this ...):

  • You're working on bare threads. That's usually considered a bad idea, because they don't clean up so easily. It's preferred to use ExecutorService and related classes. Interestingly you already do that for the prompt ...

  • You're always starting the thread when runFuturesConsumer and runKeyPressDaemon are called. This is bad, because it's not documented anywhere and I'd expect the method to keep track of whether what you try to run is already running and not start another instance when it's not needed.

  • You didn't post the code for cancelServerFuture but the naming suggests something unsettling. Cancelling usually means that there's no result, but your code implies there should be? Maybe?
    I don't see much benefit in cancelling the server computation and then transferring something to the server (short-circuit does that). Or maybe I'm just missing something ...

  • You're handling a user-cancel as a failure. I think that's misleading. A user cancellation is never a failure. Yet after the user cancelled the computation you display the message "failure caused by: stopped before the completion". This is not really the expected behaviour. Instead I'd expect either no message or something to the effect of "Successfully cancelled execution".

  • There's no code showing what exactly the Locks are that you're using, but judging by how they are used in the code, I'm not sure whether you actually need them in the first place and if you need them, whether you should lock that way ... From what I can make of the code, the only actually necessary lock should be the outputLock (which should also be used for the prompt)

  • The interruption process you use is something that's usually better known as POISON_PILL. At least that's the conventional name for it as I heard it.

    While we're on interruption: it's interesting that you compare INTERRUPTION_VALUE through Object#equals and the other cases through a reference equals (==). I'm not quite sure why that is, but alas ...

  • You're overcomplicating your handling of the prompt. Instead of calling handlePrompt (and effectively nesting a switch block there) you could just "inline" the execution there. Additional note there: "incorrect" user input isn't handled there. Passing in "asd" just blows up the code with an InputMismatchException :(

Well this should cover most of it, I strongly recommend a follow-up review though :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.