Preloader image

Antes de darmos uma olhada no @AccessTimeout, pode ajudar entender quando uma chamada precisa "esperar".

Esperando…​

Stateful Bean

Por padrão, é permitido que clientes façam chamadas concorrentes para um stateful session object e o container é requerido para serializar cada requisição concorrente. O container nunca permite acesso multi-thread a instancia de um componente stateful. Por essa razão, métodos de leitura/escrita bloqueiam metadados assim como componentes que gerenciam a própria concorrência (bean-managed concurrency) não são aplicáveis a um stateful session bean e não devem ser usados.

Isso significa que quando um método foo() de uma instancia stateful esta sendo executado e uma chega uma segunda requisição para esse ou outro método, o container EJB serializa a segunda requisição. Isso não permite com que o método seja executado concorrentemente mas espere ate o primeiro método ser processado.

O cliente também tem de esperar quando o bean @Stateful esta em uma transação e o cliente o invocar de fora dessa transação.

Stateless Bean

Se existir 20 instancias de um bean no pool e todas elas estiverem ocupadas, quando uma nova requisição chegar, o processo tem de esperar ate algum bean estar disponível no pool. (Nota: a semântica de pool, se houver, não é coberta pela especificação. O server vendor pode ou não envolver uma condição de espera)

Singleton Bean

O container força um acesso single-thread por padrão para um componente singleton. Isso é parecido com o que a anotação @Lock(WRITE) faz. Quando um método anotado com @Lock(WRITE) é executado, todos os outros métodos @Lock(WRITE) e @Lock(READ) que são chamados tem de esperar até que ele termine sua execução.

Resumindo

  • @Singleton - Um método @Lock(WRITE) esta sendo invocado e o gerenciamento de concorrência pelo container esta sendo usado. Todos os métodos sao @Lock(WRITE) por padrão.

  • @Stateful - Qualquer método de uma instancia pode estar sendo utilizado e uma segunda chamada pode acontecer. Ou o bean @Stateful esta em uma transação e o cliente o chama de fora dessa transação.

  • @Stateless - Sem instancias disponíveis no pool. Como observado, a semântica de pool (se houver) não é coberta pela especificação. Caso exista uma semântica no server vendor que envolva uma condição de espera, a anotação @AccessTimeout deveria ser aplicada.

@AccessTimeout

A anotação @AccessTimeout é simplesmente uma conveniência em torno da tupla long e TimeUnit comumente usadas na java.util.concurrent API.

import java.util.concurrent.TimeUnit;
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface AccessTimeout {
    long value();
    TimeUnit unit() default TimeUnit.MILLISECONDS;
}

Uso

Um método ou uma classe pode ser anotada com @AccessTimeout para especificar o temo máximo que uma chamada deve esperar para acessar o bean quando acontecer uma condição de espera.

A semântica para o value é a seguinte:

  • value > 0 indica um tempo de espera que é especificado pelo elemento unit.

  • value de 0 significa que acesso concorrente não é permitido.

  • value de -1 indica que a chamada do cliente vai ficar bloqueada por tempo indeterminado ate que possa proceder.

Simples!

Quando acontecer um timeout, qual exceção o cliente recebe? Citando a especificação: "if a client-invoked business method is in progress on an instance when another client-invoked call, from the same or different client, arrives at the same instance of a stateful session bean, if the second client is a client of the bean’s business interface or no-interface view, the concurrent invocation must result in the second client receiving a jakarta.ejb.ConcurrentAccessException[15]. If the EJB 2.1 client view is used, the container must throw a java.rmi.RemoteException if the second client is a remote client, or a jakarta.ejb.EJBException if the second client is a local client" Ou seja pode receber jakarta.ejb.ConcurrentAccessException. Ou no caso de EJB 2.1 estar sendo utilizado pode receber java.rmi.RemoteException se for um cliente externo ou jakarta.ejb.EJBException se for local.

Sem padrão

Note que o atributo value não tem um valor padrão. Isso foi intencional, tendo a intenção de informar que se o @AccessTimeout não for explicitamente usado, o comportamento sera o do server vendor.

Alguns servidores vão esperar por um tempo pre determinado e lançar a exceção jakarta.ejb.ConcurrentAccessException, outros podem lançar de imediato.

Exemplo

Aqui nos temos um simples @Singleton bean que possui tres métodos síncronos e um método anotado com @Asynchronous. O componente esta anotado com @Lock(WRITE) então apenas uma thread pode acessar o @Singleton por vez. Este é o comportamento padrão de um componente @Singleton, então usar a anotação @Lock(WRITE) não é necessário mas é importante para deixar claro que o componente tem um comportamento single-thread.

@Singleton
@Lock(WRITE)
public class BusyBee {

