Skip to content

Se préparer à l’arrivée de Java 9 avec docker et maven

Comme vous le savez peut-être, Java 9 se rapproche (feature complete annoncé pour le 10 décembre 2015 et general availability le 22 septembre 2016, plus d’infos sur la page du projet jdk9).

docker-logo

Il est donc temps de commencer à s’y intéresser, mais peut-être préférez-vous ne pas « polluer » votre environnement avec cette installation, alors pourquoi ne pas utiliser docker ?

Dans ce post, nous allons préparer un conteneur docker fournissant :

  • la version en cours du jdk 9
  • et maven 3.3.3

Création d’un conteneur docker pour le JDK 9

Autant ne pas réinventer la roue, je suis parti d’un Dockerfile défini dans l’article suivant :Build OpenJDK 9 using Docker

J’ai un peu refactoré l’ensemble pour regrouper les installations de package et défini un utilisateur dédié au développement pour ne pas travailler en root.

Voici le résultat :

FROM phusion/baseimage:latest

# Pre-install
RUN \
apt-get update && \
apt-get install -y \
libxt-dev zip pkg-config libX11-dev libxext-dev \
libxrender-dev libxtst-dev libasound2-dev libcups2-dev libfreetype6-dev \
mercurial ca-certificates-java build-essential wget && \
rm -rf /var/lib/apt/lists/*

# User
RUN export uid=1000 gid=1000 && \
mkdir -p /home/javadev && \
echo « javadev:x:${uid}:${gid}:JavaDev,,,:/home/javadev:/bin/bash » >> /etc/passwd && \
echo « javadev:x:${uid}: » >> /etc/group && \
echo « javadev ALL=(ALL) NOPASSWD: ALL » > /etc/sudoers.d/javadev && \
chmod 0440 /etc/sudoers.d/javadev && \
chown ${uid}:${gid} -R /home/javadev

ENV JAVA_HOME=/opt/java-bin
ENV PATH=$JAVA_HOME/bin:$PATH

# We need JDK8 to build
RUN \
wget –no-check-certificate –header « Cookie: oraclelicense=accept-securebackup-cookie » http://download.oracle.com/otn-pub/java/jdk/8u65-b17/jdk-8u65-linux-x64.tar.gz

RUN \
tar zxvf jdk-8u65-linux-x64.tar.gz -C /opt

# Let’s get JDK9
RUN \
cd /tmp && \
hg clone http://hg.openjdk.java.net/jdk9/jdk9 openjdk9 && \
cd openjdk9 && \
sh ./get_source.sh

RUN \
cd /tmp/openjdk9 && \
bash ./configure –with-cacerts-file=/etc/ssl/certs/java/cacerts –with-boot-jdk=/opt/jdk1.8.0_65

RUN \
cd /tmp/openjdk9 && \
make clean images

RUN \
cd /tmp/openjdk9 && \
cp -a build/linux-x86_64-normal-server-release/images/jdk \
/opt/openjdk9

RUN \
cd /tmp/openjdk9 && \
find /opt/openjdk9 -type f -exec chmod a+r {} + && \
find /opt/openjdk9 -type d -exec chmod a+rx {} +

ENV PATH /opt/openjdk9/bin:$PATH
ENV JAVA_HOME /opt/openjdk9

# Maven
RUN mkdir /apache-maven
RUN curl -fSL http://apache.mirrors.ovh.net/ftp.apache.org/dist/maven/maven-3/3.3.3/binaries/apache-maven-3.3.3-bin.tar.gz -o maven.tar.gz \
&& tar -xvf maven.tar.gz -C apache-maven –strip-components=1 \
&& rm maven.tar.gz*

ENV PATH /opt/openjdk9/bin:/opt/apache-maven/bin:$PATH

USER javadev
WORKDIR /home/javadev
VOLUME /home/javadev

(aussi disponible sur github : docker-jdk9-maven)

La création du conteneur est classique :

> docker build -t gcastel/openjdk9 .

à lancer dans le répertoire du Dockerfile.

Utilisation du conteneur

Tout s’est bien déroulé, pas d’erreurs ?
Il s’agit à l’heure actuelle d’un work in progress, on peut donc toujours avoir des surprises.

Si tout va bien, nous pouvons tester ce conteneur :

> docker run -it gcastel/openjdk9 java -version
openjdk version « 1.9.0-internal »
OpenJDK Runtime Environment (build 1.9.0-internal-_2015_10_20_08_31-b00)
OpenJDK 64-Bit Server VM (build 1.9.0-internal-_2015_10_20_08_31-b00, mixed mode)

Voilà, vous pouvez désormais compiler vos projets avec ce container, par exemple en montant un répertoire de travail local (localdir) comme répertoire home :

> docker run -v localdir:/home/javadev -it gcastel/openjdk9 bash

Cool ! Maintenant, occupons-nous de maven avec un petit projet utilisant une fonctionnalité de java 9 (la nouvelle API Process).

J’ai créé un petit projet de test disponible sur github.

La classe principale sera toute simple :

package fr.gcastel;

public class TestProcessAPI {
  public static void main(String[] args) throws Exception {
        System.out.println("Your pid is " + ProcessHandle.current().getPid());
  }
}

Pas besoin de dépendances particulières, on se contentera de prévenir maven qu’on est en java 9, cf. pom.xml:

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
      <source>1.9</source>
      <target>1.9</target>
    </configuration>
  </plugin>

Allons-y, nous pouvons tester :

> mvn compile

Et si tout est ok :

> cd target/classes
> java fr.gcastel.TestProcessAPI
Your pid is 24

Une dernière chose : si vous souhaitez aider la communauté java, mettez en place un jenkins dédié à la compilation de vos projets avec le JDK 9 dès qu’il sera disponible en release candidate. N’hésitez pas, ça peut permettre d’éviter des problèmes tels que ceux rencontrés par lucene à la sortie de Java 7.

Références :

UPnP, Linux et Freebox : un peu de « hacking » pour trouver les URIs de ressources

Alors, voici mon problème initial :

J’utilise un media center pour accéder au flux TV d’une Freebox Revolution. Cet accès se fait via UPnP. Or, à chaque redémarrage de la Freebox, l’URI de la ressource UPnP change et je dois donc reconfigurer ma source TV.

Le media center utilise Kodi (ex-XBMC) sur un Raspberry Pi (cf. distribution OpenElec). Mon idée est donc de scanner les ressources UPnP au démarrage pour modifier automatiquement le chemin de ma source TV.

Cette URI a le format suivant (en l’occurrence dans le fichier ~/.kodi/userdata/guisettings.xml) :

upnp://abcdefgh-8b30-359b-01e9-d706ce4bf569/0/0/106/

et je cherche à trouver la partie suivante : « 0/0/106/ » qui correspond au chemin de la ressource « Freebox TV ».

Recherche des serveurs

Première étape : trouver les serveurs UPnP du réseau pour accéder à la Freebox.

Pour trouver les serveurs UPnP disponibles sur le réseau, vous pouvez utiliser gssdp-discover (issu de gupnp-tools) pour lister les ressources :

> gssdp-discover --timeout=3

Cette commande retournera une liste de ressources (toutes celles qui auront répondu en moins de 3 secondes), dont celle qui nous intéresse :

resource available
  USN:      uuid:abcdefgh-8b30-359b-01e9-d706ce4bf569::urn:schemas-upnp-org:service:ContentDirectory:1
  Location: http://192.168.0.254:52424/device.xml

Recherche des services

Une fois le serveur trouvé, il nous faut le descriptif du service ContentDirectory.
On utilise donc l’URL trouvée précédemment :

> wget http://192.168.0.254:52424/device.xml
> cat device.xml
...
<service>
<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
<serviceId>urn:upnp-org:serviceId:ContentDirectory</serviceId>
<controlURL>/service/ContentDirectory/control
</controlURL>
<eventSubURL>/service/ContentDirectory/event
</eventSubURL>
<SCPDURL>/service/ContentDirectory/scpd</SCPDURL>
</service>
...

Nous avons donc un ensemble d’URIs, dont une qui nous donnera le descriptif des APIs du service (le SCPD) :

> wget http://192.168.0.254:52424/service/ContentDirectory/scpd
> cat scpd
...

le XML obtenu contient les différentes APIs utilisables, dont celle qui nous intéresse Browse :

<action>
<name>Browse</name>
<argumentList>
  <argument>
    <name>ObjectID</name>
    <direction>in</direction>
    <relatedStateVariable>
       A_ARG_TYPE_ObjectID
    </relatedStateVariable>
  </argument><argument>
    <name>BrowseFlag</name>
    <direction>in</direction>
    <relatedStateVariable>
       A_ARG_TYPE_BrowseFlag
    </relatedStateVariable>
  </argument>
  ...
  <argument>
    <name>TotalMatches</name>
    <direction>out</direction>
    <relatedStateVariable>
       A_ARG_TYPE_Count
    </relatedStateVariable>
  </argument><argument>
    <name>UpdateID</name>
    <direction>out</direction>
    <relatedStateVariable>
       A_ARG_TYPE_UpdateID
    </relatedStateVariable>
  </argument>
</argumentList>
</action>

Nous avons ainsi l’ensemble des informations nécessaires pour adresser le service :

  • L’URL
  • Le chemin du service que nous recherchons
  • Les paramètres à lui fournir
  • Les informations qu’il nous retournera

Requêter le service « Browse »

Voilà, maintenant on peut rassembler tous les morceaux !

Nous allons devoir requêter le service en lui fournissant les bons paramètres. On a déjà de bonnes informations grâce au descriptif XML. On peut en rassembler encore plus en effectuant un sniffing réseau durant une recherche UPnP. En l’occurrence, un petit tcpdump durant un browsing UPnP est très informatif :

> tcpdump -s 0 -A -vvv host 192.168.0.254 and tcp port 52424

Je ne recopierai pas le détail des informations obtenues, mais juste la requête SOAP que j’ai pu forger :

<s:Envelope 
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>0/0</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>id,dc:title,res,sec:CaptionInfo,
sec:CaptionInfoEx,pv:subtitlefile</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>0</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>

La partie structurante de cette requête est :

<ObjectID>0/0</ObjectID>

qui définit le chemin dans lequel je cherche les ressources UPnP : 0/0.
(j’ai déjà déterminé que le chemin de « Freebox TV » était au format « 0/0/XX », alors autant en profiter)

Un petit curl permettra alors de tester cette requête :

 curl -XPOST "http://192.168.0.254:52424/service/ContentDirectory/control" -d '...'

(où les « … » sont à remplacer par la requête SOAP)

Traitement de la réponse

La réponse aura l’allure suivante :

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:BrowseResponse 
xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<Result>
...
</Result>
<NumberReturned>4</NumberReturned>
<TotalMatches>4</TotalMatches>
<UpdateID>0</UpdateID>
</u:BrowseResponse>
</s:Body>
</s:Envelope>

Les données présentes dans la balise Result constituent la réponse en elle-même. Elle doit être à son tour désérialisée en XML :

<DIDL-Lite 
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" 
xmlns:dc="http://purl.org/dc/elements/1.1/" 
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" 
xmlns:sec="http://www.sec.co.kr/" 
xmlns:dlna="urn:schemas-dlna-org:metadata-1-0/">
<container id="0/0/86" parentID="0/0" restricted="1">
<dc:title>Freebox TV</dc:title>
<upnp:class>object.container.storageFolder</upnp:class>
</container>
<container>...</container>
...
</DIDL-Lite>

YESSS !!! On y est : l’information est là :

  • title: Freebox TV
  • container id : 0/0/86

On doit donc rechercher le titre ‘Freebox TV’ pour trouver l’ID : ‘0/0/86’ qui est le chemin initialement recherché. Win !

Hop, on assemble tout ça !

Maintenant, il faut scripter tout ça pour mettre à jour mes paramètres.

Pour ce genre de job, plusieurs possibilités me sautent à l’esprit.

Pourquoi pas un script shell ? Non, trop fouilli à maintenir et vraiment pas idéal pour traiter du XML.

Un script Groovy ? C’est mon premier réflexe pour ce type de traitement : flexible avec une API riche. Problème : la plateforme. Si on tourne sur Raspberry Pi, il faut que ça ‘poutre’. On ne peut pas se permettre de monter une JVM en mémoire à chaque démarrage.

Dernière possibilité qui me saute à l’esprit : un programme Go. Des performances dignes du C avec une API très riche et bien documentée. Banco !

Le résultat

Je ne vais pas rentrer dans le détail du programme Go, juste quelques éléments de réponse.

Le code du programme est disponible sur github : https://github.com/gcastel/go-snippets/blob/master/upnp/findFbxUpnpPath.go

Le requêtage s’effectue grâce à l’API http.NewRequest :

req, err := http.NewRequest("POST", url, bytes.NewBuffer(soap_data))
req.Header.Set("SOAPACTION", "urn:schemas-upnp-org:service:ContentDirectory:1#Browse")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
      panic(err)
}
defer resp.Body.Close()

body, _ := ioutil.ReadAll(resp.Body)

L’unmarshalling XML nécessite de définir les structures à accéder (avec leurs tags). Exemple :

type Container struct {
        XMLName xml.Name `xml:"container"`
        Title string `xml:"title"`
        Class string `xml:"class"`
        Id string `xml:"id,attr"`
}

Le parsing/unmarshalling fonctionne alors avec l’API xml.Unmarshal :

var d DIDLLite
var id string
xml.Unmarshal([]byte(response), &d)
for _,container := range d.Containers {
   if container.Title == "Freebox TV" {
     id = container.Id
   }
}

Et voilà, on en est venu à bout.
J’espère que quelques uns de ces éléments pourront être utiles 😀

Service ReST et création de ressource POST/PUT

Consommer des services ReST est devenu monnaie courante, mais la création de ressources avec un tel service nécessite de se poser quelques questions. (attention, je ne réinvente pas la roue : je cherche juste à rassembler les informations nécessaires pour se lancer)

Création de ressource : POST ou PUT ?

Première question : je crée une ressource, c’est donc avec un PUT, non ?

Eh bien, oui et non, tout dépend de l’élément responsable de la création de l’identifiant de votre ressource.

Le client fournit l’identifiant de la ressource ? Il s’agit d’un PUT

Dans le cas d’un PUT, l’URI doit spécifier directement l’identifiant de la ressource à créer.
Exemple :

$ curl -XPUT 'http://localhost:8080/restservice/resources/42' -d '{
    "name" : "mabelleresource"
}'

Le serveur fournit l’identifiant ? Il s’agit d’un POST

S’il s’agit d’un POST, c’est à la charge du serveur de donner l’identifiant.

Exemple :

$ curl -XPOST 'http://localhost:8080/restservice/resources' -d '{
    "name" : "mabelleresource"
}'

Mais ? Où est-il cet identifiant ? Eh bien … dans la réponse retournée par le serveur (et notamment par Location: qui donne l’emplacement de la ressource)

HTTP/1.1 201 - Created
Server: Pouet server v.1.02
Location: http://localhost:8080/restservice/resources/42
...
{
    "id" : "42",
    "name" : "mabelleresource"
}

Contenu de la réponse : et si on était un peu exhaustif ?

Dans la réponse à la création d’une ressource, plusieurs champs peuvent être très utiles :

  • Location, comme vu précédemment indique l’URI de la ressource
  • L’ETag de cette version de la ressource, permettant au client de mettre la donnée en cache
  • Le Content-Type et le contenu de la ressource permettant de confirmer l’id de la ressource et d’obtenir sa structure interne

Et il est très simple de fournir ces informations. Par exemple, en JAX-RS :

@Context
UriInfo uriInfo;
...

@PUT
@Path("{id}")
@Consumes("application/json")
public Response addResource(@PathParam("id") String id, Data inputdata) {
  ...
  return Response.created(uriInfo.getRequestUri())
          .tag(Integer.toString(newResource.hashCode()))
          .entity(newResource)
          .type(MediaType.APPLICATION_JSON_TYPE)
          .build();
}

Ok … et si je veux gérer correctement le ETag ?

Alors, il faut gérer la vérification de ce ETag pour déterminer si les données doivent être fournies à nouveau au client (le client fournissant la valeur à tester via le header « If-None-Match »).

Heureusement, l’objet Request prend en charge une partie de la vérification via la méthode evaluatePreconditions.

Si le ResponseBuilder retourné est null, alors on doit retourner une nouvelle resource. Sinon, ce même ResponseBuilder est utilisable directement.

Exemple d’utilisation :

    @GET
    @Path("{id}")
    public Response getApp(@PathParam("id") String id, @Context Request request) {
        Data resource = fetchData(id);
        EntityTag etag = new EntityTag(Integer.toString(resource.hashCode()));
        Response.ResponseBuilder builder = request.evaluatePreconditions(etag);

        if (builder == null) {
            builder = Response.ok(resource);
            builder.tag(etag);
        }
        return builder.build();
    }

Voilà, vous avez une bonne base de départ pour créer correctement vos ressources ReST. Bon code ! :)

Elasticsearch : MasterNotDiscoveredException et discovery.zen.minimum_master_nodes

elasticsearch-logo-icon-lg
On ne présente plus Elasticsearch, ce moteur de recherche open source distribué et surtout très accessible.

Elasticsearch est très simple à mettre en oeuvre et à utiliser, mais la mise en place d’un cluster peut tout de même poser problème, en particulier pour la gestion du « master node ».

Le master node est le noeud du cluster qui est chargé de l’allocation des « shards » parmi les différents noeuds. Il est donc important qu’à un instant T, il y ait un et un seul master node.

————— Les master nodes et le split-brain —————

Imaginons qu’une interruption réseau empêche temporairement les noeuds de votre cluster de communiquer entre eux.
Certains noeuds ne pouvant plus parler au « master node » vont déclencher spontanément l’élection d’un nouveau master.

Le résultat de cette élection sera ce qu’on appelle un split-brain : le cluster se trouve séparé en deux avec deux « master node » différents (et donc une désynchronisation des indexations).

Il s’agit bien sûr d’une situation à éviter absolument. Heureusement, il existe une solution : modifier le paramètre discovery.zen.minimum_master_nodes, défini de la manière suivante :

The discovery.zen.minimum_master_nodes allows to control the minimum number of master eligible nodes a node should « see » in order to operate within the cluster.

La solution au « split-brain » consiste donc à définir discovery.zen.minimum_master_nodes avec le nombre de noeuds du cluster / 2 + 1. S’il y a 8 noeuds dans votre cluster, vous renseignerez alors cette valeur à 5.
Ainsi, 5 noeuds votants sont nécessaires à l’élection d’un nouveau master. 2 machines ne peuvent donc pas obtenir suffisamment de votes pour être élues en simultané.

————— MasterNotDiscoveredException —————

Voilà, tout va pour le mieux dans le meilleur des mondes, votre cluster fonctionne et … vous souhaitez déconnecter une partie de vos noeuds.

Imaginons que nous ayons 8 noeuds sur notre cluster et que nous souhaitions passer sur 4 noeuds. Le paramètre discovery.zen.minimum_master_nodes de départ vaut 5.

Il faut alors modifier à nouveau le paramètre discovery.zen.minimum_master_nodes pour prendre en compte le nouveau nombre de noeuds nécessaires à l’élection (4 / 2 + 1, donc 3). Cette modification peut s’effectuer avec curl via l’API settings :

curl -XPUT localhost:9200/_cluster/settings -d '{
    "persistent" : {
        "discovery.zen.minimum_master_nodes" : 3
    }
}'

Voilà. Si on stoppe maintenant nos noeuds, le cluster continuera de tourner, n’est-ce pas ?
Oui … seulement si on ne coupe pas le master.

Si jamais le master node fait partie des noeuds arrêtés, les requêtes que vous effectuerez retourneront MasterNotDiscoveredException :

curl -XGET "http://localhost:9200/_cluster/health?pretty=true"
{
  "error" : "MasterNotDiscoveredException[waited for [30s]]",
  "status" : 503
}

————— Mais que se passe-t-il ? —————

Résumons :

  • Notre cluster comprenait 8 noeuds
  • Le « discovery.zen.minimum_master_nodes » vaut 5
  • Nous souhaitons passer à 4 noeuds
  • Nous avons modifié le « discovery.zen.minimum_master_nodes » à 3 (4 / 2 + 1)
  • On stoppe des noeuds quelconques pour se ramener à 4 noeuds => pas de soucis
  • Si le master est parmi les noeuds stoppés => MasterNotDiscoveredException

Tout se passe comme si le cluster n’appliquait plus le bon quota, puisqu’un nouveau master n’a pas été élu.

Voici mon analyse du problème :

  • La modification dynamique du paramètre « discovery.zen.minimum_master_nodes » via l’API « /_cluster/settings » est prise en compte par le master. C’est lui qui est chargé de redispatcher ce paramètre entre les noeuds
  • Si le master est stoppé, il ne peut plus servir de référence pour les paramètres dynamiques « /_cluster/settings », les noeuds utilisent donc le paramétrage par défaut (issu du elasticsearch.yml)

Bref, dans la mesure du possible, il est préférable de passer par le elasticsearch.yml pour modifier le « discovery.zen.minimum_master_nodes ».