maanantai 30. maaliskuuta 2015

Käytännön kokemuksia Microservices-arkkitehtuurista

Olen työskennellyt pitkään palvelun parissa, jonka uudelleenkirjoitettu versio päädyttiin toteuttamaan microservices-arkkitehtuurilla puolivahingossa. Alkuvaiheessa uudelleenkirjoitusprojektia tarkoitus ei ollut tehdä toteutusta microservices-arkkitehtuurilla, mutta alkuvaiheen jälkeen huomasimme, että sellainen siitä tuli tehtyä.

Toteutettu järjestelmä käsittelee satoja tuhansia viestejä päivässä. Yksittäisten viestien koko vaihtelee kilotavuista kymmeniin megatavuihin. Viestit tulevat kymmenistä eri järjestelmistä ja päätyvät kymmeniin eri järjestelmiin. Järjestelmän vanhetessa siihen liitetään vielä satoja uusia järjestelmiä, joista tulevia viestejä käsitellään ja toimitetaan eteenpäin. Järjestelmä ei saa muokata viestin tietosisältöä, vaikka sitä muunnetaan muodosta toiseen. Viestejä ei saa myöskään hukata tai monistaa. Tällaiset vaatimukset ovat hankalia toteuttaa millä tahansa arkkitehtuurilla, mutta microservices-arkkitehtuuri näyttäisi toimivan kuitenkin paremmin kuin vastaavan (molemmat ratkaisumallit käytännössä nähneenä) järjestelmän toteuttaminen eräpohjaisesti tai ESB:n pohjalle rakennetulla SOA-ratkaisulla, joissa molemmissa viestien käsittely tapahtuu yhdessä ja samassa monoliittisessa prosessissa.

Hyödyt


Microservices-arkkitehtuurien väitetään olevan skaalautuvia, hajautettavia, uudelleenkäytettäviä, teknologiariippumattomia ja helppoja ymmärtää, koska järjestelmä rakentuu yksinkertaisista palveluista. Kokemukseni mukaan microservice-arkkitehtuuri mahdollistaa juuri nämä ominaisuudet, mutta automaattisesti hyötyjä ei saa.

Skaalautuvuus vaatii yksittäisiltä palveluilta tilattomuutta ja toiminnallista riippumattomuutta toisista palveluista. Tilatonta yksinkertaista palvelua on helppo monistaa, jolloin sitä on myös helppo skaalata. Vaikka palvelua itseään on helppo monistaa, niin lisäksi pitää ratkaista miten palvelun eri instanssit löytyvät. Mielestäni palvelun instanssien löytämisen ratkaiseminen on hankalampaa kuin useiden palveluinstanssien luominen.

Tilaton palvelu on helppo hajauttaa, koska sen instanssit eivät riipu toisistaan. Näin palvelu voi toimia sijainnista riippumatta useassa eri paikassa yhtä aikaa.

Mikäli palveluiden välinen kommunikaatio on toteutettu jollain laajasti tuetulla protokollalla, kuten HTTP:llä, ei palvelun sisäisellä toteutuksella tai toteutustekniikalla ole merkitystä. Tästä seuraa toteutuksen teknologiariippumattomuus.

Uudelleenkäytettävyys ja yksinkertaisen palvelutoteutuksen ymmärrettävyys ovat myös merkittäviä etuja järjestelmän koko elinkaarta ajatellen. Mikäli yksi toteutus pystyy tarjoamaan saman palvelun usealle eri käyttäjälle, ei toteutusta tarvitse toistaa uudestaan ja uudestaan eri paikoissa. Palvelutasolla toteutuksen ja palvelun tarkoituksen ymmärtäminen on helppoa, mikäli palvelu tekee vain yhtä asiaa. Käytännössä yhden asian tekeminen tarkoittaa yksinkertaista toteutusta. Uudelleenkäytettävyys ja ymmärrettävyys tukevat siis molemmat toisiaan.


Ylläpidettävyys ja järjestelmän elinkaari


Yksinkertaisista palveluista koostuva järjestelmä on helpompi ylläpitää palvelutasolla kuin monoliittinen järjestelmä. Näin ollen toteuttajien vaihtuvuus ei haittaa palvelun ylläpitoa ja jatkokehitystä niin merkittävästi kuin monoliittisissa järjestelmissä, joissa yksittäisen toiminnallisuuden muuttaminen edellyttää usein koko järjestelmän tuntemista. On kuitenkin huomattava, että vaikka yksittäisten palveluiden ylläpitäminen ja jatkokehitys on helppoa, voi järjestelmän kokonaiskuva jäädä kehittäjille vieraaksi. Järjestelmätason ymmärrettävyysongelmiin microservices-arkkitehtuuri ei mielestäni juuri tuo helpotusta.

