HOAB

History of a bug

@EJB @Asynchronous ExceptionExecution

Rédigé par gorki Aucun commentaire

Le problème :

La récupération des résultats des EJB asynchrone.

La technique de base pour déclencher un traitement asynchrone :

  1. créer une méthode taggué "@Asynchronous", la méthode retourne un Future<?>
  2. l'appeler
  3. attendre le retour

Souvent, on déclenche X fois la méthode, le tout passe dans une boucle :

  1. Pour toutes les tâches
    1. créer une méthode taggué "@Asynchronous", la méthode retourne un Future<?>
    2. l'appeler
    3. stocker le Future
  2. attendre que tous les Futures soit terminés
  3. récupérer les résultats

On part du principe que la méthode asynchone gère correctement ses erreurs à son niveau (un try/catch Exception avec un retour Future correct).

Seulement, certaines exceptions peuvent être remontées par le container : transaction timeout, erreur lors du commit, etc... sous forme d'ExecutionException. Et là, si le code est mal fait, on peut avoir des surprises et ne pas savoir dire ce qui s'est passé exactement

Solution :

Il faut donc penser lors de la récupération des résultats à :

  1. prévoir un try/catch d'ExecutionException / CancellationException / InterruptedException pour les Future.get()
  2. bien le mettre PAR Future, puisque chaque Future.get() va pouvoir lancer sa propre exception. (Certaines tâches peuvent provoquer une erreur au commit et pas d'autre)
  3. gérer un état global de récupération du résultat (est-ce que le code est correct si seulement une partie des tâches ont réussi ?)
  4. la gestion de traces et d'erreurs associés pour permettre de retrouver les tâches en erreur.

Exemple :

  • ici, le résultat est retourné dans une map
  • sinon le bean lui-même est retourné avec l'exception, il contient des maps pour stocker les erreurs et les futures
  • la gestion de traces et d'erreur est faite par l'appelant, les informations nécessaires pour décrire le contexte métier du Future sont dans un bean "FutureDescription"
// Attributs : 
private Map<Future<R>, FutureDescription> futures = new ConcurrentHashMap<Future<R>, FutureDescription>();
private Map<Future<R>, Exception> futureException = new ConcurrentHashMap<Future<R>, Exception>();

...

public Map<Future<R>, R> extractResultsMap(boolean throwException) throws MultiThreadException {

Map<Future<R>, R> results = new HashMap<Future<R>, R>();

// parcours de chaque Future pour récupérer son résultat d'exécution
for (Future<R> future : futures.keySet()) {
   try {
         results.put(future, future.get());
   } catch (InterruptedException | ExecutionException e) {
         futureException.put(future, e);
   }
}
if (throwException && (futureException.size() > 0)) {
    throw new MultiThreadException(ErrorCode.TECHNICAL_GLOBAL, "Erreur pendant la recuperation des resultats", this);
}
return results;

}

 

 

Couplés avec :

  • une classe qui fait du monitor pour tracer l'avancement des threads
  • une classe qui stocke les Future, leur résultats, leurs exceptions
  • des méthodes utilitaires

Cela donne les outils nécessaire pour gérer facilement l'asynchronisme.

Hibernate Envers - Audit de table - Relation 1-N

Rédigé par gorki Aucun commentaire

Le problème :

Envers est un module core d'Hibernate qui permet d'auditer des entités, i.e, historiser toutes les modifications de l'objet.

Le principe est de stocker dans une table quasiment identique les différentes versions de l'objet.

Exemple :

Table source : UTILISATEUR(id, version, nom, adresse)

Table audit : UTILISATEUR_AUD(id, rev, nom, adresse)

Le champ version de la table source est utilisé pour l'optimist locking, on n'a donc pas besoin de ce champ dans la tableau auditée.

Par contre la table auditée a un champ un peu équivalent : "révision" qui permet de stocker les différentes versions de l'objet.

Attention cette révision est différente de la version de l'objet, en effet cela correspond plus à un numéro de commit global : tous les objets modifiés et audités dans une même transaction auront la même révision (un peu comme un commit SVN).

Coté code source :

@Audited
@Entity
public class UTILISATEUR {

    @Id
    private Long id;

    @Version
    private Long version;

    private String nom;

    private String adresse;
}

Facile comme tout (il manque des trucs bien sur : création de la table d'audit, des séquences qui vont bien, des informations supplémentaires trackées avec l'audit, etc...)

A chaque sauvegarde d'une instance UTILISATEUR, on aura une ligne dans UTILISATEUR_AUD.

Bon, mais que se passe-t-il si au lieu d'avoir 1 adresse, UTILISATEUR a N adresses ?

@Audited
@Entity
public class UTILISATEUR {

    @Id
    private Long id;

    @Version
    private Long version;

    private String nom;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "UTILISATEUR_ID")
    private List<String> adresses;
}

