EclipseLink JPA2 Cache Coordination (Glassfish 3.1.2.2, ActiveMQ 5.8)

Posted by | · · · · · · · · | Szoftverfejlesztés | 4 hozzászólás EclipseLink JPA2 Cache Coordination (Glassfish 3.1.2.2, ActiveMQ 5.8) című bejegyzéshez

Vajon milyen problémák merülhetnek fel abból, hogy több webalkalmazás ugyanazt az adatbázist használja egyszerre?

Amit nagyon hamar tapasztaltunk, hogy az egyik alkalmazásban indukált adatbázis tartalom változásról a másik alkalmazás nem látott semmit. Ezen blog-omban erről szeretnék beszélni illetve egy megoldást a problémára bemutatni.

Áttekintés

Az EclipseLink támogat egy shared (L2) objektum cache-t. Alapértelmezetten ez a cache engedélyezett. Amivel normál esetben nincs semmi gond. Akkor lép fel probléma, ha másik alkalmazás, vagy egy cluster-ezett környezettben ugyanazon alkalmazás más szerveren, közvetlenül módosítja az adatbázis tartalmat.

Épp egy olyan projekten dolgozunk ahol pontosan a fentebb említett probléma merült fel – több webalkalmazás ugyanazon adatbázist használja, s az L2 cache-nek köszönhetően az egyik alkalmazáson indukált változásokról a másik alkalmazás nem értesül – s engem ért az a megtiszteltetés, hogy kikutassam ezen problémára a megoldást.

Az EclipseLink biztosít egy cache coordination nevű feature-t, amely lehetővé teszi, hogy egy sor EclipseLink session szinkronizálja változásait egy megosztott hálózaton, úgymint egy alkalmazás server clusteren. Az EclipseLink cache coorination-t RMI-n és JMS-en keresztül támogat.

Jelen blog-ban azt szeretném bemutatni, hogy Glassfish 3.1.2.2. alkalmazás szerverrel és ActiveMQ 5.8-as JMS szerverrel, hogyan lehet megvalósítani az EclipseLink L2 cache szinkronizációt.

Cache Coordination JMS-el

A problémát a következőképp kívántuk megoldani:

  • Egy JMS szerverre beregisztrálunk egy topic-ot.
  • Ezt a topic-ot az alkalmazás szerverünkön bekonfiguráljuk.
  • Végül ezen topic-hoz kötjük be az EclipseLink cache coordination szolgáltatását.

JPA2CacheCoordination

JPA Cache szinkronizáció JMS szerver segítségével

A webalkalmazások egy glassfish 3.1.2.2. alkalmazás szerveren futnak. JMS szervernek az ActiveMQ-t választottuk.

ActiveMQ 5.8 telepítése és inicializálás

Az ActiveMQ telepítése és indítása elég egyszerű. Az ActiveMQ weboldal Getting Started-je elég jó (bár a 4.x-es verziókhoz készült).

Miután sikeresen telepítettük az ActiveMQ-t (ami linux-on egy tar kicsomagolásából állt) a következőképp indíthatjuk el az active mq broker-t:

  • Lépjünk az éppen telepített ActiveMQ könyvtárunkba.
  • Ebből a könyvtárból adjuk ki a bin/activemq utasítást (ez némi inicializálást végez), ami azonban még nem indítja el a broker-t (a Getting Started-ban pedig ez áll). Majd ezt követően bin/activemq start parancs kiadásával indítható a broker.
  • ActiveMQ alapértelmezett portja a 61616 (netstart segítségével leellenőrzőjük, hogy tényleg elindult-e).
  • http://localhost:8161/admin-on pedig elérjük az adminisztrációs felületet (admin/admin).

A JMS szerverünk készen van a használatra, jöhet az alkalmazás szerverünk konfigurációja.

Glassfish 3.1.2.2 konfigurációja

Az alábbiakban leírom, hogyan készítjük fel a Glassifish-ünket arra, hogy az ActiveMQ-val kapcsolatot teremtsen.

Általánosságban véve szükségünk van egy queue-ra vagy egy topic-ra, amely csatlakozik a külső JMS szerverhez. Mivel most azt szeretnénk elérni, hogy egy sor EclipseLink session legyen szinkronban, topic-ot fogunk használni. Ezt leszámítva a konfiguráció, melyet bemutatni készülök általános.

Kapcsolat teremtés