Isoissa järjestelmissä palvelun elinkaari on usein hyvin pitkä, usein vuosia tai jopa kymmeniä vuosia. Näiden vuosien aikana parhaina pidetyt toteutusteknologiat vaihtuvat ja järjestelmän sujuva jatkokehitys edellyttää usein uusien teknologioiden liittämisen vanhaan järjestelmään. Mikäli palveluiden välisessä viestinnässä käytetty protokolla on kuitenkin vielä käyttökelpoinen, voi uuden microservice-palvelun toteuttaa parhaalla mahdollisella teknologialla aiemmasta toteutuksesta välittämättä. Monoliittista suurta järjestelmää ei voi yleensä muuttaa käyttämään aiemmasta poikkeavia teknologioita, koska vanhaa ja uutta toteutusta ei voi liittää helposti yhteen.

Koska parhaatkin arvaukset tarvittavista ominaisuuksista menevät usein metsään, on hyvä, mikäli järjestelmän osia voi vaihtaa pikkuhiljaa uusiin ja tarpeita vastaaviin. Yksinkertainen palvelu on helppo kirjoittaa kokonaan uusiksi eikä muutaman palvelun uudelleenkirjoittaminen vie paljoa aikaa. Näin missä tahansa järjestelmän ylläpito- ja jatkokehitysvaiheessa voidaan tarvittaessa pienin kustanuksin uusia järjestelmää pala kerrallaan eikä kallista sekä riskialtista kertahyppäystä versiosta toiseen tarvita.

Järjestelmän elinkaaren kannalta mielestäni tärkein päätös on palveluiden välisen kommunikaatiotavan valitseminen: mitä protokollaa palvelut puhuvat toisilleen? Miten mahdollinen autentikaatio ja autorisaatio tehdään? Entä miten palvelut löytävät toisensa?


Käytännön rajoitteet ja ongelmat


Microservice-arkkitehtuuri vaatii huolellista palveluiden hallinnoinnin etukäteissuunnittelua. Kun puhutaan kymmenistä erilaisista palveluista eli käytännössä kymmenistä eri prosesseista, täytyy palveluiden konfiguraatioiden hallinnan ja palveluiden käyttöönoton (deployment) olla hyvin hallussa.

Lukuisten palveluiden asentaminen ja ylläpitäminen käsipelillä on vaikeata. Kun palveluita asennetaan usealle koneelle ja mahdollisesti monistetaan dynaamisesti, on käsityö mahdotonta. Siksi järjestelmän asennusten automatisointi on välttämättömyys jo hyvin vaatimattomassa microservice-pohjaisessa järjestelmässä. Automaattinen asennus on kuitenkin hyvästä jo muutenkin: http://pedanttinen.blogspot.fi/2012/04/automatisoidun-asennuksen-autuus.html

Monesta pienestä palvelusta (usein prosessista) koostuvassa järjestelmässä yhtenäisen tiedon kerääminen ja tilannekuvan ylläpitäminen on haasteellista. Järjestelmän käsittelemän yksittäisen kokonaisuuden prosessointi kulkee monen eri palvelun kautta mahdollisesti monella eri koneella, jolloin esimerkiksi virhetilanteessa voi olla vaikeata löytää missä kohtaa järjestelmää virhe tapahtui. Järjestelmän tilannekuvan luominen eli kunkin ajanhetken toimintakyvyn selvittäminen voi vaatia monimutkaista päättelyä. Esimerkiksi yhden palveluinstanssin toimimattomuus ei välttämättä merkitse järjestelmätasolla mitään, sillä palveluinstanssi ei välttämättä ole käytössä tai saman palvelun toinen instassi saattaa järjestelmätasolla peittää vian.

Usein ison monoliittisen järjestelmän käynnistäminen kestää hyvin kauan, mikä on kehitystyön kannalta todella ikävää. Microservice-arkkitehtuurissa yksittäisen palvelun käynnistäminen puolestaan on hyvin nopeata, mikä tukee hyvin nopeata kehityssykliä. Kolikon kääntöpuolena on se, että kokonaisen microservice-pohjaisen järjestelmän käynnistäminen on helposti vielä hitaampaa kuin monoliittisen järjestelmän eikä kehittäjän ole helppo laittaa koko järjestelmää omalle kehityskoneelleen pyörimään. Joissain ongelmaselvitystilanteissa tämä voi olla erittäin ikävää.

Palvelun uudelleenkäytettävyys vaatii onnistumista rajapintasuunnittelussa, eikä microservices-arkkitehtuuri tätä vaatimusta muuta. Päin vastoin, rajapintasuunnittelu on entistä hankalampaa, koska palvelun ei pitäisi välittää tai tietää varsinaisista kutsujista mitään. Palvelun rajapinnan suunnittelussa pitää keskittyä erityisesti varsinaisen ongelman ratkaisuun eikä miettiä miten palvelua olisi esimerkiksi helppo kutsua palvelusta x tai mikä olisi minimiratkaisu palvelun y ongelmaan.