Solution :

Déjà lire la documentation.

Ensuite choisir : est-ce que les adresses doivent être auditée ? Si oui, on continue.

Evidemment il va y a voir une deuxième table ADRESSE_AUD.

Mais un des points forts d'envers est d'auditer seulement les deltas : une ADRESSE modifiée ne veut pas dire que UTILISATEUR est aussi modifié.
La conséquence importante est que là où on a une relation 1..N, on passe à une relation N..N. Pourquoi ?

Voici ce qui se passe :

// Sauvegarde d'un utilisateur avec une adresse, dans les tables d'audit on obtient

U1 (rev1) -> A1 (rev1)

// on modifie l'adresse (toujours du 1..N)

U1 (rev1) -> A1 (rev1)

U1 (rev1) -> A1 (rev2)

// on modifie l'utilisateur (la relation devient N..N ! deux utilisateurs pointent vers la même adresse)

U1 (rev1) -> A1 (rev1)

U1 (rev1) -> A1 (rev2)

U1 (rev3) -> A1 (rev2)

P.S : Notez bien que les objets modifiés dans une même transaction ont la même révision

Les conséquences directes sont :

- une table d'association est nécessaire entre UTILISATEUR_AUD et ADRESSE_AUD (alors que ce n'est pas le cas dans le modèle normal).

- en 1..N, le lien est porté par la table fille, on a donc dans ADRESSE une colonne qui référence l'utilisateur (UTILISATEUR_ID) ; dans la table d'audit, c'est la table d'association qui porte ce lien.

@Audited
@Entity
public class UTILISATEUR {

    @Id
    private Long id;

    @Version
    private Long version;

    private String nom;

    private List<String> adresses;
}

 

@Audited
@Entity
public class ADRESSE {

    @Id
    private Long id;

    @Version
    private Long version;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "UTILISATEUR_ID")
    @AuditJoinTable(name = "UTILISATEUR_ADRESSE_AUD", inverseJoinColumns = {
        @JoinColumn(name = "ADRESSE_ID")
    })
    private List<String> adresses
}

Je mettrais bien le DDL, mais j'ai la flemme :)

Trucs à savoir

  • évidemment Envers utilise beaucoup de ressource, attention aux performances
  • en plus des tables d'audit, il y a une table pour référencer toutes les révisions, cette table est customisable pour y ajouter des informations (utilisateur, objets, etc...)
  • il est possible d'utiliser des EJB dans les EntityTrackingRevisionListener, en faisant un lookup JNDI.(entre autre pour retrouver un EJB qui possède une variable @RequestScoped). Si j'ai le temps, je ferai une description rapide du truc.
  • Best practice : il y a des fonctions pour recharger les révisions, mais ça ne marche bien que si l'objet "chapeau" est modifié à chaque révision. Sinon c'est compliqué de retrouver l'historique à partir de l'objet chapeau. (Il suffit par exemple de positionner une date de modification) => ça n'aide pas les performances.

 

@EJB @Singleton et ConcurrencyManagement

Rédigé par gorki Aucun commentaire

Le problème :

Un EJB singleton qui sert de cache remonte une erreur avec JBoss 7.1.1 :

EJB Invocation failed on component XXXX for method public java.lang.Object:
javax.ejb.EJBTransactionRolledbackException: JBAS014373: EJB 3.1 PFD2 4.8.5.5.1
concurrent access timeout on org.jboss.invocation.InterceptorContext$Invocation@398e7b17 
- could not obtain lock within 5000MILLISECONDS

Solution :

Tout est déjà écrit bien sur, encore aurait-il fallu le lire :)
Par défaut les EJB @Singleton sont

  1. @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)

  2. protégés par un @Lock(LockType.WRITE)

donc les méthodes sont synchronisés par le container. On peut

  • soit passer la méthode ou la classe en LockType.READ,
  • soit passer en Bean-managed (@ConcurrencyManagement(ConcurrencyManagementType.BEAN)). Là c'est vous qui gérer le tout.

Pour reproduire systématiquement, il suffit de mettre un sleep de 6s dans la méthode problématique.

Note:

  • Si le pool des ejb n'est pas assez grand on a un permit timeout et non un concurrent ascess timeout.
  • Le timeout sous JBoss est configuré dans cette section très intéressante :