Használjuk a Glassfish Admin Console-ját (http://localhost:4848/) a konfigurációhoz.

  1. Az ActiveMQ lib illetve lib/optional könyvtárból másoljuk a Glassfish lib könyvtárába az alábbi jar-okat:
    • slf4j-api-1.6.6.jar
    • slf4j-log4j12-1.6.6.jar
    • log4j-1.2.17.jar.
  2. Töltsük le a megfelelő activemq-rar-t innen (jelenleg az activemq-rar-5.8.0.rar-t) és deploy-oljuk.
  3. Hozzunk létre egy új Resource Adapter Config-ot (Resources->Resource Adapter Configs) az alábbi paraméterekkel:
    • Resource Adapter Name: activemq-rar-5.8.0
    • Thread Pool ID: thread-pool-1
    • A property-ket hagyjuk alapértelmezetten (ServerUrl: tcp://localhost:61616, UserName: admin, Password: admin, UseInboundSession: false)
  4. Hozzunk létre egy új Connector Connection Pool-t (Resources->Connectors->Connector Connection Pools) az alábbi paraméterekkel:
    • Pool name: amqpool
    • Resource Adapter: activemq-rar-5.8.0
    • Connection Definition: javax.jms.TopicConnectionFactory
  5. Hozzunk létre egy új Connector Resource-t (Resources->Connectors->Connector Resources) az alábbi paraméterekkel:
    • JNDI Name: amqres
    • Pool Name: amqpoolí
    • Transaction Support: NoTransation
  6. Hozzunk létre egy új Admin Object Resource-t (Resources->Connectors->Admin Object Resources). Az egyik forrásban, amit felhasználtam, a szerző felhívta a figyelmet arra, hogy ezt ne az admin console-ból tegyük meg, mert az Admin Object Resource felvétele bug-os, hanem parancssorból. (A tapasztalata az volt, hogy miután mindennel elkészült és szerette volna használni a létrehozott kapcsolatot a következő kivételt kapta kódjának futtatásakor: javax.naming.NameNotFoundException: <admin resource object neve> not found)
    Az Admin Object Resource paraméterei:
    • JNDI Name: amqtopic
    • Resource Adapter: activemq-rar-5.8.0
    • Resource Type: javax.jms.Topic
    • Class Name: org.apache.activemq.command.ActiveMQTopic
    • Egy property: PhysicalName: amqtopic
    • Parancssori utasítás:
      create-admin-object –restype javax.jms.Topic –raname activemq-rar-5.8.0 –property PhysicalName=amqtopic amqtopic
  7. Ezennel a Glassfish konfigurációja kész is.

Teszteljük le, hogy a kapcsolat tényleg létrejött-e. A létrehozott amqpool-on nyomjunk rá a Ping button-ra. Emellett megnézhetjük az ActiveMQ logját is (<ActiveMQ_INSTALL_DIR>/data/activemq.log). A ping-elést követően az ActiveMQ log fájljának utolsó bejegyzése a következőképp néz ki:

2013-09-02 18:08:43,377 | WARN  | Transport Connection to: tcp://127.0.0.1:41098 failed: java.io.EOFException | org.apache.activemq.broker.TransportConnection.Transport | ActiveMQ Transport: tcp:///127.0.0.1:41098@61616

De nem kell aggódni, minden rendben van, ahogy ezen issue is mutatja.

Miután sikeresen felkonfiguráltuk a glassfish-ünket már csak az L2 cache szinkronizáció bekapcsolása van hátra.

EclipseLink Cache Coordination bekapcsolása

A rossz megoldás

Az EclipseLink dokumentációja alapján már csak annyi a dolgunk, hogy felkonfiguráljuk a cache coordination-t. Ehhez nem kell mást tennünk, mint az EclipseLink-nek meg kell adnunk milyen protocolt szeretnénk használni a cache coordination-hoz, mi esetünkben ez jms lesz, továbbá meg kell adni, hogy a szinkronizáció milyen topic-ot és connection factory-t használjon, melyek a korábban létrehozott amqtopic és amqres. Ezeket a tulajdonságokat a persistence.xml-be kell felvennünk:

<property name=“eclipselink.cache.coordination.protocol” value=“jms” />
<property name=“eclipselink.cache.coordination.jms.topic” value=“amqtopic” />
<property name=“eclipselink.cache.coordination.jms.factory” value=“amqres” />

Azonban ez így nem működik. Az alkalmazás elindul, de amikor eljön az idő, hogy az L2 cache-t szinkronizálni kell sajnálatos módon az alábbi kivételt kaptam:

Exception [EclipseLink-22114] (Eclipse Persistence Services - 2.3.2.v20111125-r10461): org.eclipse.persistence.exceptions.RemoteCommandManagerException
Exception Description: Local JMSTopicRemoteConnection[Service[EclipseLinkCommandChannel, 96086cac-9ace-4d93-8f9b-0236d3005c78, null], topic amqtopic]: failed to deserialize retrieved message ID:thomas-wcs-55534-1377687702515-61:1:1:1:1.
Internal Exception: java.lang.IllegalStateException: WEB9031: WebappClassLoader unable to load resource [org.eclipse.persistence.sessions.coordination.MergeChangeSetCommand], because it has not yet been started, or was already stopped
    at org.eclipse.persistence.exceptions.RemoteCommandManagerException.errorDeserializeRemoteCommand(RemoteCommandManagerException.java:200)
    at org.eclipse.persistence.internal.sessions.coordination.broadcast.BroadcastRemoteConnection.failDeserializeMessage(BroadcastRemoteConnection.java:211)
    at org.eclipse.persistence.internal.sessions.coordination.jms.JMSTopicRemoteConnection.onMessage(JMSTopicRemoteConnection.java:224)
    at org.eclipse.persistence.internal.sessions.coordination.jms.JMSTopicRemoteConnection$JMSOnMessageHelper.run(JMSTopicRemoteConnection.java:491)
    at java.lang.Thread.run(Thread.java:722)
Caused by: java.lang.IllegalStateException: WEB9031: WebappClassLoader unable to load resource [org.eclipse.persistence.sessions.coordination.MergeChangeSetCommand], because it has not yet been started, or was already stopped
<strong>10</strong>    at org.glassfish.web.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1401)
    at org.glassfish.web.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1359)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:266)
    at org.apache.activemq.util.ClassLoadingAwareObjectInputStream.load(ClassLoadingAwareObjectInputStream.java:77)
    at org.apache.activemq.util.ClassLoadingAwareObjectInputStream.resolveClass(ClassLoadingAwareObjectInputStream.java:46)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1610)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1515)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1769)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1348)