    @Asynchronous
    public Future stayBusy(CountDownLatch ready) {
        ready.countDown();

        try {
            new CountDownLatch(1).await();
        } catch (InterruptedException e) {
            Thread.interrupted();
        }

        return null;
    }

    @AccessTimeout(0)
    public void doItNow() {
        // faz alguma coisa
    }

    @AccessTimeout(value = 5, unit = TimeUnit.SECONDS)
    public void doItSoon() {
        // faz alguma coisa
    }

    @AccessTimeout(-1)
    public void justDoIt() {
        // faz alguma coisa
    }
}

O método @Asynchronous não tem uma relação direta com o @AccessTimeout, mas serve como uma forma simple de travar ("lockar") o bean para realizarmos o teste. Ele nos permite testar o comportamento concorrente do componente.

public class BusyBeeTest extends TestCase {

    public void test() throws Exception {

        final Context context = EJBContainer.createEJBContainer().getContext();

        final CountDownLatch ready = new CountDownLatch(1);

        final BusyBee busyBee = (BusyBee) context.lookup("java:global/access-timeout/BusyBee");

        // Esse método assíncrono nunca termina.
        busyBee.stayBusy(ready);

        // Você ainda esta trabalhando abelhinha?
        ready.await();


        // Beleza, a abelha esta ocupada.


        { // Timeout imediato
            final long start = System.nanoTime();

            try {
                busyBee.doItNow();

                fail("A abelha continua ocupada");
            } catch (Exception e) {
                // A abelha continua muito ocupada como esperado.
            }

            assertEquals(0, seconds(start));
        }

        { // Timeout em 5 segundos
            final long start = System.nanoTime();

            try {
                busyBee.doItSoon();

                fail("A abelha deve estar ocupada");
            } catch (Exception e) {
                // A abelha continua ocupada como esperado.
            }

            assertEquals(5, seconds(start));
        }

        // Esse método vai te fazer esperar para sempre, apenas teste se estiver com bastante tempo :D
        // busyBee.justDoIt();
    }

    private long seconds(long start) {
        return TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start);
    }
}

Executando

mvn clean test

Saida

-------------------------------------------------------
    T E S T S
-------------------------------------------------------
Running org.superbiz.accesstimeout.BusyBeeTest
Apache OpenEJB 4.0.0-beta-1    build: 20111002-04:06
http://tomee.apache.org/
INFO - openejb.home = /Users/dblevins/examples/access-timeout
INFO - openejb.base = /Users/dblevins/examples/access-timeout
INFO - Using 'jakarta.ejb.embeddable.EJBContainer=true'
INFO - Configuring Service(id=Default Security Service, type=SecurityService, provider-id=Default Security Service)
INFO - Configuring Service(id=Default Transaction Manager, type=TransactionManager, provider-id=Default Transaction Manager)
INFO - Found EjbModule in classpath: /Users/dblevins/examples/access-timeout/target/classes
INFO - Beginning load: /Users/dblevins/examples/access-timeout/target/classes
INFO - Configuring enterprise application: /Users/dblevins/examples/access-timeout
INFO - Configuring Service(id=Default Singleton Container, type=Container, provider-id=Default Singleton Container)
INFO - Auto-creating a container for bean BusyBee: Container(type=SINGLETON, id=Default Singleton Container)
INFO - Configuring Service(id=Default Managed Container, type=Container, provider-id=Default Managed Container)
INFO - Auto-creating a container for bean org.superbiz.accesstimeout.BusyBeeTest: Container(type=MANAGED, id=Default Managed Container)
INFO - Enterprise application "/Users/dblevins/examples/access-timeout" loaded.
INFO - Assembling app: /Users/dblevins/examples/access-timeout
INFO - Jndi(name="java:global/access-timeout/BusyBee!org.superbiz.accesstimeout.BusyBee")
INFO - Jndi(name="java:global/access-timeout/BusyBee")
INFO - Jndi(name="java:global/EjbModule748454644/org.superbiz.accesstimeout.BusyBeeTest!org.superbiz.accesstimeout.BusyBeeTest")
INFO - Jndi(name="java:global/EjbModule748454644/org.superbiz.accesstimeout.BusyBeeTest")
INFO - Created Ejb(deployment-id=org.superbiz.accesstimeout.BusyBeeTest, ejb-name=org.superbiz.accesstimeout.BusyBeeTest, container=Default Managed Container)
INFO - Created Ejb(deployment-id=BusyBee, ejb-name=BusyBee, container=Default Singleton Container)
INFO - Started Ejb(deployment-id=org.superbiz.accesstimeout.BusyBeeTest, ejb-name=org.superbiz.accesstimeout.BusyBeeTest, container=Default Managed Container)
INFO - Started Ejb(deployment-id=BusyBee, ejb-name=BusyBee, container=Default Singleton Container)
INFO - Deployed Application(path=/Users/dblevins/examples/access-timeout)
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.071 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0