Changing Play 2 DB Configuration At Runtime: Beware of Dragons

Maybe you work for a company that leaves DB passwords in plaintext in configuration files on their servers. You would probably be among the majority. Or maybe your company encrypts passwords in configuration files: Also not that uncommon. What’s not common (in my experience) is having the devops team “transmit” the password to the application after it has already been started. It’s a reasonable requirement, but out of the box, Play 2.1 presents several hurdles.

Changing The Configuration

We all know that immutability of shared objects is a good thing for application concurrency (If you don’t know that, check out Java: Concurrency In Practice by Brian Goetz). But when objects are immutable I expect there to be an obvious or well-documented way to build another. Fortunately there is such a thing, but it’s neither obvious nor well-documented: com.typesafe.config.ConfigFactory. You can use this factory to produce a com.typesafe.config.Config object which can then be fed into a play.Configuration constructor. Armed with this knowledge you might be tempted to try something like this.

public class Global extends GlobalSettings { @Override public Configuration onLoadConfig(Configuration configuration, File file, ClassLoader classLoader) { Map<String, Object> map = Maps.newHashMap(configuration.asMap()); addValue(map,”db.default.pass”, “prodPass”); return new Configuration(ConfigFactory.parseMap(map)); }   private void addValue(Map<String,Object> properties, String key, Object value) { addValue(properties, key.split(“\\.”), value); }   private void addValue(Map<String,Object> properties, String[] paths, Object value) { if(paths.length > 1) { Map<String,Object> nextMap = (Map<String,Object>)properties.get(paths[0]); if(nextMap == null) { nextMap = Maps.newHashMap(); properties.put(paths[0],nextMap); } addValue(nextMap, Arrays.copyOfRange(paths, 1, paths.length),value); } else { properties.put(paths[0],value); } } }

If you did try this, you’d be greeted with this error message.