<subsystem xmlns="urn:jboss:domain:ejb3:1.2">
            <session-bean>
                <stateless>
                    <bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
                </stateless>
                <stateful default-access-timeout="5000" cache-ref="simple"/>
                <singleton default-access-timeout="5000"/>
            </session-bean>
            <pools>
                <bean-instance-pools>
                    <strict-max-pool name="slsb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
                    <strict-max-pool name="mdb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
                </bean-instance-pools>
            </pools>
            <caches>
                <cache name="simple" aliases="NoPassivationCache"/>
                <cache name="passivating" passivation-store-ref="file" aliases="SimpleStatefulCache"/>
            </caches>
            <passivation-stores>
                <file-passivation-store name="file"/>
            </passivation-stores>
            <async thread-pool-name="default"/>
            <timer-service thread-pool-name="default">
                <data-store path="timer-service-data" relative-to="jboss.server.data.dir"/>
            </timer-service>
            <remote connector-ref="remoting-connector" thread-pool-name="default"/>
            <thread-pools>
                <thread-pool name="default">
                    <max-threads count="10"/>
                    <keepalive-time time="100" unit="milliseconds"/>
                </thread-pool>
            </thread-pools>
        </subsystem>

 

 

SimpleDateFormat - erreur/incohérence de parse - weekyear

Rédigé par gorki Aucun commentaire

Le problème :

SimpleDateFormat ne me retourne pas la date correcte :

        try {

            SimpleDateFormat sdf = new SimpleDateFormat("dd/MMM/YYYY:HH:mm:ss", Locale.US);
            Date date = new Date();
            String formatted = sdf.format(date);
            System.out.println(formatted); // 19/Jun/2013:13:31:16
            System.out.println(sdf.parse(formatted)); // Sun Dec 30 13:32:15 CET 2012
        } catch (ParseException e) {
            e.printStackTrace();
        }

Pas cohérent !

Solution :

Ce petit test est un bon moyen de vérifier votre formatter SimpleDateFormat.

1) vérifier la locale. C'est courant d'avoir une date à parser "01/Jun/2013" et de ne pas préciser la locale. Du coup SimpleDateFormat prend votre locale par défaut, souvent FR, et esssaiera de trouver le mot Juin, mais ça vous le verrez assez : erreur de parse, etc....

2)... et ça, la bêtise : il ne faut pas utiliser YYYY mais yyyy. cf documentation (nouveau depuis Java 7)

Au moins je suis pas tout seul : ici, surtout là

 

Donc :

SimpleDateFormat sdf = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss", Locale.US);

 

Oracle XE 11G2 et Windows 7 64 bits

Rédigé par gorki Aucun commentaire

Le problème :

C'est galère d'installer Oracle XE sur windows 7 64 bits.

Parfois ça marche, parfois ça marche pas.

Solution 1 (non testée) :

Installer Oracle complet, disponible pour utilisation personnelle en version 64 bits.

Solution 2 :

Une erreur se produit à l'installation (fichier introuvable : KEY_XE.reg). D'après ces sites : ici et , il faut, pendant l'installation, copier le fichier en question dans le répertoire temporaire de l'installation. Le 2ème site propose un petit .bat (key_oracle.bat) pour l'installer dans tous les répertoires temporaires, (l'installation silencieuse n'a pas marché chez moi, mais c'est basé sur des délais, donc variable suivant les environnements), à exécuter après avoir fait tous les choix, au début de l'installation proprement dite.

Plusieurs problèmes peuvent se poser après l'installation :

  • pas de base créée (répertoire C:\oraclexe\app\oracle\oradata\XE vide).

    • là c'est mort
    • en général c'est le problème du KEY_XE.reg, mais je ne suis pas sur
    • peut-être que "sys" comme mot de passe pose problème (?!?)
  • le TNS XE n'est plus reconnu (ORA-12514:No TNS Listener)

    • a priori cela se produit quand on arrête les services OracleServiceXE ou OracleXETNSListener et qu'on les redémarre,
    • ajouter la description du SID XE dans le fichier <ORACLE_HOME>/network/admin/listener.ora
    (SID_DESC =
      (SID_NAME = XE)
      (ORACLE_HOME = c:\oraclexe\app\oracle\product\11.2.0\server)
    )
  • sur expdp :ORA-12638.

    • solution ici
    • modifier le fichier <oracle home>/network/admin/sqlnet.ora
SQLNET.AUTHENTICATION_SERVICES= (NTS)

en

SQLNET.AUTHENTICATION_SERVICES= (NONE)
  • le port 8080 est utilisé

select dbms_xdb.gethttpport as "HTTP-Port"
            , dbms_xdb.getftpport as "FTP-Port" from dual;
begin
     dbms_xdb.sethttpport('80');
     dbms_xdb.setftpport('2100');
end;
/

Trucs à savoir

  • le service HTTP est démarré par le service OracleXE
  • se logguer en "/ as sysdba" est compliqué (il faut que votre user soit dans un groupe ActiveDirectory précis) : à oublier
  • ne pas oublier le mot de passe SYSTEM / SYS indiqué lors de l'installation c'est la clé de tout, et comme on ne peut pas utiliser le sysdba c'est impossible à changer
  • seul deux services sont nécessaires pour la base : OracleServiceXE ou OracleXETNSListener. Et pas besoin de les mettre en démarrage automatique
Fil RSS des articles