Elävän elämän esimerkki


Esimerkkijärjestelmä käsittelee satoja tuhansia yksittäisiä viestejä päivässä, jotka tulevat kymmenistä eri ulkoisista järjestelmistä ja päätyvät kymmeniin eri järjestelmiin. Käsittely on pääosin viestin muuntamista muodosta toiseen sekä reitittämistä ulkoisten järjestelmien välillä. Ehdottomina vaatimuksina on keskeisen viestisisällön säilyttäminen muunnoksista huolimatta muuttumattomana, viestin 100% toimitusvarmuus ja se, että viesti toimitetaan eteenpäin täsmälleen kerran. Järjestelmä ei voi luottaa sisääntulevien viestin sisältöön millään tavalla ja virheellisistä viesteistä pitää pitää kirjaa ja antaa palaute. Nämä vaatimukset ovat haastavia mille tahansa systeemiarkkitehtuurille.

Yksittäiset palvelut on toteutettu järjestelmässä Javalla ja näiden välinen viestintä tapahtuu HTTP/REST-rajapinnoilla JSON-muotoisilla viesteillä. Palvelut jakaantuvat karkeasti ottaen kolmeen ryhmään: 1) Väyläpalveluihin, 2) apupalveluihin sekä 3) ajastettuihin palveluihin. Palveluita järjestelmässä on yli neljäkymmentä ja lisää on tekeillä.

Järjestelmän perustehtävä on siis hakea viesti yhdestä kanavasta, käsitellä viesti ja toimittaa toiseen kanavaan. Koska erilaisia viestinkäsittelytapoja on vähintään niin paljon kuin sisääntulevia kanavia, pitää käsittelyreitin olla hyvin joustava. Käsittelyreitin muodostaa palvelu, joka liikuttaa viestiä toisten palveluiden välillä (voidaan ajatella viestiväylänä) ja ajoaikaisesti konfiguroitava joukko palveluita, joille viesti kuljetetaan. Käsittelyreitti tarkoittaa siis joukkoa palveluita ja niiden välistä käsittelyjärjestystä.

Esimerkkireitti voisi lukea XML:ää järjestelmään ja toimittaa siitä muodostettuja sähköposteja ulos. Yksinkertaisimmillaan reitti voisi sisältää täsmälleen yhden palvelun, joka ottaa viesti-XML:n sisään ja lähettää sen sähköpostiksi muunnettuna maailmalle. Käytännössä järkevämpää on pilkkoa reitti pienemmiksi palveluiksi. Palvelut voisivat olla seuraavat: 1) XML:n validointi, 2) XML:n parsinta eli olennaisten tietojen hakeminen, 3) tiedon muuntaminen sähköpostiksi ja 4) sähköpostin lähettäminen. Hienommaksi jauhetusta palvelujoukosta huomaa heti, että jokainen palvelu voisi olla helposti uudelleenkäytettävissä toisellakin reitillä!

Reiteillä olevat palvelut käyttävät apupalveluita, jotka tekevät usein toistuvia asioita tai esimerkiksi toimivat jaettuina tietovarastoina. Viestien käsittelyssä useimmiten tarvittava palvelu on XSL-muunnos - joskus hyvin raskas sellainen - mitä ei kannata toteuttaa jokaiseen palveluun erikseen. XSL-muunnospalvelusta on siis tehty apupalvelu, jota muut palvelut käyttävät muunnosten tekemiseen.

Reiteillä olevat palvelut toimivat, kun reitille saapuu käsiteltävä viesti. Ne vaativat siis ulkoisen herätteen. Koska järjestelmä tarvitsee myös kellonaikaan sidottuja toiminnallisuuksia, tarvitaan väylä- ja apupalveluiden lisäksi vielä kolmas palvelutyyppi eli ajastatetut palvelut. Nämä palvelut valvovat esimerkiksi  järjestelmän tilaa ja lähettävät ajastettuja raportteja.

Reiteillä sijaitsevat palvelut eivät tiedä mitä palveluita niitä ennen on kutsuttu tai mitä palveluita niiden jälkeen tullaan kutsumaan. Palvelut kuitenkin kommunikoivat toistensa kanssa asettamalla viesteihin lisätietoja. Esimerkiksi ulosmenoreitin määrittävä palvelu saattaa asettaa viestille tiedon, että ulosmenoreitti on sähköposti. Näin sähköpostisisällön luova palvelu ymmärtää luoda viestille sähköpostin ja sähköpostia lähettävä palvelu huomaa sen lähettää maailmalle. Huomattakoon kuitenkin, että mikään edellä mainittu palvelu ei tiennyt mistä sen käyttämät tiedot ilmestyivät ja miksi.

