21

I know that messing with threads inside an EJB is a big no-no, but I would just like to ask for advice on how handle this case. My EJB is calling an external Web service which may sometimes return a "busy" status. When that happens, I would like to wait for a while and then resubmit the request using the same data as before.

What would be the best way to implement this?

勿绮语
  • 9,170
  • 1
  • 29
  • 37
Ariod
  • 5,757
  • 22
  • 73
  • 103

4 Answers4

14

EJB 3.1 brought a new @Asynchronous feature that you can take advantage of:

@Asynchronous
@TransactionAttribute(NOT_SUPPORTED)
public Future<WebServiceResult> callWebService(int retries) {
    WebServiceResult result = webService.call();

    if (!result.equals(BUSY)) {
        return result;
    }

    if (retries <= 0) {
        throw new TooBusyException();
    }

    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

    return callWebService(retries - 1);
}

Then simply call you web service with:

Future<WebServiceResult> result = yourEJB.callWebService(1);

// Can do some interesting stuff here.
// ...
// ...

result.get(2, SECONDS);  // Block for up to 2 seconds.

As you can see you get configurable number of retries and timeout for free.

How does this differ from just calling Thread.sleep()? Returning Future is more explicit and manageable. Also I don't think Thread.sleep() is that harmful. The only problem is that this EJB instance can now longer be reused by other clients. With Future asynchronous invocation happens inside some other EJB and thread pool. As to importance of Thread#interrupt() inside the catch block, refer Why invoke Thread.currentThread.interrupt() when catch any InterruptException?

Another idea: use aspect around calling web service, catch BusyException once and retry.

Community
  • 1
  • 1
Tomasz Nurkiewicz
  • 334,321
  • 69
  • 703
  • 674
  • 4
    how is it any better than using Thread.sleep() in the called EJB? It's still a violation of the spec - it's just more explicit, but you're blocking 2 EJB instances - the spawned (asynchronous) one and the calling one. – Piotr Nowicki Nov 20 '11 at 15:42
  • @PiotrNowicki: you are right. The problem is that the OP *would like to wait for a while*, so some blocking is unavoidable. Of course it would be great to use JMS or some scheduler but then some layer above is needed (see *PeterPeiGuo* answer #1). In general waiting is always problematic in EE environment. – Tomasz Nurkiewicz Nov 20 '11 at 15:49
  • Agreed. It could be an asynchronous call straight from the client or pooling on the client-side. – Piotr Nowicki Nov 20 '11 at 15:54
  • 1
    @Tomasz - I think you've mentioned the best solution, which is to use [MDBs](http://docs.oracle.com/cd/B25221_05/web.1013/b14428/undejbs.htm#CIHGDAGD), especially as the OP is already using EJBs, and so therefore has some familiarity with them. – michaelok Dec 06 '11 at 17:44
  • If you absolutely need to use `Thread#sleep()`, then by all means wrap it in a `try-catch` on `InterruptedException` and inside the catch, do `Thread.currentThread().interrupt()` to guarantee proper handling of interrupted state by the container. See also a.o. http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception Edit: I see that you're not active anymore for almost 2 years. I'll edit the answer myself to prevent copypaste programmers from running into trouble. – BalusC Aug 11 '15 at 20:07
  • The example looks like a misuse of async functionality. Why write code if all it will do is wait? If waiting is the result of using async, it's wasted effort, just use a plain sync call. On the other hand, if useful work will happen if not blocking, then async is appropriate. – Rodrigo Gomez Jun 11 '19 at 00:14
12

In the EJB restrictions FAQ it specifically states that you

should not create or manage threads

And putting a thread to sleep counts as "managing" it.

In your case, when the web service returns a "busy" status, you could schedule a job to retry sending the message at a later point in time, for instance by using the Quartz Scheduler. The execution would end there, and any further processing should be delegated to the job scheduler.

Nick
  • 2,827
  • 4
  • 29
  • 39
Óscar López
  • 232,561
  • 37
  • 312
  • 386
  • 6
    You could also use built-in Java EE timers support. – Piotr Nowicki Nov 20 '11 at 15:49
  • 1
    If timers are used for retry, how do you propose to keep/pass the arguments from the original request, in order to reconstruct the request for the second call? – Ariod Nov 20 '11 at 17:00
  • Invoking a whole new layer, messaging, may solve the problem but I can see this is a heavy handed approach to some cases. What if a thread only sleeps for a few seconds? Even 10 seconds at most. All that infrasture work, setup a queue, JNDI, etc, plus the overhead of context switching, can be too much and too complex. There should be flexibility in the standard to allow waiting if the developer judges it to be a good trade off. – Rodrigo Gomez Jun 10 '19 at 23:27
0

But an external web service is external and you are opening a network connection to it and you wish to do some management stuff. This is what JCA is for, not EJB.

Chris
  • 194
  • 6
0

Here is a suggestion(or alternative way) with simple control's pool:

1 - Pass your context(EJB) as a parameter to your method(rest endpoint, scheduler, default's methods)

2 - Control the state with complementary scheduler or entity flags

3 - Be careful with the volume of data/processing

4 - Recommendations: Metrics, logs, and tests, tests, tests are strongly recommended

5 - This code is on SpringBoot but was tested in Jboss(with modifications) under EJB Context - Test carefully

6 - Use/modify as you wish: (send suggestions/comments)

BaseControlExecutor.java

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;


public class BaseControlExecutor {

    private final ScheduledThreadPoolExecutor poolExec = new ScheduledThreadPoolExecutor(2);

    public void execWithTimeout(final Runnable runnable, long timeout,
            TimeUnit timeUnit) throws Exception {
        execWithTimeout(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                runnable.run();
                return null;
            }
        }, timeout, timeUnit);
    }

    public <T> T execWithTimeout(Callable<T> callable, long timeout,    TimeUnit timeUnit) throws Exception {

        final Future<T> future = poolExec.submit(callable);

        try {
            return future.get(timeout, timeUnit);
        } catch (TimeoutException e) {
            future.cancel(true);
            throw e;
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof Error) {
                throw (Error) t;
            } else if (t instanceof Exception) {
                throw (Exception) t;
            } else {
                throw new IllegalStateException(t);
            }
        }
    }
}