BadPath: path parameter: Invalid path ‘[B’: Token not allowed in path expression: ‘[‘ (you can double-quote this token if you really want it here)

It might as well say, “Here there be dragons!” At least then I’d be confused and entertained. After importing the Typesafe Config source code and stepping through the parsing process in my debugger I found that there is a property called akka.actor.serialization-bindings.[B, which was causing the parser to choke. With a better understanding of how the parser works, I tried adding double-quotes to the key, which turned out to be the solution.

public Configuration onLoadConfig(Configuration configuration, File file, ClassLoader classLoader) { Map<String, Object> map = Maps.newHashMap(configuration.asMap()); Configuration akka = configuration.getConfig(“akka.actor.serialization-bindings”); addValue(map,”akka.actor.serialization-bindings”, null); addValue(map,”akka.actor.serialization-bindings.\”[B\”“, akka.getString(“\”[\”B”)); addValue(map,”akka.actor.serialization-bindings.\”java.io.Serializable\”“, akka.getString(“\”java.io.Serializable\”“)); addValue(map,”db.default.pass”, “prodPass”); return new Configuration(ConfigFactory.parseMap(map)); }   private void addValue(Map<String,Object> properties, String key, Object value) { if(key.contains(“\”“)) { List<String> paths = Lists.newArrayList(); for(String path : key.split(“\\.\”“)) { if(path.endsWith(“\”“)) { paths.add(String.format(“\”%s\”“, path.substring(0,path.length()-1))); } else { paths.addAll(Lists.newArrayList(path.split(“\\.”))); } } addValue(properties, paths.toArray(new String[paths.size()]), value); } else { addValue(properties, key.split(“\\.”), value); } }   private static void addValue(Map<String,Object> properties, String[] paths, Object value) { if(paths.length > 1) { Map<String,Object> nextMap = (Map<String,Object>)properties.get(paths[0]); if(nextMap == null) { nextMap = Maps.newHashMap(); properties.put(paths[0],nextMap); } addValue(nextMap, Arrays.copyOfRange(paths, 1, paths.length),value); } else { properties.put(paths[0],value); } }

It would be nice if there were a way to get a configuration map with escaped quotes where necessary, but it would be fairly painless to do it yourself.

Transmitting The Password Using a Named Pipe

The second part of the requirement is being able to transmit the new password to a running app and then reloading the EntityManagerFactory (client is using JPA). With a JavaEE app you can do this via JMX, or an unpublished web endpoint. You could do the latter with Play except that there doesn’t seem to be a way to reload the integrated EntityManagerFactory after the app has started. A very simple solution would be to have the app block on startup waiting for input from a named pipe.

@Override public Configuration onLoadConfig(Configuration configuration, File file, ClassLoader classLoader) { if(configuration.getString(“production.level”).equals(“PROD”)) { Configuration newConf = waitForConfig(configuration); if(newConf != null) { return newConf; } } return super.onLoadConfig(configuration, file, classLoader); }   private Configuration waitForConfig(Configuration configuration) { Configuration newConf = null; try { BufferedReader reader = new BufferedReader(new FileReader(new File(configuration.getString(“config.pipeloc”)))); String password = reader.readLine(); Map<String,Object> newConfig = configuration.asMap(); … addValue(newConfig, “db.default.pass”, password); newConf = new Configuration(ConfigFactory.parseMap(newConfig)); } catch (Exception e) { throw new RuntimeException(e); } return newConf; }

This is pretty easy to accomplish. You only need to create the pipe using the mkfifo command and the just echo the password to the pipe. This solution works well, but I found it to be kludgy. For one thing, the onLoadConfig() is called before the Play Application object has been created, so you can’t use Play.application().isProd() to determine the mode. This necessitates creating a config property called production.level which can have the values “PROD” or “DEV.” Yuck. Pretty big code smell there. The named pipe can also get into weird states on some operating systems. I’ve noticed that on OSX you can continually write to the pipe even when there is no consumer, so if someone inadvertently did this before your app started, it could cause problems. A named pipe is also not accessible outside of that machine without using an SSH tunnel. There are a lot of reasons why this is not an optimal solution. Fortunately there is an alternative.

Reloading The DataSource and JPA Configuration After Startup

The original requirement of allowing the app to start using a default db configuration and then changing and reloading the EntityManagerFactory can be accomplished by writing a Play plugin (Objectify has a good write-up on creating Play 2 plugins). The first plugin replaces the Play javaJdbc plugin, and allows you to recreate the DataSource at runtime. This plugin borrows heavily from Play’s built-in DB.scala.

public class MyBoneCPPlugin extends Plugin {   private final Application application; private BoneCPDataSource ds;   public MyBoneCPPlugin(Application application) { this.application = application; }   public void onStart() { Configuration config = application.configuration().getConfig(“db.default”); loadDataSource(config); }   public void loadDataSource(Configuration config) { try { Class.forName(config.getString(“driver”)); ds = new BoneCPDataSource(); ds.setJdbcUrl(config.getString(“url”)); ds.setUsername(config.getString(“user”)); ds.setPassword(config.getString(“pass”));   // Pool configuration goes here   // Bind in JNDI play.JNDI.getContext().rebind(config.getString(“jndiName”), ds); } catch (Exception e) { throw new RuntimeException(e); } }   public void onStop() { try { ds.close(); } catch (Exception e) { Logger.debug(e.getMessage(),e); } } }

All that’s happening here is that the plugin is creating and adding the BoneCPDataSource to the JNDI Context so that it’s accessible to JPA. The plugin also exposes a method that recreates the data source and rebinds it to the JNDI context. The second plugin replaces the Play javaJpa plugin. It allows reloading the JPA EntityManagerFactory at runtime. In actuality, this is almost a verbatim copy of the play-java-jpa source with a few modifications. As much as I hate to reproduce so much of the code, it was necessary. Maybe the Play team will consider adding realoadability.

public class JPAPlugin extends Plugin { private Map<String,EntityManagerFactory> emfs = new HashMap<String,EntityManagerFactory>();   public void onStart() { Configuration jpaConf = Configuration.root().getConfig(“jpa”); if(jpaConf != null) { for(String key: jpaConf.keys()) { String persistenceUnit = jpaConf.getString(key); emfs.put(key, Persistence.createEntityManagerFactory(persistenceUnit)); } } }   public void resetFactories() { emfs = new HashMap<String,EntityManagerFactory>(); onStart(); }   public EntityManager em(String key) { EntityManagerFactory emf = emfs.get(key); if(emf == null) { return null; } return emf.createEntityManager(); } }

The added resetFactories() method allows recreating the JPA EntityManagerFactory list.

public class JPA {   …   public static void reloadWithProperties(Map<String,String> props) { MyBoneCPPlugin dbPlugin = Play.application().plugin(MyBoneCPPlugin.class); JPAPlugin jpaPlugin = Play.application().plugin(JPAPlugin.class);   Map<String,Object> configMap = Play.application().configuration().asMap(); for(Map.Entry<String,String> entry : props.entrySet()) { addValue(configMap, entry.getKey(), entry.getValue()); }   dbPlugin.loadDataSource(new Configuration(ConfigFactory.parseMap(configMap).getConfig(“db.default”))); jpaPlugin.resetFactories(); } }

The added reloadWithProperties() method in the JPA helper class calls the loadDataSource() method of the data source plugin and then calls resetFactories(). The full plugin code can be found here. If you want to use it run

play publish-local

from the plugin project root directory and then import it in your project’s Build.scala file. Also don’t forget to remove javaJdbc and javaJpa from Build.scala. You can then use it exactly as you would the built-in JPA plugin with the added benefit of being able to reload your DB configuration at runtime.

Scroll to Top