Kun palvelu käynnistyy, se rekisteröityy konfiguraatiota hallitsevaan apupalveluun. Palvelu kysyy konfiguraatiopalvelulta oman konfiguraationsa ja tarvittessa selvittää missä sen tarvitsemat muut palvelut sijaitsevat. Palveluihin on siis kovakoodattu asennusvaiheessa sen käyttämän konfiguraatiopalvelun sijainti, mutta kaikki muut tiedot voidaan selvittää ajoaikaisesti.

Konfiguraatiopalvelu sisältää järjestelmän kaiken ajoaikaisesti muutettavissa olevan konfiguraation. Käytännössä ajoaikainen konfiguraatiomuutos saa konfiguraatiopalvelun kutsumaan muita palveluita, joita konfiguraatiomuutos koskee ja päivittämään näiden palveluiden konfiguraation. Konfiguraatiopalvelun kautta hallitaan myös käsittelyreittejä eli uusia viestien käsittelyreittejä hallitaan ajoaikaisesti.

Koska yksittäisen viestin käsittelyä on hyvin hankala seurata pelkästään esimerkiksi lokitiedostojen sisältöä seuraamalla, on järjestelmään rakennettu tapahtumia keräävä palvelu. Kaikki yksittäisen viestin käsittelyyn liittyvät merkittävät tapahtumat lähetetään tapahtumapalveluun säilöön. Näin minkä tahansa viestin käsittelyhistoria on saatavilla ja vikatilanteissa on helppo selvittää mitä viestille tapahtui. Lisäksi lokitietoa kerätään keskitetysti, jolloin akuutit virhetilanteet on helpompi selvittää.

Järjestelmän tilaa valvotaan pääasiassa palvelutasolla. Mikäli yksittäinen palvelu menee yllättäen alas, järjestelmä hälyttää. Lisäksi järjestelmän läpi ajetaan jatkuvasti testiviestejä, joilla järjestelmän toimintaa voidaan valvoa järjestelmätasolla. On nimittäin mahdollista, että mikään palvelu ei ole alhaalla, mutta järjestelmä ei silti kokonaisuutena toimi.

Järjestelmän hajautus perustuu jonojen hajautettuun lukemiseen. Jokainen käsittelyreitti muodostuu osareiteistä, jotka päättyvät aina jonoon. Yhden käsittelyreitin sisällä mennään siis useamman kerran jonoon. Jonosta lähtiessään viesti saattaa jatkaa käsittelyä samalla palvelimella kuin aiemmin tai sitten jossain muualla. Näin pienet kokonaisuudet saadaan käsiteltyä samalla palvelimella (tai oikeammin samassa palveluryhmässä) ja kuitenkin tarvittaessa käsittely skaalautuu useammalle koneelle (tai palveluryhmälle).

Yksinkertaisessa esimerkkikuvassa alla esitetään sisääntulevat viestit tiedostoina, jotka haetaan kuvan yläreunassa olevasta laatikosta. Sisääntuleva viesti käsitellään kolmessa eri palvelussa ennen kuin siitä muodostetaan tulostiedosto. Käsittelyjärjestys on ajoaikaisen konfiguraation määräämä. Palvelu, joka viestejä liikuttaa käsittelypalveluiden välillä ei näy kuvassa. Jokaisesta kolmesta käsittelypalvelusta on olemassa kaksi instanssia, jotka on hajautettu eri ajoympäristöihin. Kunkin viestin käsittely saman palvelun eri instansseissa määräytyy jonosta lukemisjärjestyksessä. Tyypillisesti palvelut lukevat jonosta niin paljon viestejä kuin pystyvät, jolloin yleensä vähiten kuormitettu instanssi käsittelee viestin, koska se ehtii jonosta eniten uusia viestejä hakemaan. Kuten kuvasta näkyy, järjestelmän hajautus perustuu jonototeutuksen hajautukseen, mikä siirtää yhden hankalan ongelman valmiskomponentin ratkaistavaksi. Laadukkaita hajautettuja jonototeutuksia on tarjolla useita ja käytetty toteutus on helppo vaihtaa (muutoksia vain reitityksestä huolehtivaan palveluun), joten valmiskomponentin käyttö soveltuu järjestelmään enemmän kuin hyvin. Käytännössä kuvatunlaisen arkkitehtuurin skaalautuvuutta rajoittavat jonototeutuksen rajoitukset, viestejä sisääntuovan järjestelmän rajoitukset sekä tallennusta hoitavien järjestelmien rajoitukset.



Omat kokemukseni microservice-arkkitehtuurista ovat äärimmäisen positiiviset. En kuitenkaan lähtisi näin monimutkaisella tavalla ratkaisemaan kaikkia mahdollisia ongelmia. Monoliittiratkaisun microservices kuitenkin voittaa varmasti lähes aina, mikäli järjestelmä on yhtään laajempi.