<strong>20</strong>    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
    at org.apache.activemq.command.ActiveMQObjectMessage.getObject(ActiveMQObjectMessage.java:185)
    at org.eclipse.persistence.internal.sessions.coordination.jms.JMSTopicRemoteConnection.onMessage(JMSTopicRemoteConnection.java:215)
    ... 2 more

Érdekesség, hogy a kivétel akkor érkezik, amikor megtörtént a változás fogadása, azaz működik minden, csak az üzenet feldolgozása nem sikerült. Ennek oka ebben a sorban látható:

WEB9031: WebappClassLoader unable to load resource [org.eclipse.persistence.sessions.coordination.MergeChangeSetCommand], because it has not yet been started, or was already stopped

Az EclipseLink forráskód idevágó részének felületes átvizsgálása utáni feltételezésünk a következő:
A mélyben az történik, hogy az EclipseLink elindít egy új szálat ami pollozgatja a topicot. Az nem újdonság, hogy java ee containeren belül nem illik szálat manuálisan indítani. Így elveszthetjük a java ee környezetet, vagy annak egy részét. A pontos kimenetel alkalmazásszerver függő, TomEE-n például simán működött.
Valósznűleg itt az történt, hogy mikor a pollozgató programszál megkapta az üzenetet, már nem érte el az alkalmazás-t, vagy annak classloader-ét. Ezért mondhatja a hibaüzenet, hogy nem fut, vagy le van állítva.
Az is kiderül némi hézagos dokumentációból, és a forrsákódból, hogy van más pollozási mód. Saját message driven bean-el is el lehet kapni az üzenetet, ekkor nem fog külön pollozgató szál indulni, viszont a mi felelősségünk továbbítani az üzenetet az eclipselink felé.

A jó megoldás

Hozzunk létre egy saját MessageDrivenBean-t (MDB), mely onMessage() metódusa ráhív a JMSTopicRemoteConnection.onMessage() metódusra.

import javax.annotation.PostConstruct;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.eclipse.persistence.internal.sessions.coordination.jms.JMSTopicRemoteConnection;
import org.eclipse.persistence.sessions.coordination.CommandManager;
import org.eclipse.persistence.sessions.coordination.RemoteCommandManager;
import org.eclipse.persistence.sessions.server.ServerSession;

