As I followed the latest attempt at a twitter exodus to mastodon, I found myself immersed in a fresh timeline teeming with things I had somehow missed lately. One in particular caught my attention: decentralized identity proofs using keyoxide.
I got interested in identity proofs when I joined keybase. I eventually went all-in with a semi-paranoid GPG setup[1] :
But storing all my social network handles in my GPG key is painful, hence keyoxide !
The core idea is to verify claims of identity, the claims can be made using 2 protocols:
proof@ariadne.id=
prefix (the classic method)proof=
prefix.In both cases keyoxide will parse the prefixed information, try to determine a custom provider for the provided claim and verify that the proof actually exists. The proof consists in placing the gpg key fingerprint with an openpgp4fpr:
prefix in a known location for the corresponding service.
Injecting the proof in GitHub requires creating a specific gist such as this, for Twitter you will have to post a tweet, for mastodon to add a metadata to your profile, for gitlab to create a project with a specific description, etc.
The claims must match the service’s verification provider expectations, and can be verified at https://keyoxide.org/sig by copy and pasting the signed claims.
This leaves the problem of distributing the signed claims. For now there is no obvious solution, I chose to add the file to the gist proof as that makes it easy to update for me and easy to reach for others.
[1] Maybe I’ll write about this someday but this is all pretty well documented already.
]]>At work we use jenkins for continuous integration tasks, we naturally investigated how to automate our polymer tests from it. The saucelabs documentation on how to properly integrate selenium tests started from jenkins is comprehensive but the bit on Setting Up Reporting between Sauce Labs and Jenkins
assumes you have access to the webdriver instance:
to set the job name and job number you are expected to directly set them in the capabilites
DesiredCapabilities capabilities = new DesiredCapabilities();
// ...
capabilities.setCapability("build", System.getenv("JOB_NAME") + "__" + System.getenv("BUILD_NUMBER"));
the let jenkins display the saucelabs tests results you have to make webdriver write a specific line to stdout
or stderr
SauceOnDemandSessionID=<session id> job-name=<some job name>
Neither the capabilities nor the session id are readily accessible in wct
or in wct-sauce
.
However wct-sauce
includes a travis-specific integration to report job name and number. All you have to do to make it work from jenkins is set 2 environment variables TRAVIS_JOB_NUMBER
and TRAVIS_JOB_NAME
from the corresponding variables in jenkins.
Making webdriver write the correct line on one of the standard outputs is not so easy. You will have to write a wct
plugin such as this one :
module.exports = function(wct, pluginOptions, plugin) {
wct.on('browser-end', function(def, error, stats, sessionId, browser) {
wct.emit('log:info', 'SauceOnDemandSessionID=' + sessionId + ' job-name=' + process.env.BUILD_NUMBER);
})
};
How to write a plugin is almost documented in the wct
’s readme. I couldn’t find a reference documentation of the available events and had to go through the code to find the browser-end
event published by wct
. As I write this, I am pretty sure it is the only event which contains the sessionId or a connected browser.
It is far from perfect : if the test times out or throws, there is a good chance that browser-end
will not be emitted.
The next step is configuring the plugin in wct
which is inferrable from the readme since plugins is plural it accepts a list of plugins, like this:
{
"plugins": {
"jenkins":{
"disabled": false,
}
"sauce": {
"disabled": true,
"browsers": [...
]
}
}
}
The last step and most frustrating one for me was to get this all to work together. As it happens, just the js
code isn’t enough to make a wct
plugin. You also need a line in a package.json
somewhere. This is documented in the readme adding this was enough:
{
// ...
"wct-plugin": {
"cli-options": {
}
},
// ...
}
Before you ask, no I have not tried removing the cli-options
item. It’s in the readme and that’s the limit of my confidence and the time I have to waste on this. Feel free to comment if it works for you without.
Of course package.json
means an npm module with its own directory (as far as I understand anyway, I’ll be happy to be shown wrong). I placed all this in :
wct-jenkins
├── lib
│ └── plugin.js
└── package.json
So you need 6 lines of boilerplate in two separate files in their own directory structure to write a line to the standard output. Javascript tooling is amazing.
I have no intention of publishing and maintaining npm packages. After a bit more digging I found that if I could put it in the toplevel directory of my project the module it could be picked up by adding this to my project’s package.json
’s devDependencies
section :
"wct-jenkins":"file:./wct-jenkins/",
Then I was able to load the module with a require at the top of the guplfile :
var wct_jenkins = require('wct-jenkins');
The saucelabs results finally got displayed in the jenkins build.
I still feel this was harder than it should have been but at least it works. Any suggestions to simplify the process by a npm,gulp,wct,js specialist are more than welcome.
]]>~/.gitconfig
:
git currrent-tag
looks up the latest tag in the repositorygit previous-tag
looks up the tag immediately before the latest tag in the repositorygit changelog
displays only the changes between these two tagsThe code for the aliases:
[alias]
current-tag= describe --abbrev=0 --tags
previous-tag= "!sh -c 'git describe --abbrev=0 --tags $(git current-tag)^'"
changelog = "!sh -c 'git --no-pager lg --first-parent $(git previous-tag)..$(git current-tag)'"
In this article I am going to detail an SBT combo allowing for SHA-1 based continuous delivery to an integration environment. The combo then allows to easily promote from this integration environment to QA, PreProd and Production platforms, creating a human understandable version in the process.
We start from a very basic play project with the following structure
.
├── app
│ └── eu
│ └── byjean
│ └── Health.scala
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ └── play.sbt
└── test
└── resources
The first piece of the combo is to use the sbt-buildinfo plugin to encode the project version in the generated artifact.
To add the build info plugin we will create a buildinfo.sbt
in the project directory with the following content (feel free to change version number to upgrade to the latest release)
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.4.0")
The tree now looks like
.
├── app
│ └── eu
│ └── byjean
│ └── Health.scala
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ ├── buildinfo.sbt
│ └── play.sbt
└── test
└── resources
Then configure your build to use it by changing build.sbt to look like
lazy val `ultimate-build` = (project in file(".")).enablePlugins(PlayScala, BuildInfoPlugin)
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion)
buildInfoPackage := "eu.byjean"
This will create an object called BuildInfo
in the configured package. Using this object we can create a useful little endpoint in our app : GET /health
.
We need to implement the Health#check
method :
val isoDateTimeWrites = new Writes[org.joda.time.DateTime] {
def writes(d: org.joda.time.DateTime): JsValue = JsString(d.toString(ISODateTimeFormat.dateTime()))
}
def check=Action { request =>
val json = Json.obj(
"version" -> BuildInfo.version,
"timestamp" -> Json.toJson(DateTime.now())(isoDateTimeWrites),
"reverse" -> routes.Health.check().absoluteURL(secure = true)(request)
)
Ok(json)
}
Is a good start. When calling this endpoint we get a small json payload with the version of the project:
$> http :9000/health
HTTP/1.1 200 OK
Content-Length: 112
Content-Type: application/json; charset=utf-8
Date: Fri, 10 Jul 2015 16:07:40 GMT
{
"reverse": "https://localhost:9000/health",
"timestamp": "2015-07-10T18:07:40.594+02:00",
"version": "0.1-SNAPSHOT"
}
This call can be extended as the application grows. I usually add checks on external system availability, making the service return a failure code (I usually choose 502) if a critical system used by the app stops responding.
Now that we can display our own version, let’s customize it.
sbt-git is a very useful plugin, it will provide you with a nice prompt showing git information right there in sbt. It can also derive the version of the project from the git history in various ways.
To enable it create a git.sbt
file in the project
directory with the following content (again check for newer versions):
addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.8.5")
Your project tree should now look like
.
├── app
│ └── eu
│ └── byjean
│ └── Health.scala
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ ├── buildinfo.sbt
│ ├── git.sbt
│ └── play.sbt
└── test
└── resources
We need to enable at least the GitVersioning
plugin, in my sample I also activate the GitBranchPrompt
which I find very useful.
Change your build.sbt accordingly:
lazy val `ultimate-build` = (project in file(".")).enablePlugins(PlayScala, BuildInfoPlugin, GitVersioning, GitBranchPrompt)
enables both plugins.
Now we can configure the versioning system. We need to choose a versioning scheme which is compatible with both SHA-1 based versioning for developers and semantic versioning for stakeholders.
The default scheme in sbt-git looks at the project tags. The first to match the gitTagToVersionNumberSetting
is used to assign the version. If you tag
your app v1.0.1
it will pick it up, that commit associated to the tag will have the SBT version set to 1.0.1
, it you make local changes it will become 1.0.1-SNAPSHOT
.
Upon the next commit, the version reverts to the base version suffixed by the SHA-1. This is fine if you are manually handling version bumps but not so nice if you want to automate releases1.
This leads us to the second versioning scheme offered by sbt-git. This scheme simply uses the output of git describe
as version. It can be activated by adding the following to build.sbt
.
git.useGitDescribe := true
Using useGitDescribe
has a few shortcomings :
-SNAPSHOTS
Luckily the versioning scheme is pretty easy to extend to eliminate these problems. First, make the version start somewhere :
git.baseVersion := "0.0.0"
Now to avoid accidental versioning issue from non version related tags and enforce my -SNAPSHOT
rules :
val VersionRegex = "v([0-9]+.[0-9]+.[0-9]+)-?(.*)?".r
git.gitTagToVersionNumber := {
case VersionRegex(v,"") => Some(v)
case VersionRegex(v,"SNAPSHOT") => Some(s"$v-SNAPSHOT")
case VersionRegex(v,s) => Some(s"$v-$s-SNAPSHOT")
case _ => None
}
This scheme yields the following versions in order:
0.0.0-SNAPSHOT
0.0.0-xxxxx-SNAPSHOT
//with xxxxxx a SHA-11.0.0
// for a commit whose SHA-1 has been tagged with v1.0.01.0.0-2-yyyyy-SNAPSHOT
// for the second commit after the tagThese versions are compatible with both nexus rules if you deploy your binaries there and with semantic versioning rules while preserving SHA-1 information whenever it is necessary.
When releasing an application (as opposed to a library), it is beneficial to package it up and release the whole package. The sbt-native-packager makes it easy to target various kinds of packages zip, tarball, dmg, rpm, deb you name it and it will package it for you. Such packages make the lives of anyone who needs to handle operations around the application much easier.
In a play application, which is what I used for this example, the plugin is configured by default and the universal:packageBin
will produce a zip file of the project complete with a run script, all the jars, a config directory and a documentation directory with the scaladoc for the project. However the publish settings are left untouched and the package itself is not published.
Fortunately the plugin authors have that covered, adding the following line to your build will change the publish settings to add the binary package to the published artifacts:
import com.typesafe.sbt.packager.SettingsHelper._
publishTo := Some("temp" at "file:///tmp/repository")
makeDeploymentSettings(Universal, packageBin in Universal, "zip")
Here I choose to publish a zip, feel free to adjust that to your needs with the help of the documentation
The next step to the ultimate sbt build is to add the sbt-release plugin. As for the other plugins, create a release.sbt
file in your project directory with the following content:
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0")
Your project tree should then look like this :
.
├── app
│ └── eu
│ └── byjean
│ └── Health.scala
├── build.sbt
├── conf
│ ├── application.conf
│ ├── logback.xml
│ └── routes
├── project
│ ├── build.properties
│ ├── buildinfo.sbt
│ ├── git.sbt
│ ├── play.sbt
│ └── release.sbt
└── test
└── resources
Now the plugin is present, lets configure it so it plays nice with our versioning scheme. By default the sbt-release plugin behaves kind of like the maven release plugin. It will :
with-defaults
argument is used).All these steps are here to ensure a repeatable build. I think it lacks a test run with the release version applied to be an exact match for the maven release process. In our case though where the version is fully derived from the VCS, this is slightly overkill.
With our setup, if we want to be able to repeat a specific version build all we have to do is checkout the corresponding tag which will automatically set the version to the correct value. Additionally, writing the version to an SBT file will kill the SHA-1 based versioning scheme we were using.
Once again the plugin author made it easy to change the release steps so we can customize our build as we want. Here is the sequence I use :
with-defaults
argument is used).This way we do run the tests with the actual release version (some applications have tests which depend on the application version).
Tagging the release ensures we can repeat the build once the artifacts are published and the changes are pushed. If anything bad happens before the last step, just delete the local tag if it was created and you are back to square one. No more messing with files to propagate the version.
The first thing we need is to redefine the steps to set the release and next development versions to avoid writing to the version file:
import sbtrelease._
// we hide the existing definition for setReleaseVersion to replace it with our own
import sbtrelease.ReleaseStateTransformations.{setReleaseVersion=>_,_}
def setVersionOnly(selectVersion: Versions => String): ReleaseStep = { st: State =>
val vs = st.get(ReleaseKeys.versions).getOrElse(sys.error("No versions are set! Was this release part executed before inquireVersions?"))
val selected = selectVersion(vs)
st.log.info("Setting version to '%s'." format selected)
val useGlobal =Project.extract(st).get(releaseUseGlobalVersion)
val versionStr = (if (useGlobal) globalVersionString else versionString) format selected
reapply(Seq(
if (useGlobal) version in ThisBuild := selected
else version := selected
), st)
}
lazy val setReleaseVersion: ReleaseStep = setVersionOnly(_._1)
Next we need to change slightly the way the release version is computed. Sbt-git derives the version number directly from the tag which means our snapshot builds numbered 1.0.0-x-gyyyyyy-SNAPSHOT
is actually destined to be released as 1.0.1
not as 1.0.0
(since the 1.0.0
is derived from an existing tag). We need to change the release version computation logic slightly :
releaseVersion <<= (releaseVersionBump)( bumper=>{
ver => Version(ver)
.map(_.withoutQualifier)
.map(_.bump(bumper).string).getOrElse(versionFormatError)
})
Finally you need to decide if you want to push the default build artifacts (usually a jar) which is the right choice for a library, or the packaged artifacts which is most likely what you want for an application. Then you can override the releaseProcess
to match your need.
Below is a sample release process for an application, to switch it to a library you would uncomment the publishArtifacts and comment the next line which is used to publish the package from the Universal namespace.
releaseProcess := Seq(
checkSnapshotDependencies,
inquireVersions,
setReleaseVersion,
runTest,
tagRelease,
// publishArtifacts,
ReleaseStep(releaseStepTask(publish in Universal)),
pushChanges
)
With all this, releasing a bugfix can be done with the following command line :
sbt release with-defaults
Changing the default version bump from bugfix to minor is just a matter of changing releaseVersionBump
to the appropriate settings for you.
In the comments, Loki mentionned that you may want to add the following step to your release process:
releaseStepCommand(ExtraReleaseCommands.initialVcsChecksCommand),
This step will ensure that you don’t have uncommitted changes in your workspace. Having uncommitted changes while releasing would break the repeatable build. A checkout of the tag in a fresh clone of the repository would not have the uncomitted changes and might result in a different binary. I didn’t mention this initially because our process is to trigger releases on a CI server which starts by doing a clone from scratch in a temporary workspace. If your release process isn’t as strict, adding the initialVcsChecksCommand
step at the beggining of your release process is definitely a good idea.
We now have an SBT build which delegates versioning to git, packages applications as a deployable zip file, tags the release automatically and publishes it to your company’s artifact repository before pushing the tag on your remote git server in a single command. At the same time, every package built is versioned with the SHA-1 of the HEAD which was checked out to build it. You will find the complete project’s code on github.
You might wonder where the buildinfo plugin I introduced initially comes in ? Having the binary package be able to report its own version enables relatively simple package promotion schemes.
Imagine the development package is continuously built and deployed to an integration platform, promoting a build to the QA platform is simple : fetch the version from the deployed instance in integration, parse it to extract the SHA-1, check it out and tag the release.
Promoting from QA to pre-prod or prod is even simpler : fetch the version from the QA platform, fetch the deployable package from the artifacts repository and deploy it to the target environment. But that’s a story for another post.
This might change in the future, follow sbt-git#93 for more ↩
I found it quite painful as I didn’t see an obvious way to actually create a test without already having the corresponding class. I was able to create an empty class but it would force me to write a lot of boiler plate manually (all the test annotations and all the static imports manually).
I happened to discuss this with Yann Cebron from Jetbrains at Devoxx France who showed me a neat trick using file templates2.
If you go to the project tool window, select a package in the test folder and try to create a new file with the default configuration, you should see something like this :
That’s a lot of templates and there is nothing to create a Junit4 test, lets create one.
Select edit file templates...
and you will reach a window with a green +
sign click that, name it Junit4
and paste the following code (or your own variation thereof) :
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
import org.junit.Test;
import static org.junit.Assert.*;
public class ${NAME} {
@Test
public void test_${NAME}() throws Exception {
}
}
Now save this and go back to the project tool window, select a package in the test folder and try to create a new file again. This time you should see the Junit4
template.
This is nice and nifty but it can still be improved : there is an Intellij action called from template
(you can find it with find action...
which is cmd+shift+a on mac)
Using this action only custom templates applicable will be displated, in a test directory for a mixed scala/java project it will show a much smaller menu making it even easier to create your test first.
You can find the exercise at http://github.com/devlyon/mixter. ↩
This feature has been broken in a few Intellij builds and the user templates wouldn’t show up in the menu, see the corresponding issue on youtrack. ↩
Future[Option[T]]
.
L’application du principe de séparation des responsabilité a permis une première amélioration, l’utilisation d’exceptions métier est une solution dans certains cas mais sacrifie une partie de l’information de typage.
Finalement l’utilisation d’un type ad hoc composant les propriétés d’une Future
et d’une Option
s’est avéré être un parfait complément à la séparation initiale.
Le seul défaut de cette dernière approche est le besoin de maintenir ce type et de construire un nouveau type pour chaque nouvelle composition: FutureO
(Future
et Option
), FutureL
(Future
et List
), etc. Ces types ne sont pas spécifiques à un projet et idéalement devraient être extraits dans une bibliothèque. Il s’avère qu’une telle bibliothèque existe déjà.
Dans cet article, le dernier de cette série, je vous propose un refactoring utilisant les MonadTransformer
de Scalaz 7.x.
Code —-
Le code pour cet article est disponible sur github sous le tag futureOption/4-optionT_scalaz
et dans la branche futureOption
MonadTransformer
Je n’ai pas l’intention de me prêter au périlleux exercice qui consiste à essayer de définir ce que représente une Monad
, d’autres s’y sont attelés et une recherche google vous fournira toute l’information que vous pourriez vouloir (et sans doute plus).
Les types monadiques ont des propriétés intéressantes du point de vue de la composition. C’est parceque Future
et Option
peuvent être considérés comme des types monadiques que nous avons pu les composer pour créer FutureO
.
D’un point de vue purement pragmatique1, on peut considérer que tout type qui respecte le contrat logique suivant est monadique :
Monad[A]{
def this(a:A):Monad[A] //=> il faut un constructeur pour le type concret
def map[B](f:A=>B):Monad[B]
def flatMap[B](f:A=>Monad[B]):Monad[B]
def filter(f:A=>Boolean):Monad[A]
}
Dans la bibliothèque standard de scala, les types Option
, Future
, Try
, String
, Map
, Seq
, et bien d’autres peuvent donc être considérés comme monadiques. Scalaz propose des alternatives monadiques à certains types de la bibliothèque standard qui ne sont pas compatibles avec l’interface (comme Either par exemple).
Si deux types sont compatibles avec le contrat ci-dessus, il est possible d’implémenter un MonadTransformer
pour ces deux types. L’implémentation d’un tel type n’est pas forcément triviale, heureusement Scalaz propose déjà un grand nombre d’implémentations. Celle qui nous intéresse et qui permet de composer Option
et Future
s’appelle OptionT
. Il permet en réalité de composer Option
avec n’importe quelle type monadique.
Nous allons remplacer le type FutureO
du précédent article par OptionT[Future, Article]
, commençons par la signature d’ArticleRepository
trait ArticleRepository {
def findById(id: String): OptionT[Future,Article]
}
Mécaniquement, nous sommes amenés à changer l’implémentation pour que le code compile. Je conserve volontairement la variable articleFO
pour continuer de mettre en évidence les types intermédiaires.
class FakeArticleRepository extends ArticleRepository {
override def findById(id: String): OptionT[Future,Article] = {
val articleFO:Future[Option[Article]] = id match {
case "0" => Future.successful( Option(Article("0", "good article", 10.0) ))
case id @ "1" => Future.successful(None)
case "2" => Future.failed(new java.io.IOException("Connection lost !!") )
}
OptionT(articleFO)
}
}
Dans ArticleREST
le résultat de l’appel à findById
est passé à la méthode ResultMapper#toJsonResult
. La signature de cette dernière doit donc changer pour accepter une instance de Option[Future,A]
.
def toJsonResult[A](subjectFuture: OptionT[Future,A][A])
(onNotFound : => Result,
onError:PartialFunction[Throwable, Result]=internalServerErrorHandler)
(implicit writer: Writes[A]): Future[Result] = {
subjectFuture.map(jsonOk).getOrElse(onNotFound).recover(onError)
}
Le code d’ArticleREST
n’a pas besoin de changer et la gestion des erreurs est la même que pour FutureO
, par contre il manque une toute petite brique pour que le programme fonctionne :
[error] /Users/jean/dev/sdev/src/articles/futureOptions/app/mvc/ResultMapper.scala:41: could not find implicit value for parameter F: scalaz.Functor[scala.concurrent.Future]
[error] subjectFuture.map(jsonOk).getOrElse(onNotFound).recover(onError)
[error] ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed
[error] Total time: 6 s, completed 19 mai 2015 18:06:04
Il manque un paramètre implicite permettant de prouver à Scalaz qu’une Future est bien un Functor. Si vous utilisez une version de Scalaz supérieur à 7.1.x, il suffit d’ajouter l’import
import scalaz.std.scalaFuture
pour les versions précédentes ou si vous souhaitez limiter au maximum le nombre d’implicites dans le scope, la définition suivante suffit :
implicit val futureFunctor = new Functor[Future] {
override def map[A, B](fa: Future[A])(f: (A) => B): Future[B] = fa.map(f)
}
Dans les deux cas, l’ExecutionContext présent dans le scope implicite sera utilisé pour construire la preuve que Future
est bien un Functor
.
L’utilisation du type OptionT[Future,Article]
offre les même avantages que l’utilisation de FutureO, et va bien au-delà en généralisant cette composition à tous les types qui offrent un comportement monadique. Avantage supplémentaire, il n’est plus nécessaire de maintenir sa propre bibliothèque de types “pré composés”, ceux-ci sont accessible directement par Scalaz.
Il est tout a fait possible de commencer par développer quelques types ad hoc puis de les remplacer par des types de Scalaz en utilisant des alias de types et quelques imports. Ainsi lorsque le coût de maintenance ou le degré de répétition deviennent trop importants ou que Scalaz est importé pour d’autres raisons la migration se fait avec un minimum de modifications.
Scalaz souffre d’une image négative, l’utilisation d’opérateurs unicodes, l’utilisation massive d’implicites et la personnalité corrosive de certains de ses défenseurs y ont largement contribué.
Cependant, il est maintenant possible d’utiliser les types que propose la bibliothèque de façon selective ce qui fait diminuer le coût d’entrée de cette lib dans un projet.
La véritable définition est mathématique et très formelle. Elle entraine régulièrement des débats sans fin à propos de types qui ne respectent pas tout à fait les lois monadiques (par exemple Future et Try à cause des exceptions). Certes ces types ne sont pas parfaitement purs et il est possible qu’il existe des implémentations pures mais du point de vue de l’utilisateur ça n’a pas tant d’importance. ↩
Future[Option[T]]
en appliquant le principe de séparation des responsabilité et en utilisant des exceptions métier.
Ces deux approches, relativement simples ont toutes deux montré des limites: la première mélange le traitement de cas d’erreurs avec le traitement de cas normaux, la seconde perd de l’information au niveau du système de type et nécessite une connaissance de précise de l’implémentation ou une documentation détaillée pour pouvoir être correctement manipulée.
Dans cet article je vous propose d’explorer une piste proposée par la programmation fonctionnelle: la composition du type Future et du type Option dans un type ad hoc.
Code —-
Le code pour cet articles est disponible sur github sous le tag futureOption/3-type_ad_hoc
et dans la branche futureOption
FutureO
Il s’agit de créer un type représentant spécifiquement la composition d’une future et d’une option,qui conserve la sémantique de ces deux types et qui soit compatible avec une expression for.
Cette idée n’est pas nouvelle, Edofic et Loic ont tout deux proposé une implémentation à laquelle j’ai ajouté le withFilter
nécessaire pour supporter les conditions de garde dans les expressions for ainsi que le getOrElse qui permet de fournir à l’option une valeur par défaut:
import scala.concurrent.{Future, ExecutionContext}
case class FutureO[+A](future: Future[Option[A]]) extends AnyVal {
def flatMap[B](f: A => FutureO[B])(implicit ec: ExecutionContext): FutureO[B] = {
val newFuture = future.flatMap{
case Some(a) => f(a).future
case None => Future.successful(None)
}
FutureO(newFuture)
}
def map[B](f: A => B)(implicit ec: ExecutionContext): FutureO[B] =
FutureO(future.map(option => option map f))
def filter(p: A => Boolean)(implicit ec: ExecutionContext): FutureO[A] =
FutureO(future.map(_.filter(p)))
final def withFilter(p: A => Boolean)(implicit executor: ExecutionContext): FutureO[A] =
filter(p)(executor)
def getOrElse[AA >: A](default: AA)(implicit executor: ExecutionContext):Future[AA] =
future.map(_.getOrElse(default))
}
FutureO
La première étape pour utiliser notre type FutureO
est de changer la signature d’ArticleRepository
trait ArticleRepository {
def findById(id: String): FutureO[Article]
}
Mécaniquement, nous sommes amenés à changer notre implémentation pour que le code compile. J’en profite pour introduire la variable articleFO
pour mettre en évidence les types intermédiaires.
class FakeArticleRepository extends ArticleRepository {
override def findById(id: String): FutureO[Article] = {
val articleFO:Future[Option[Article]] = id match {
case "0" => Future.successful( Option(Article("0", "good article", 10.0) ))
case id @ "1" => Future.successful(None)
case "2" => Future.failed(new java.io.IOException("Connection lost !!") )
}
FutureO(articleFO)
}
}
Dans ArticleREST
le résultat de l’appel à findById
est passé à la méthode ResultMapper#toJsonResult
. La signature de cette dernière doit donc changer pour accepter une instance de FutureO
.
Ce changement nous force également à changer la gestion d’erreur pour le cas NotFound
. Nous n’avons plus d’exceptions donc la signature PartialFunction[Throwable,Result]
ne peut plus s’appliquer. Nous la remplaçons par une valeur de type Result ce qui permet au code appelant de continuer de controler le resultat HTTP effectivement renvoyé au client du service. Voici la nouvelle implémentation :
def toJsonResult[A](subjectFuture: FutureO[A])
(onNotFound : => Result,
onError:PartialFunction[Throwable, Result]=internalServerErrorHandler)
(implicit writer: Writes[A]): Future[Result] = {
subjectFuture.map(jsonOk).getOrElse(onNotFound).recover(onError)
}
def jsonNotFound(msg: String) = NotFound(Json.obj("reason" -> msg))
Le code appelant ne change que très peu, il suffit d’enlever le case
de la PartialFunction
.
def get(id: String) = Action.async { implicit request =>
val articleFuture = articleRepository.findById(id)
mvc.ResultMapper.toJsonResult(articleFuture)(
mvc.ResultMapper.jsonNotFound(s"no article for $id")
)
}
L’utilisation d’un type ad-hoc nous a permis de séparer le traitement logique du succès de celui de l’absence de valeur. Cette dernière bénéficie tout de même d’un traitement spécifique par rapport aux autres erreurs qui corresponds assez bien à la réalité métier de l’application. L’absence de valeur n’est pas une erreur technique mais une erreur métier.
Le type ad-hoc permet de composer facilement divers appels de service dans des expressions-for comme dans le cas d’utilisation d’exceptions, mais au contraire des exceptions, le cas d’erreur métier lié à l’absence de la valeur n’est pas dissimulée dans les signatures de méthodes.
Le seul inconvénient de cette approche est de devoir créer et maintenir les différents types représentant les compositions ad-hoc utilisées dans le programme. Cette charge relativement faible peut devenir importante sur un projet de grande envergure.
]]>Future[Option[T]]
sur la lisibilité du code. L’extraction d’un ResultMapper
et l’utilisation du pattern matching ont permis de séparer les différentes problématiques du code initial.
En conclusion je faisait remarquer que la répartition des traitements succès/erreur dans le mapper était suspecte. Elle devient problématique lorsque vous voulez coordonner plusieurs appels à des services ayant ce type de signature, le happy path est alors pollué par l’extraction des valeurs dans les couches successives de type conteneurs.
Je vais maintenant montrer que l’utilisation d’exceptions métier est une façon de regrouper les cas d’erreurs dans le même bloc et de conserver un happy path simple.
Code —-
Le code pour cet articles est disponible sur github sous le tag futureOption/2-business_exception
et dans la branche futureOption
ArticleNotFound
Notez que nous n’utilisons pas les exceptions métier en tant qu’exceptions mais en tant que valeurs. Elle ne sont pas utilisées avec throw
et ne contournent donc pas le flot d’exécution normal du programme.
Pour marquer la différence entre les exceptions métier du projet et les exceptions classiques, créons un trait racine:
package support
import scala.util.control.NoStackTrace
trait BusinessException extends RuntimeException with NoStackTrace
Notez l’utilisation du trait NoStackTrace, fourni par la librairie standard de scala. Il permet d’éviter la coûteuse construction de la stacktrace lors de la création d’un objet à partir d’une classe qui hérite de java.lang.Throwable
.
object ArticleRepository{
case class ArticleNotFound(id:String) extends BusinessException
}
Dans le code actuel, l’exception ArticleNotFound n’a de sens métier qu’au niveau du repository, elle est donc définie dans l’objet compagnon de celui-ci.
Nous avons maintenant une valeur qui peut être placée dans un Future.failed
et qui dénote de l’absence d’un article. Nous pouvons donc changer la signature du repository:
trait ArticleRepository {
def findById(id: String): Future[Article]
}
Notez que cette signature est celle qu’expose notre façade pour le repository. Dans le cadre de l’article nous contrôlons également l’implémentation, mais dans le cas contraire c’est la façade qui se chargerait de faire l’adaptation entre la signature source et celle que nous désirons avoir (et, oui, il faut toujours encapsuler les services externes utilisé dans notre code ;) ).
Changer la signature du repository nous oblige à corriger les erreurs de compilations. Tout d’abord le FakeArticleRepository
doit implémenter la nouvelle signature.
class FakeArticleRepository extends ArticleRepository {
def findById(id: String): Future[Article] = {
id match {
case "0" => Future.successful( Article("0", "good article", 10.0) )
case id @ "1" => Future.failed( ArticleRepository.ArticleNotFound(id) )
case "2" => Future.failed( new java.io.IOException("Connection lost !!") )
}
}
}
Il suffit de changer le cas de l’id 1, en remplaçant Future.sucessful(None)
par Future.failed( ArticleRepository.ArticleNotFound(id) )
Reste à corriger la signature de la méthode du ResultMapper
qui acceptait une valeur de Future[Option[T]]
et doit maintenant accepter une valeur de Future[T]
. Dans un projet plus riche, il pourrait être utile de conserver les deux.
def toJsonResult[A](subjectFuture: Future[A], noneMsg: => String = "NotFound")
(implicit writer: Writes[A]): Future[Result] = {
subjectFuture.map {
case subject => jsonOk(subject)
}.recover {
case ArticleNotFound(id) => jsonNotfound(noneMsg)
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
Nous pouvons déplacer le traitement du cas où l’article n’existe pas dans le bloc recover (ce qui était la raison principale de cette réécriture). Le traitement du cas normal est donc séparé du traitement des cas d’erreur.
Le code compile et les tests repassent, nous allons pouvoir nettoyer un peu.
Dans le contrôlleur, nous avons une variable intermédiaire dont le nom est articleOptionFuture
ce qui n’a plus de sens puisque le type Option
n’est plus utilisé. Un petit re-nomage plus tard et le code devient :
class ArticlesREST(val articleRepository: ArticleRepository) extends Controller {
def get(id: String) = Action.async { implicit request =>
val articleFuture = articleRepository.findById(id)
mvc.ResultMapper.toJsonResult(articleFuture, s"no article for $id")
}
}
Rester à nettoyer ResultMapper
, problématique plus conséquente:
def jsonOk[A](subject: A)(implicit writer: Writes[A]) = Ok(Json.toJson(subject))
def toJsonResult[A](subjectFuture: Future[A], noneMsg: => String = "NotFound")
(implicit writer: Writes[A]): Future[Result] = {
subjectFuture.map(jsonOk(_)).recover {
case ArticleNotFound(id) => jsonNotfound(noneMsg)
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
La signature de jsonOk impose l’utilisation des parenthèses et du _
en raison de ses deux listes d’arguments. Le compilateur scala, ne permet pas de mettre la liste des arguments implicites en premier, ce qui permettrait de transformer notre méthode en fonction. Il est possible de contourner cette limitation de la façon suivante :
def jsonOk[A:Writes]: A => Result = (subject: A)=> Ok(Json.toJson(subject))
def toJsonResult[A](subjectFuture: Future[A], noneMsg: => String = "NotFound")
(implicit writer: Writes[A]): Future[Result] = {
subjectFuture.map(jsonOk).recover {
case ArticleNotFound(id) => jsonNotfound(noneMsg)
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
Il reste cependant un problème de taille : notre solution actuelle introduit une dépendance directe entre le ResultMapper et le repository des articles.
On pourrait définir un trait NotFoundException
dans le package support où se trouve BusinessException
.
trait NotFoundException extends BusinessException
mixer ce trait dans ArticleNotFound
object ArticleRepository{
case class ArticleNotFound(id:String) extends NotFoundException
}
et écrire toJsonResult
de la façon suivante :
def jsonOk[A:Writes]: A => Result = (subject: A)=> Ok(Json.toJson(subject))
def toJsonResult[A](subjectFuture: Future[A], noneMsg: => String = "NotFound")
(implicit writer: Writes[A]): Future[Result] = {
subjectFuture.map(jsonOk).recover {
case notFound:NotFoundException => jsonNotfound(noneMsg)
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
Cette approche est assez restrictive, elle implique qu’une exception “NotFound” renverra nécessairement un code 404 avec un message. Cette réponse est peut être valide pour la plupart des APIs mais n’est pas nécessairement juste. Scala nous permet de faire beaucoup mieux !
Imaginons que la signature de toJsonResult
soit la suivante :
def toJsonResult[A](subjectFuture: Future[A])
(onError: PartialFunction[Throwable, Result])
(implicit writer: Writes[A]): Future[Result]
L’implémentation de ArticlesREST pourrait alors passer la gestion d’erreur correcte de la façon suivante :
import mvc.ResultMapper
class ArticlesREST(val articleRepository: ArticleRepository) extends Controller {
def get(id: String) = Action.async { implicit request =>
val articleFuture = articleRepository.findById(id)
toJsonResult(articleFuture){
case ArticleNotFound(articleId) => jsonNotfound(s"no article for $articleId")
}
}
}
En fournissant un handler par défaut nous pourrions conserver exactement la même implémentation, tout en offrant aux services qui le souhaitent la possibilité de gérer eux même tout ou partie des erreurs.
L’implémentation du ResultMapper pourrait proposer ces handlers par défaut :
def notFoundHandler(noneMsg: => String = "NotFound"): PartialFunction[Throwable, Result] = {
case notFound: NotFoundException=> jsonNotfound(noneMsg)
}
val internalServerErrorHandler: PartialFunction[Throwable, Result] = {
case e: Exception=> jsonInternalServerError(e.getMessage, e)
}
Et l’implémentation de toJsonResult
devient alors :
def toJsonResult[A](subjectFuture: Future[A])
(onError: PartialFunction[Throwable, Result] = notFoundHandler() )
(implicit writer: Writes[A]): Future[Result] = {
val defaultHandler = notFoundHandler() orElse internalServerErrorHandler
subjectFuture.map(jsonOk).recover(onError orElse defaultHandler)
}
Une dernière amélioration pourrait être de faire de ResultMapper un trait qui soit mixé dans le controller plutôt que de l’exposer sous la forme d’un objet exterieur.
L’utilisation d’exceptions métier nous a permis de regrouper le traitement d’erreur et proposer des traitements par défaut tout en offrant la possibilité d’utiliser un traitement spécifique. Utiliser les expressions-for
sur les valeurs de retour des services permet de les composer facilement sans avoir N niveaux de conteneurs à traverser pour atteindre les valeurs à manipuler.
Cette approche a cependant un défaut important par rapport à la précédente : les types des services ne sont plus auto-suffisants. Une documentation des erreurs possibles et des exceptions correspondantes sera indispensable pour une bonne utilisation des services. Bien que notre utilisation des exceptions ne casse pas le flot du programme sur le plan technique, elle dissimule des informations importantes qui ne peuvent être retrouvées que par de la documentation.
]]>Futures[T]
. Avec une telle API, on se retrouve vite avec des signatures de type Future[Option[T]]
. Transformer proprement un tel résultat vers des réponses HTTP n’est pas forcément évident et peut amener de la duplication même dans des cas simples. Dans cet article nous allons voir une façon d’éviter ce problème.
Code —-
Le code pour cet articles est disponible sur github sous le tag futureOption/1-separation_responsabilite
et dans la branche futureOption
Partons d’un exemple simple et développons un micro-service qui expose des Articles
au format JSON. Il ne permet que de lire le détail d’un article à partir de son identifiant en accédant à la ressource suivante :
GET /article/:id
Un article est un élément simple défini comme suit :
case class Article(id: String, name: String, price: BigDecimal)
object Article {
implicit val jsonFormat = play.api.libs.json.Json.format[Article]
}
Afin de lire un article depuis notre base de donnée, nous disposons d’un Repository
asynchrone dont l’interface est la suivante :
trait ArticleRepository {
def findById(id: String): Future[Option[Article]]
}
Partons d’une implémentation naïve de la ressource Play :
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json.Json
object ArticleController extends play.api.mvc.Controller {
val articleRepository: ArticleRepository = ArticleRepositoryImpl
def get(id: String) = Action.async { implicit request =>
val articleOptionFuture = articleRepository.findById(id)
articleOptionFuture.map(articleOption =>
articleOption.map(article =>
Ok(Json.toJson(article))
).getOrElse(NotFound(Json.obj("reason" -> s"no article for $id")))
)
}
}
Concentrons nous sur deux points :
articleOptionFuture
n’est pas géré. Si la Future
est une Failure
elle va remonter dans le framework. Celui-ci utilise un handler par défaut qui retourne une erreur 500 avec un contenu de type text/html
en cas de Failure
, quelque soit le type de contenu demandé par le client. Ici nous voudrions rester cohérents et toujours renvoyer un contenu de type application/json
.Il est facile de corriger le premier point en interceptant la Failure
pour renvoyer un message d’erreur JSON, toujours avec un code 500 :
object ArticleController extends play.api.mvc.Controller {
val articleRepository: ArticleRepository = ArticleRepositoryImpl
def exception2Location(exception: Exception): Option[String] =
Option(exception.getStackTrace)
.flatMap(_.headOption)
.map(_.toString)
.getOrElse("unknown")
def jsonInternalServerError(msg: String, cause: Exception) = {
val jsonMsg = Json.obj(
"reason" -> msg,
"location" -> exception2Location(cause)
)
InternalServerError(jsonMsg)
}
def get(id: String) = Action.async { implicit request =>
val articleOptionFuture = articleRepository.findById(id)
articleOptionFuture.map(articleOption =>
articleOption.map(article =>
Ok(Json.toJson(article))
).getOrElse(NotFound(Json.obj("reason" -> s"no article for $id")))
).recover {
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
}
Nous avons maintenant un service qui renvoie du JSON même en cas d’erreur, tout en conservant la sémantique des codes de retour HTTP.
Nous avons dû extraire des méthodes pour conserver un minimum de libilité. Ces méthodes n’ont pas l’air d’être spécifque à notre controller : elles ne manipulent aucunement les articles. Il est probables qu’elles ne soient pas à leur place, mais nous y reviendront plus tard.
(Si vous êtes horrifés que je fasse du refactoring sans tests, rassurez-vous j’ai des tests mais ils ne sont pas l’objet de cet article)
Le comportement de la méthode get
est maintenant correct. Cependant la lecture reste difficile :
Procédons à un premier refactoring pour séparer la notion de mapping d’une valeur vers un résultat HTTP :
object ArticleController extends play.api.mvc.Controller {
val articleRepository: ArticleRepository = ArticleRepositoryImpl
def jsonOk(article:Article)=Ok(Json.toJson(article))
def jsonNotfound(msg: String) = NotFound(Json.obj("reason" -> msg))
def exception2Location(exception: Exception): Option[String] =
Option(exception.getStackTrace)
.flatMap(_.headOption)
.map(_.toString)
.getOrElse("unknown")
def jsonInternalServerError(msg: String, cause: Exception) = {
val jsonMsg = Json.obj(
"reason" -> msg,
"location" -> exception2Location(cause)
)
InternalServerError(jsonMsg)
}
def get(id: String) = Action.async { implicit request =>
val articleOptionFuture = articleRepository.findById(id)
articleOptionFuture.map( articleOption =>
articleOption.map( article => jsonOk(article)
).getOrElse( jsonNotfound(s"no article for $id") )
).recover {
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
}
Ce refactoring améliore un peu les choses mais get
reste difficile à lire.
La syntaxe abbrégée de scala pour les fonctions de mapping n’aide pas vraiment :
def get(id: String) = Action.async { implicit request =>
val articleOptionFuture = articleRepository.findById(id)
articleOptionFuture.map(
_.map(jsonOk).getOrElse(jsonNotfound(s"no article for $id"))
).recover {
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
Une autre alternative est le pattern matching :
def get(id: String) = Action.async { implicit request =>
val articleOptionFuture = articleRepository.findById(id)
articleOptionFuture.map {
case Some(article) => jsonOk(article)
case None => jsonNotfound(s"no article for $id")
}.recover {
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
Je trouve cette forme plus facile à lire. Il saute aux yeux que la fonction gère 1 cas de succès et 2 cas d’erreurs. les cas d’erreur ne sont pas gérés ensemble c’est l’une des limites de ce refactoring.
C’est regrettable car les 2 cas d’erreur ne dépendent pas vraiment de la resource, ils sont assez génériques. Nous pouvons tout de même extraire la responsabilité de transformer un résultat (succès ou échec) en JSON dans une classe spécialisée :
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json.Json
import scala.concurrent.Future
object JsonResultMapper extends Results {
import play.api.libs.json.Writes
def jsonOk[A](subject: A)(implicit writer: Writes[A]) = Ok(Json.toJson(subject))
def jsonNotfound(msg: String) = NotFound(Json.obj("reason" -> msg))
def exception2Location(exception: Exception): String =
Option(exception.getStackTrace)
.flatMap(_.headOption)
.map(_.toString)
.getOrElse("unknown")
def jsonInternalServerError(msg: String, cause: Exception) = {
val jsonMsg = Json.obj(
"reason" -> msg,
"location" -> exception2Location(cause)
)
InternalServerError(jsonMsg)
}
def toJsonResult[A](subjectOptionFuture: Future[Option[A]],noneMsg: => String = "NotFound")
(implicit writer: Writes[A]): Future[SimpleResult] = {
subjectOptionFuture.map {
case Some(subject) => jsonOk(subject)
case None => jsonNotfound(noneMsg)
}.recover {
case e: Exception => jsonInternalServerError(e.getMessage, e)
}
}
}
et notre ressource devient alors :
object ArticleController extends play.api.mvc.Controller {
val articleRepository: ArticleRepository = ArticleRepositoryImpl
def get(id: String) = Action.async { implicit request =>
val articleOptionFuture = articleRepository.findById(id)
JsonResultMapper.toJsonResult(articleOptionFuture, s"no article for $id")
}
}
Nous avons amélioré notre code initial, extrait une fonctionnalité transverse et fortement gagné en lisibilité dans la resource. Celle ci n’a désormais pour responsabilité que de coordonner le chargement de l’article et de demander la transformation en JSON au service correspondant. Dans le cas d’un appel plus complexe, on pourrait effectuer la validation du format d’entrée et extraire l’appel du repository dans un service.
Cependant l’implementation toJsonResult
du JsonResultMapper
restent suspectes. Les cas d’erreurs ne sont pas traités dans le même bloc logique et utiliser le pattern matching pour “cacher” l’imbrication des appels à map fonctionne mais laisse également à désirer.
Pourquoi Vagrant ——-
Cependant il y a un inconvénient à cette multiplication, certains outils ne s’installent pas facilement sur tous les environnements. Si l’on veut réduire la barrière à l’entrée sur un projet et amener les gens a essayer de nouveaux outils, il est préférable d’éliminer un maximum de barrières.
C’est là que Vagrant va nous aider, la personne qui “sait” comment installer va construire le fichier de définition de la VM, les autres lancent
git clone git://uberduper/project.git && vagrant up
puis vont boire un café pendant que vagrant leur prépare un environnement.
Nous avons déjà croisé le fichier de définition lors de la construction de la VM vide. Mais pour une VM vide nous ne nous étions pas penchés sur les directives de provisioning. Le fichier de définition peut en comporter plusieurs, chacune réfère a un type de provisionner particulier : file, shell, ansible, … Les types de provisionner sont décrits de façon extensive dans la documentation, nous allons regarder un peu plus en détail un provisioner shell.
Il faut commencer par le déclarer dans le fichier de définition :
PROVISION = "provisioning.sh"
PROVISION_ARGS = "vagrant"
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--cpuexecutioncap", "90", "--memory", "2048"]
end
config.vm.provision :shell do |s|
s.path = PROVISION
s.args = PROVISION_ARGS
end
config.vm.synced_folder ".", "/home/vagrant/website"
end
En tout début de fichier sont déclarées deux variables qui définissent le nom du script shell de provisionning et ses arguments, il n’est pas obligatoire de passer par des variables. Les lignes 15-17 déclarent un provisioner de type shell qui va donc appeller le script shell provisioning.sh avec l’argument ‘vagrant’
Notre fichier de définition dépends d’une image precise64
donc notre VM tournera sous ubuntu. Le script suivant effectue pour vous les tâches suivantes:
#!/usr/bin/env bash
USER=$1
apt-get update -y
apt-get upgrade -y
apt-get install -y python-software-properties
add-apt-repository -y ppa:webupd8team/java
add-apt-repository -y ppa:chris-lea/redis-server
apt-get update -y
echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
apt-get install -y curl git gdebi-core oracle-java8-installer redis-server
cp /etc/redis/redis.conf /etc/redis/redis.conf.default
cp /home/vagrant/dev/conf/redis-devoxxfr.conf /etc/redis/redis.conf
cd /tmp
wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.1.0.deb >/dev/null
gdebi -n elasticsearch-1.1.0.deb >/dev/null
wget http://dl.bintray.com/sbt/debian/sbt-0.13.2.deb >/dev/null
gdebi -n sbt-0.13.2.deb >/dev/null
service elasticsearch start
service redis start
cd /home/$USER/dev
su -l $USER -c "sbt update"
Construire une image vide ——- Crééz vous un dossier pour le projet et placez vous dedans:
mkdir website
cd website
Crééz un fichier nommé Vagrantfile
et ajoutez-y le contenu suivant:
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--cpuexecutioncap", "90", "--memory", "2048"]
end
config.vm.synced_folder ".", "/home/vagrant/website"
end
Maintenant vous avez un Vagrantfile
minimal, il va télécharger une image nommée precise64
depuis le Cloud ( en fait http://files.vagrantup.com/ ) et s’en servir pour créer une instance virtualbox headless avec 2Go de RAM. L’image precise64
est en fait une image contenant Ubuntu 12.04.4 (precise pangola) pré-installé.
Ce fichier vagrant va également configurer une synchronisation du dossier courant côté hôte (donc le dossier website créé plus haut) avec le dossier /home/vagrant/website
dans la VM. Par défaut le dossier courant est mappé sur /vagrant
dans la VM mais je préfère rester dans un sous dossier de $HOME
sur l’hôte comme sur la vm.
( plus d’info sur la configuration de l’instance virtualbox sous-jacente sur http://www.virtualbox.org/manual/ch08.html#vboxmanage-modifyvm )
Nous sommes maintenants prêts à démarrer notre VM:
vagrant up
Cela devrait vous afficher quelque chose comme :
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'precise64'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: vagrant_default_1396680997107_72702
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
default: Adapter 1: nat
==> default: Forwarding ports...
default: 22 => 2222 (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
default: SSH address: 127.0.0.1:2222
default: SSH username: vagrant
default: SSH auth method: private key
default: Error: Connection timeout. Retrying...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
default: The guest additions on this VM do not match the installed version of
default: VirtualBox! In most cases this is fine, but in rare cases it can
default: prevent things such as shared folders from working properly. If you see
default: shared folder errors, please make sure the guest additions within the
default: virtual machine match the version of VirtualBox you have installed on
default: your host and reload your VM.
default:
default: Guest Additions Version: 4.2.0
default: VirtualBox Version: 4.3
==> default: Mounting shared folders...
default: /vagrant => /private/tmp/vagrant
default: /home/vagrant/website => /private/tmp/vagrant
Bravo, votre VM est démarrée, il reste à s’y connecter. Lors du démarrag, vous avez peut-être remarqué les lignes:
==> default: Forwarding ports...
default: 22 => 2222 (adapter 1)
On se connecte donc en ssh sur la machine:
vagrant ssh
À l’issue de cette commande vous êtes dans un shell exécuté dans la machine virtuelle.
Welcome to Ubuntu 12.04 LTS (GNU/Linux 3.2.0-23-generic x86_64)
* Documentation: https://help.ubuntu.com/
Welcome to your Vagrant-built virtual machine.
Last login: Fri Sep 14 06:23:18 2012 from 10.0.2.2
vagrant@precise64:~$
Quittez ce shell, nous allons maintenant arrêter la VM:
vagrant halt
qui devrait vous afficher
==> default: Attempting graceful shutdown of VM...
Une dernière commande qui peut ête utile, supprimer la VM. Cela peut servir si vous décidez d’arrêter de travailler sur le projet ou si vous voulez recréer complètement la VM from scratch.
vagrant destroy
Cette commande va vous demander confirmation et si besoin arrêter la VM.
default: Are you sure you want to destroy the 'default' VM? [y/N] y
==> default: Destroying VM and associated drives...
Félicitations! Vous êtes maintenant armé pour créer des machines virtuelles avec vagrant.
]]>Si vous n’avez pas encore installé Homebrew, il est grand temps de le faire. Je vous promet que vous ne le regretterez pas. Homebrew est très propre et installe ses logiciels dans /usr/local/Cellar/
puis crée des liens symboliques vers les logiciels dans /usr/local/bin
. Aucune “pollution” du système n’est a craindre.
L’installation de homebrew lui-même est très simple:
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
Avec Homebrew, installer vagrant devient facile:
brew update
brew tap phinze/homebrew-cask
brew install brew-cask
brew cask install vagrant
Félicitations! Vagrant est maintenant installé sur votre mac. Vous pouvez le vérifier en faisant:
$ vagrant --version
Vagrant 1.5.1
Enjoy !
]]>Getting both to cooperate in dev mode without hitting the cross origin restrictions requires a proxy. Instead of using yet another tool in dev mode to do the proxyfication (I do recommand using such a tool in production though), I decided to have grunt proxify requests he wasn’t able to handle.
It all starts with writing a quick proxy handler wich goes in your Gruntfile.js
:
var proxyHandler = function proxyHandler(){
var httpProxy = require('http-proxy');
var proxy = new httpProxy.RoutingProxy();
return function proxyHandler(req, res, next){
var buffer = httpProxy.buffer(req);
setTimeout(function () {
proxy.proxyRequest(req, res, {
port: 9000, //
host: 'localhost',
buffer: buffer
});
}, 200);
};
};
Beware, I hard coded the definition for the port and host, you may want to change these. Also make sure you have http-proxy installed with npm install -s http-proxy
Ensure your grunt connect server doesn’t conflict with play’s
connect: {
options: {
port: 9001, // play defaults to 9000
livereload: 35729,
// change this to '0.0.0.0' to access the server from outside
hostname: 'localhost',
middleware: function(connect, options) {
var middlewares = [];
var directory = options.directory || options.base[options.base.length - 1];
if (!Array.isArray(options.base)) {
options.base = [options.base];
}
options.base.forEach(function(base) {
// Serve static files.
middlewares.push(connect.static(base));
});
// Make directory browse-able.
middlewares.push(connect.directory(directory));
//has to be last since we don't use connect routing at all!
middlewares.push(proxyHandler());
return middlewares;
}
}
...
}
Finally make sure 'connect:livereload'
appears in the server task. You can now start play (using the run command) on port 9000 and grunt which will bind to 9001 and proxy any unknown request to 9000.
If you use the play-yeoman plugin all you have to do is to start your play app with run (you only have one terminal window :),since the plugin will also lauch grunt and its proxy.
]]>java.nio.file
package, within it came the Files featuring a very interesting method : probeContentType(Path path)
. Mime type detection is always a pain, thus having a simple way to do it in the JDK is a very interesting feature indeed. Unfortunately, on Mac OS X, this feature is broken (see my gist for a test program).
As the javadoc for probeContentType explains, mime type detection is based on having a FileTypeDetector installed. The default one provided in JDK 7 is the GnomeFileTypeDetector
class and for some reason it won’t pick up libgio even if it is installed on the system. Or at least, I haven’t managed to get it to detect the lib, if you do I would love to hear from you in the comments. A bug was opened on this subject at Oracle but they decided to fix it only in jdk8 which is not yet released. I tried submitting a new bug report hoping to prompt Oracle to backport the JDK8’s file detector to JDK7. I have little hope, so you can forget about probeContentType
on OSX, unless …
The FileTypeDetector mechanism uses SPI (Service Provider Interface) to allow for loading additional detection providers. I hacked JDK 8’s default provider and created a small maven project to generate a jar which will register the provider with the JDK. Since this is platform specific issue and some of my coworkers use linux I didn’t add the jar to my project dependencies. Instead I dropped it into my JDK7’s jre/lib/ext
folder. This way the jar is registered automatically whenever I use that JDK.
The code can be found on github. Hopefully I have respected the requirements of the JDK licensing by reproducing both the license and the copyright header.
For the impatients, I made a branch with a binary of the jar and a sample mime.types file lifted from some apache source repository and ready to be copied to $HOME/.mime.types
.
The pull request has been merged. In the discussion, Josh Suereth suggested it should be possible to use the sbt-native-packager which replaced the dist command in play 2.2 to make a better build pack.
** EDIT 2013-08-19 **
I recommend reading up the pull request discussion at github. Basically, if you have a JVM based application, your absolute minimum slug size will be around 77MB because you _must_ package your own JRE in your slug. The default JRE for the stack is not upgraded on a regular basis leaving you exposed to security vulnerabilities.
I had been bothered by my Play! 2 apps slug size before, but never took the time to investigate. I couldn’t understand why sbt dist
would yield a 34MB zip while Heroku would end up with a > 100MB archive. While deploying an upgrate to Play! 2.1.3, I noticed it had bloated to 142MB: I had to act.
The current Heroku buildpack uses sbt clean compile stage
as its main command instead of sbt dist
. I haven’t tried to change that as I wanted something working fast, but I speculate it would be the best way to go for a Play! 2 app.
I cloned the official Heroku buildpack for scala, added some debug output in bin/compile
through du -sh ./*
and find . \! -type d | xargs ls -Slh
to try and understand were the bloat was coming from. To configure a custom buildpack for you app, all you have to do is run the following command :
$ heroku config:set BUILDPACK_URL=https://github.com/jeantil/heroku-buildpack-scala.git
Here is the output from du -sh ./*
:
4.0K ./.gitignore
8.0K ./.ivy2
77M ./.jdk
12K ./.profile.d
251M ./.sbt_home
4.0K ./.travis.yml
4.0K ./LICENSE
4.0K ./Procfile
8.0K ./README.md
92K ./app
32K ./conf
55M ./project
1.1M ./public
4.0K ./system.properties
44M ./target
56K ./test
My first reaction was : 55MB in project ?! Since I had run a find on the whole directory I was able to check out what was in project, looking only for MB sized artifacts. Here is what I found:
$ grep ./project deploy.log | grep M
55M ./project
14M Aug 15 08:19 ./project/boot/scala-2.10.0/lib/scala-compiler.jar
6.8M Aug 15 08:19 ./project/boot/scala-2.10.0/lib/scala-library.jar
3.1M Aug 15 08:19 ./project/boot/scala-2.10.0/lib/scala-reflect.jar
11M Aug 15 08:19 ./project/boot/scala-2.9.2/lib/scala-compiler.jar
8.5M Aug 15 08:19 ./project/boot/scala-2.9.2/lib/scala-library.jar
1.2M Aug 15 08:19 ./project/boot/scala-2.9.2/org.scala-sbt/sbt/0.12.3/ivy-2.3.0-rc1.jar
2.0M Aug 15 08:19 ./project/boot/scala-2.9.2/org.scala-sbt/sbt/0.12.3/main-0.12.3.jar
1.1M Aug 15 08:23 ./project/target/streams/$global/update/$global/out
Keeping both versions of the scala compiler in the production slug is not really useful. I haven’t tried to check why sbt places this here on Heroku and not on my workstation, but the first thing I added to my buildpack was :
if [ -d $BUILD_DIR/project/boot ] ; then
echo "-----> Dropping project boot dir from the slug"
rm -rf $BUILD_DIR/project/boot
fi
If you read carefully the output of du
above, you will have noted a .jdk
folder weighing in at 77MB. That’s right, the default buildpack will leave the JDK in the slug. Remove it since it is only used for compilation:
if is_play $BUILD_DIR && [ -d $BUILD_DIR/.jdk ] ; then
echo "-----> Dropping jdk from the slug"
rm -rf $BUILD_DIR/.jdk
fi
Along the same line, you can drop all the intermediate compilation artifacts with :
if [ -d $BUILD_DIR/target ] ; then
echo "-----> Dropping compilation artifacts from the slug"
rm -rf $BUILD_DIR/target/scala-*
rm -rf $BUILD_DIR/target/streams
rm -rf $BUILD_DIR/target/resolution-cache
fi
And now my slug is back to 39MB which is still a bit fat but not so bad. As I said in the introduction, the best would probably be to change the buildpack and use the artifacts generated by sbt dist.
Until the pull request is accepted by the Heroku maintainers, you can fork my version of the build pack. I suggest you don’t use my version directly as I will not maintain my fork after the pull request.
]]>