sunnuntai 15. maaliskuuta 2015

Installing Ubuntu 15.04 on Surface Pro 3

UPDATE: see my experiences with release version from http://pedanttinen.blogspot.fi/2015/08/installing-dual-boot-ubuntu-1504-on.html

Here's my experiences on installing Ubuntu 15.04 from daily build. This is not the official release version try out!

By following these instructions, you get Ubuntu 15.04 installed on Surface Pro 3, but at least at the moment everything does not work right. The older instructions based on 14.10 do not help to overcome the issues.

I'm using Surface Pro 3 i5/256GB/8GB version. I wanted to keep pre-installed Windows 8.1 as secondary operating system, because Ubuntu still has many known issues. So, I'm creating a dual boot setup.

Before installingn Linux, you have to make some space for it. I used Windows tools to reduce the partition size. I left 50GB for Windows partition, which means that there's about 20GB of free space before anything else than default software has been installed.

Reducing Windows partition size

  1. Start diskmgmt.msc tool
  2. Try to reduce the size of the Windows partition
  3. In my case, I had to remove Bitlocker from the Windows partition before I was able to reduce the size

Booting from USB stick

  1. Remove secure boot. Shutdown Surface and restart it by first holding down volume up button and then the power button.
  2. Disable secure boot.
  3. Shutdown Surface
  4. Start Surface by holding volume down button and then power button