@MessageDriven(activationConfig = {
 @ActivationConfigProperty(propertyName = "destination", propertyValue = "amqtopic"),
 @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),
 @ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "NonDurable"),
 @ActivationConfigProperty(propertyName = "clientId", propertyValue = ""),})
public class JMSCacheCoordinationMDB implements MessageListener {

private JMSTopicRemoteConnection connection;
 @PersistenceContext
 private EntityManager em;

 @PostConstruct
 public void init() {
   CommandManager commandManager = em.unwrap(ServerSession.class).getCommandManager();
   connection = new JMSTopicRemoteConnection((RemoteCommandManager) commandManager);
 }

 @Override
 public void onMessage(Message message) {
   connection.onMessage(message);
 }

}

Majd szükségünk van egy glassfish-ejb-jar.xml (WEB-INF/) fájlra a következő tartalommal:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-ejb-jar PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 EJB 3.1//EN" "http://glassfish.org/dtds/glassfish-ejb-jar_3_1-1.dtd">
<glassfish-ejb-jar>
<enterprise-beans>
     <ejb>
       <ejb-name>JMSCacheCoordinationMDB</ejb-name>
       <mdb-connection-factory>
         <jndi-name>amqres</jndi-name>
       </mdb-connection-factory>
       <mdb-resource-adapter>
         <resource-adapter-mid>activemq-rar-5.8.0</resource-adapter-mid>
       </mdb-resource-adapter>
     </ejb>
   </enterprise-beans>
</glassfish-ejb-jar>

És végül az EclipseLink Cache Coordinatin protocol tulajdonsága: jms-publishing. Így a persistance.xml cache coordination tulajdonságai a következőképp festenek:

<property name=“eclipselink.cache.coordination.protocol” value=“jms-publishing” />
<property name=“eclipselink.cache.coordination.jms.topic” value=“amqtopic” />
<property name=“eclipselink.cache.coordination.jms.factory” value=“amqres” />

Példa alkalmazás futtatása

Innen le tudod tölteni a példaalkalmazást.

Amire szükséged lesz

  • MySQL adatbázis test nevű shema-val melyben van egy data nevű tábla id (int), text (varchar(2000)) mezőkkel, amiben van egy rekord 1-es id-val.
  • Glassfish 3.1.2.2
  • ActiveMQ 5.8
  • Netbeans

Hogyan tudod kipróbálni

Készítettem egy servlet-et (/dataaccess) mellyel a data tábla 1-es id-jű rekordját tudjuk módosítani és lekérni.