EndpointControlRest.java

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@RestController
@RequestMapping(value = "/report")
@Api(tags = "Endpoint of Future")
public class EndpointControlRest extends BaseControlExecutor {

    Logger logger = LoggerFactory.getLogger(EndpointControlRest.class);

    //single metric of execution
    protected final AtomicLong counter = new AtomicLong();

    @GetMapping(path = "/withThread", produces = { "application/json" })
    @ApiOperation(value = "Return Hello count.")
    public String greeting() {

        Long countRunner = counter.incrementAndGet();
        String json = ""; //or EJB context to use in Thread - becareful

        new Thread(() -> {

            try {
                execWithTimeout(new Runnable() {
                    @Override
                    public void run() {

                        Instant start = Instant.now();
                        logger.info("Report init - " + countRunner);

                        //generating reports
                        generateBackgroundReport(json);

                        logger.info("Report End - " + countRunner);

                        Instant finish = Instant.now();
                        long timeElapsed = Duration.between(start, finish).toMillis();

                        logger.info("###DEBUG - " + countRunner + " - OK |Time exe: " + timeElapsed);

                    }
                }, 120, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                logger.info("###DEBUG - " + countRunner + " - Timeout - " + e.getMessage());
            } catch (Exception e) {
                logger.info("###DEBUG - " + countRunner + " - Exception - " + e.getMessage());
            }
        }).start();

        logger.info("####DEBUG - Rest call released");
        return "Hello " + countRunner;
    }

    public String generateBackgroundReport(String json){

        //simulating work
        Long x = 0L;
        for(Long i = 0L; i < 1000000000L; i ++){
            x = i + 1;
        }
        logger.info("####DEBUG -report: " + x);
        return "OK";
    }
}
Leonardo Savio
  • 399
  • 4
  • 7