Install Ubuntu 15.04

  1. To speed up installation process, do not let installer to access internet
  2. During installation, choose manual partitioning
  3. I created the following partitions 
    1. Primary 30GB for root (the / mount point)
    2. Primary (actually not sure whether you can choose between logical and primary for swap) 8GB swap
    3. Primary partition for /home with rest of the space
  4. I installed Linux boot loader on /dev/sda. Many posts in internet say that it would be better to install it on the Windows EFI partition i.e. /dev/sda2, but I got it working this way, so I cannot tell whether it's a good idea or not
  5. After installation is ready, boot Surface and you will end up back in Windows. Ubuntu is nowhere to find
  6. To fix the problem, you have to use boot-repair tool
    1. To run boot-repair, boot Linux from USB stick and run the following commands in shell (these instructions are based on the official guide: https://help.ubuntu.com/community/Boot-Repair)
      1. sudo add-apt-repository ppa:yannubuntu/boot-repair
      2. sudo apt-get update
      3. sudo apt-get install -y boot-repair && boot-repair
      4. Accept default recommended repair
      5. Remember to note down the link boot-repair is giving you
      6. Check the last lines generated by the tool, because they are needed later
    2. Most likely because my Ubuntu 15.04 is from daily build, I wasn't able to execute the boot-repair tool from Ubuntu 15.04 USB stick, so I had to use 14.04 Ubuntu to do this. Since Ubuntu 14.04 does not support Surface type cover keyboard out-of.the-box, you have to use the Ubuntu's default virtual keyboard Onboard, which you can access from the application menu.
  7. After reboot, you still end up in Windows automatically. To make GRUB 2 working, you have to go to Windows CMD as administrator and then execute the command boot-repair tool gave you in the end of its generated output. Most likely it's the one below
    1. bcdedit /set {bootmgr} path \EFI\ubuntu\shimx64.efi
  8. Shutdown the system and boot by pressing volume up + power button. Enable secure boot (if you don't enable it, Surface will keep on booting to Windows)
  9. Now after reboot, GRUB 2 should show you the menus for selecting Windows or Ubuntu dual boot.
  10. At this point, I made Windows my default boot option
    1. Boot to Linux
    2. sudo vi /etc/default/grub
    3. Change line with GRUB_DEFAULT=0 to GRUB_DEFAULT=2 (you can calculate the correct number by checking which line in boot menu has Windows. Indexing starts from 0). Save changes.
    4. sudo update-grub
    5. Next time Windows will be selected automatically, if user does not select anything else in the boot menu
  11. It's worth to notice that if you don't have type cover attached to Surface during boot, you can still use on-screen keyboard to select the right operating system from boot menu. Nice!

What works and what doesn't

Working out-of-the-box
  • Type cover
  • Surface pen as a mouse / pointing device (buttons do not work however)
  • Touch screen (sometimes stops working for a while and has some strange behaviour at times)
  • WLAN
  • Bluetooth
Not working out-of-the-box
  • Cameras
  • Touch pad
  • Ubuntu does not recognize that it's running on laptop with battery. At least the desktop software cannot show the state of the batter
The biggest problems in running Ubuntu 15.04 on Surface at the moment is the lack of touch pad support. It would be nice also to get the touch screen working without random problems. I will keep on using Windows as long as I'm not able to solve the touch pad issue. Otherwise the system seems to run ok.

maanantai 29. joulukuuta 2014

Sonatype Nexus not returning the latest releases through REST interface

Sonatype's Nexus Maven repository behaves strangely after using Rebuild Metadata feature from the Nexus UI. For some reason this feature updates all maven-metadata.xml files under repository and adds this line to all of them

<latest>0.0.48</latest>

The added line breaks Nexus's REST query API so that it always returns the version given in the latest element instead of the real latest released version. So, for example this query would always return version 0.0.48, if the latest element is in maven-metadata.xml file.

$ curl
"http://<hostname>:<port>/nexus/service/local/artifact/maven/redirect?r=releases-repository-name&g=com.group&a=artifact-name&e=jar.md5&v=LATEST

To fix this, you can run the following command under the broken repository to remove all latest elements.

$ find . -name maven-metadata.xml -exec sed -i -r "/<latest>.+<\/latest>/d" \{\} \;

An alternative way to query the latest releases is the following.

$ curl
"http://<hostname>:<port>/nexus/service/local/artifact/maven/redirect?r=releases-repository-name&g=com.group&a=artifact-name&e=jar.md5&v=RELEASE

This query works even with the latest element.

It seems that latest element is not affecting queries of the latest SNAPSHOTs.

For more information about this subject.

lauantai 24. toukokuuta 2014

How to make Ubuntu play music over bluetooth

After installing latest Ubuntu version 14.04 (Trusty Tahr) I found out by accident that Ubuntu is able to play music from bluetooth source. Here's how it works on my laptop (Sony Vaio VPCEA2S1E, perhaps there's differences in the bluetooth adapters?).

Check has PulseAudio already loaded module-bluetooth-discover.
$ pactl list | grep -i module-bluetooth-discover

If this command does not return anything, you should load the module with the following command.
$ pactl load-module module-bluetooth-discover

Now the first command shows that the module is loaded.
$ pactl list | grep -i module-bluetooth-discover
Name: module-bluetooth-discover

After executing the command, you should be able to add your for example your phone as the source of audio. In my Windows Phone Lumia 1020, I navigate to Settings -> Accessories -> add using bluetooth. If you have music playing on your phone and you connect to your Ubuntu via bluetooth, your phone will show text connected music. And the music is coming out of your computer's speakers!

I also discovered that in the latest Ubuntu version my bluetooth keyboard Logitech diNovo Edge settings have to be adjusted manually. The mouse pointer does not move fast enough when using keyboard's touchpad. In the previous version Ubuntu 12.04 I was able to set the speed and acceleration of pointer fast enough from System Settings UI. In 14.04, I cannot. However, it's easy to change the acceleration and speed of pointer from command line. Here's the command.
$ xset m 3 1

All in all, I'm very happy with the latest Ubuntu release!

lauantai 1. helmikuuta 2014

Setting up Logback with nice default appender

I spent some time looking for a solution to make Logback log everything to console in development environment and still have easy setup for logging into file in production environment.

So, my goal was that if I execute my application without any extra Logback configuration, everything goes to console. And in case configuration file exists in the environment, Logback would use that and forget the default configuration.

The default console logging configuration is in the root of classpath and is named as logback.xml (the file Logback looks for by default). Here's the contents the file.

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

  <define name="INCLUDED_FILE_EXISTS" class="ch.qos.logback.core.property.FileExistsPropertyDefiner">
    <path>${configpath}/included-logback.xml</path>
  </define>
  
  <if condition='property("INCLUDED_FILE_EXISTS").equals("true")'>
    <then>
      <!-- this configuration is used in other than development environments i.e. custom config per environment -->
      <include file="${configpath}/included-logback.xml"/>
    </then>
    <else>
      <!-- This configuration is used only in development enviroment -->
      <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
          <pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
        </encoder>
      </appender>
      <root level="WARN">
        <appender-ref ref="STDOUT" />
      </root>
      <logger name="fi.foo.bar.package" level="DEBUG"/>
    </else>
  </if> 
</configuration>

This configuration has two important sections. First one is the use of FileExistsPropertyDefiner. It checks the existence of the given file in file system and sets value of property INCLUDED_FILE_EXISTS to true or false. If you want, you can also check existence of a file on classpath by using ResourceExistsPropertyDefiner. The variable configpath is defined in system properties.  Second interesting section is where the INCLUDED_FILE_EXISTS property is used to include the external file. If the file was found, its configuration is used and default configuration is discarded completely.

Here is example of the included file. The file has name included-logback.xml.

<included scan="true">
  <!-- This file is scanned so you can update it while application is running -->
  <appender name="FILELOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${configpath}/logs/application.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${configpath}/logs/application-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
    </rollingPolicy>
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="FILELOG" />
  </root>
</included>

torstai 10. lokakuuta 2013

Creating a bootable Xubuntu USB stick and installing Xubuntu on another USB stick

A friend of mine was complaining that his Asus Eee 1015BX is running too slow with Windows 7 Starter that came pre-installed with the laptop. It's no wonder that Windows is slow, because the laptop has only 1GB of memory. As a great (Ubuntu) Linux evangelist, I wanted to help the poor guy and give him a chance to test Ubuntu without any risks.

Because Ubuntu would probably be too heavy for such low end hardware, I thought that Xubuntu would serve better. I didn't want to change anything on the laptop. I just wanted to offer the Ubuntu experience without any risks. I decided that the best way to achieve this would be to install Xubuntu on USB disk.

To install Xubuntu on USB disk, you need installation media. Well, this laptop has only two USB ports (of course also LAN), so I cannot use DVD as in my previous installations. First problem was to create Xubuntu installation media. For some reason Ubuntu Startup Disk Creator doesn't allow user to create bootable USB stick from Xubuntu ISO image. Luckily there's an alternative, UNetbootin, which worked perfectly.

Next step was to run the installation software. You cannot set Eee's BIOS to boot from USB, but you can hit escape key during start-up to change booting options. After I got Eee to boot from USB, I inserted another USB stick, which would serve as the installation target media.

I'm not going through the details step-by-step, because installer is quite easy to use. I created /, /boot and /home partitions and no swap. It's important to notice that you shouldn't use USB stick to store a swap partition. Firstly, swap on USB is slow and secondly, it will - according to internet discussions - destroy your USB stick after some time. Installation took a lot of time, almost an hour. It would never take that long on a hard drive.

After the installation, there's some things you can do to optimise the speed of the system and the lifetime of the USB drive. Here's what I did.

Edit /etc/fstab. Add noatime flag to / and /home to avoid updating access times on files. This should speed things up  and help to avoid extra wearing of the USB stick. Here's an example.
# / was on /dev/sdb5 during installation
UUID=ae7eb15f-a65b-4e2b-a008-20a2b41ce72a /               ext4    noatime,errors=remount-ro 0       1
# /boot was on /dev/sdb1 during installation
UUID=190d4960-d773-451f-bbb3-b1a8bd39238b /boot           ext4    defaults        0       2
# /home was on /dev/sdb6 during installation
UUID=2fb3fc52-6155-4fdf-89f5-d991bca484fc /home           ext4    defaults,noatime        0       2

Disable mlocate so that it won't re-index the whole disk every day.
sudo chmod -x /etc/cron.daily/mlocate

Disable journaling from / and /home (you need to do these in single user mode or by starting Linux from some other media like the USB stick containing installation media, because the partitions cannot be mounted during the operation). By disabling journaling, you may end up with corrupted file system if the system crashes. On the other hand, you reduce the number of writes on the USB drive which should make things faster and prolong the lifetime of USB drive.
sudo tune2fs -O ^has_journal /dev/sdb5
sudo tune2fs -O ^has_journal /dev/sdb6

By the way, you can also prepare the drives already beforehand and format the partitions for ext4 without journaling with the following command. I did this after my first installation attempt, because I hoped it would speed up the slow installation process (I'm not sure did it help).
sudo mke2fs -t ext4 -O ^has_journal /dev/sdb6

After editing the file system parameters, it's a good idea to check that the file system is ok (-f for forcing the check)
sudo e2fsck -f /dev/sdb5
sudo e2fsck -f /dev/sdb6


Here's what I learned from all of this. Xubuntu is much much faster than Windows 7 on the Asus Eee. That was no surprise. All the hardware worked properly. After installing the proprietary AMD display drivers, battery consumption went down and battery life is at least as good as on Windows. Although it takes some time to install Xubuntu from USB to USB, it's a very good way to make a test drive with Xubuntu. You don't risk anything and user can always go back to Windows without any extra hassle.

Although Xubuntu worked well, my USB stick didn't. I bought a tiny Kingston 32GB Datatraveller Micro to serve as installation target media. The Datatraveller Micro is so small that you can carry it attached to the laptop all the time. Unfortunately, it's also slow making Xubuntu work very slowly during disk intensive operations. I compared the write speed of Datatraveller Micro to Datatraveller 100 on USB 2 port. Datatraveller Micro's write speed was only 6MB/s whereas Datatraveller 100's write speed was 22MB/s. Although the physical dimensions of Datatraveller Micro are perfect for running laptop Linux from it, the speed of the drive makes it questionable for this purpose.

This story may have a happy ending, because it seems that Ubuntu is getting yet another happy user in the very near future (after I install Xubuntu on Eee's hard drive)!

lauantai 2. helmikuuta 2013

Easy JUnit testing with Elastic Search

It was quite difficult to find good examples on JUnit testing (this is more like integration testing than unit, but nevertheless JUnit is used) Elastic Search code. Here's my attempt to fix this issue.

The idea is to start a standalone Elastic Search instance in the test. This way we don't have to make sure that every developer has access to Elastic Search instance running somewhere out of build's control. The downside of starting an instance inside test is that it may get quite slow to run the test. However, that's another problem to tackle.

I'm using Jersey HTTP client in this example to connect to Elastic Search. Any other client works as well.

Maven POM 

Maven POM that's needed to execute my example.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fi.elastic-search.test</groupId>
  <artifactId>elastic-junit</artifactId>
  <name>elastic-junit</name>
  <packaging>jar</packaging>
  <version>1.0.0-BUILD-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.elasticsearch</groupId>
      <artifactId>elasticsearch</artifactId>
      <version>0.20.3</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-client</artifactId>
      <version>1.17</version>
</dependency> 
  </dependencies>
</project>

JUnit test code 

Here's the Java code starting standalone Elastic Search instance and creating an index.
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.ws.rs.core.MediaType;

import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.node.Node;
import static org.elasticsearch.node.NodeBuilder.nodeBuilder;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.api.client.filter.LoggingFilter;


public class ElasticSearchTest {
    private Client client;
    private static String testIndexName = "junitindexname";
    private static final String HTTP_BASE_URL = "http://localhost";
    private static final String HTTP_PORT = "9205";
    private static final String HTTP_TRANSPORT_PORT = "9305";
    private static final String elasticSearchBaseUrl = HTTP_BASE_URL + ":" + HTTP_PORT;

    private static Node node;

    @BeforeClass
    public static void startElasticSearch() throws Exception {
        final String nodeName = "junittestnode";

        Map settingsMap = new HashMap();
        // create all data directories under Maven build directory
        settingsMap.put("path.conf", "target");
        settingsMap.put("path.data", "target");
        settingsMap.put("path.work", "target");
        settingsMap.put("path.logs", "target");
        // set ports used by Elastic Search to something different than default
        settingsMap.put("http.port", HTTP_PORT);
        settingsMap.put("transport.tcp.port", HTTP_TRANSPORT_PORT);
        settingsMap.put("index.number_of_shards", "1");
        settingsMap.put("index.number_of_replicas", "0");
        // disable clustering
        settingsMap.put("discovery.zen.ping.multicast.enabled", "false");
        // disable automatic index creation
        settingsMap.put("action.auto_create_index", "false");
        // disable automatic type creation
        settingsMap.put("index.mapper.dynamic", "false");

        removeOldDataDir("target/" + nodeName);

        Settings settings = ImmutableSettings.settingsBuilder()
                .put(settingsMap).build();
        node = nodeBuilder().settings(settings).clusterName(nodeName)
                .client(false).node();
        node.start();
    }

    private static void removeOldDataDir(String datadir) throws Exception {
        File dataDir = new File(datadir);
        if (dataDir.exists()) {
            FileSystemUtils.deleteRecursively(dataDir, true);
        }
    }

    @AfterClass
    public static void stopElasticSearch() {
        node.close();
    }

    @Before
    public void initialize() {
        // create client for each test
        ClientConfig clientConfig = new DefaultClientConfig();
        client = Client.create(clientConfig);
        client.addFilter(new LoggingFilter(System.out));
    }
    
    @Test
    public void testCreateIndex() {
        WebResource service = client.resource(elasticSearchBaseUrl);
        // check first that does the index already exist
        ClientResponse clientResponse = service.path(testIndexName).head();
        if (clientResponse.getClientResponseStatus().equals(ClientResponse.Status.OK))
        {
            Assert.fail("Index exists already");
        }

        String indexJson = 
                "{\"settings\" : {\"index\" : {\"number_of_shards\" : 1,\"number_of_replicas\" : 0}}}";
        try {
            String response = 
                    service.path(testIndexName).
                    queryParam("refresh", "true").
                    queryParam("timeout","5m").
                    accept(MediaType.APPLICATION_JSON).
                    put(String.class, indexJson);
            if (!response.contains("ok")) {
                Assert.fail("Creating index failed. IndexName: " + testIndexName);
            }
        } catch (UniformInterfaceException e) {
            // failed due to Client side problem
            throw e;
        }

        // wait for Elastic Search to be ready for further processing
        HashMap statusParameters =
                new HashMap();
        final String timeout = "30s";
        statusParameters.put("timeout", timeout);
        statusParameters.put("wait_for_status", "green");
        String statusResponse = status(statusParameters);
        if (statusResponse.contains("red")) {
            Assert.fail("Failed to create index");
        }
    }
    
    public String status(final Map optionalParameters) {
        WebResource service = client.resource(elasticSearchBaseUrl);
        WebResource webResource = service.path("_cluster").path("/health");
        for (Entry entry : optionalParameters.entrySet()) {
            webResource = webResource.queryParam(entry.getKey(),entry.getValue());
        }
        return webResource.get(String.class);
    }

}