Példa:

  • <context-root>/dataaccess?operation=get: Ki tudod íratni az 1-es id-jű data rekord text mezőjét.
  • <context-root>/dataaccess?operation=update&text=someText: Update-elni tudod az 1-es id-jű data rekord text mezőjét.
  • [/list_styes]

    Összefoglalás

    Az L2 cache szinkronizálása webalkalmazások közt, glassfish alkalmazás szerverekkel, némi nehézséget okozott. Nézzük a buktatókat.

    Elsőnek ott volt a glassfish konfigurációja során  az admin object resource létrehozása az admin console-ból nem a jó választás, parancssorból érdemes létrehozni. Megnéztem, hogy vajon az admin console-ból illetve parancssorból létrehozott admin object resource a domain.xml-ben miben különbözik egymástól.

    Parancssori utasítással létrehozott admin object resource:

    <admin-object-resource
     res-adapter="activemq-rar-5.8.0"
     res-type="javax.jms.Topic"
     jndi-name="amqtopic"
     class-name="org.apache.activemq.command.ActiveMQTopic">
     <property name="PhysicalName" value="amqtopic"></property>
    </admin-object-resource>
    

    Admin console-ból létrehozott admin object resource:

    <admin-object-resource
     enabled="false"
     res-adapter="activemq-rar-5.8.0"
     res-type="javax.jms.Topic"
     description=""
     jndi-name="amqtopic"
     class-name="org.apache.activemq.command.ActiveMQTopic">
     <property name="PhysicalName" value="amqtopic"></property>
    </admin-object-resource>
    

    Az enabled=”false” elég gyanúsnak néz ki. Az érdekes, hogy az admin console-ből ez nem látszik, illetve kipróbáltam, ha állítgatom ezt a property-t a felületről, akkor az vajon tükröződik-e a domain.xml-ben és sajnálatosan az volt az eredmény, hogy nem. Tehát érdemes parancssorból létrehozni és még meg is nézni a domain.xml-ben, hogy minden rendben van-e.

    A második buktató az EclipseLink cache coordination konfigurációját követő WEB9031: WebappClassLoader unable to load resource kivétel volt. Ennek okára csak úgy sikerült rájönnünk, hogy magának az EclipseLink forráskódját kezdtük tanulmányozni, amely ahhoz a felfedezéshez vezetett, hogy a glassfish érzékeny lelkivilágát megbolygató dolgot műveltek, s amelyet csak egy megkerülő úttal lehet orvosolni. Készítenünk kell egy MessageDrivenBean-t, amelyből nekünk kell ráhívni az EclipseLink egy MessagesDrivenBean onMessage() metódusára.

    A harmadik, s egyben utolsó buktató, hogy a cache coordination protocol-ja MessageDrivenBean használatakor nem jms, hanem jms-publishing, amely persze az EclipseLink dokumentációjában nem szerepelt.

    Észrevételeket legnagyobb örömmel fogadom és remélem hasznos volt számodra ezen kis tutorial.

    Referenciák

    EclipseLink JPA Cache Coordination

    EclipseLink JMS Cache Coordination Using MDB

    EclipseLink About Cache Coordination

    Blog: EclipseLink JPA2 Distributed Cache Coordination

    Blog: JMS with Glassfish v3 & ActiveMQ 5.7

    ActiveMQ


    4 hozzászólás

    Viczián István says:

    2013. szeptember 24. at 14:06

    Nagyon jó írás!
    Az alkalmazásokkal kapcsolatban nem világos minden. Ez két teljesen különböző alkalmazás? De gondolom akkor a perzisztens rétegnek ugyanannak kell lennie, ugyanazokkal az entitásokkal.
    Ha meg ugyanaz az alkalmazás, akkor viszont egy sima clusterbe kötés nem segít?

    Reply

    Trinn Tamás says:

    2013. szeptember 25. at 09:28

    Örülök, hogy érdeklődést keltett a téma és van hozzá kérdés is.

    A példaként feltett alkalmazásról, illetve, hogy azzal hogy tesztelhetjük a cache coordination-t nem igen részleteztem, de akkor most bepótolom. Ez tényleg egy alkalmazás. De ezt több példányban deploy-olhatjuk egy glassfish domain-ra, vagy akár többre is. Így elérjük azt, hogy több alkalmazás használja egyszerre ugyanazt az adatbázis.
    Egy valós projektben könnyen előfordulhat (sőt inkebb ez a helyzet), hogy az egyes alkalmazásokban külön-külön nem található meg az összes entitás, de ez nem okoz gondot az eclipse link cache coordination-nak. Tegyük fel, hogy van két alkalmazásunk I. és II., s az entitásokat három halmazba sorolnánk A, B és C, ahol A az olyan entitások halmaza, amely csak az I. alkalmazásban található meg, B az olyan entitások halmaza, amely csak a II. alkalmazásban található meg s C az olyan entitások halmaza mely mind I.-ban és II.-ban is megtalálható. Így ha I.ben a C entitás halmazban levő valamely entitást változás érinti, akkor a II.-hez eljut ezen változás, s mivel II-ben is megtalálható ezen entitás, így bekövetkezik a II. cache frissítése. Továbbá ha I.ben az A-ban levő valamely entitást érinti változás, akkor bár erről a változásról értesül II., de mivel nála nincs ilyen entitás egyszerűen nem foglalkozik vele.

    Egyszerű clusterbe kötés alatt esetleg arra gondoltál, hogy glassfish-ben hoznál létre egy cluster-t? Ha igen, akkor bár nem csináltam még ilyent, de annyi ismeretre szert tettem, hogy ez esetben is fel kellene konfigurálni a cache szinkronizálást. Tehát nem lehet megkerülni a problémát ilyen egyszerűen.

    Remélem sikerült tisztáznom a felmerülő kérdéseket. Bármi egyéb észrevételt, kérdést örömmel fogadok.

    Reply

    Viczián István says:

    2013. szeptember 27. at 13:28

    Köszi a választ! Értem.
    Igen, kíváncsi lennék, hogy mennyire bonyolult Glassfish clusterben a cache szinkronizálás felkonfigurálása. Na majd egyszer kipróbálom. 🙂

    Reply

    Anon says:

    2013. október 2. at 11:45

    Hello!
    Nagyon hasznos iras. Koszonom! Nalunk csak fapados glassfish futott, tobbszoros deploy nelkul, igy a kerdes nem is merult fel. De nagyon jo, hogy leirtad ezeket a tapasztalatokat!

    Reply

    Leave a comment