Actionhero v14 and a problem with recursive configuration

actionhero javascript node.js 
2016-06-24T20:55:23.636Z
↞ See all posts


Actionhero is now at version 14.0.1!


The V14.0.0 release includes a new way to save and reuse formatters and validators for your actions, and also gives you greater control of your Redis connections. The V14.0.1 release fixes a bad initialization of the above…

You can see the whole changelog here: https://github.com/evantahler/actionhero/releases/tag/v14.0.0

Named Validators & Formatters

Allows for action validators and formatters to use both named methods and direction functions.

1exports.cacheTest = { 2 name: 'cacheTest', 3 description: 'I will test the internal cache functions of the API', 4 outputExample: {}, 5 inputs: { 6 key: { 7 required: true, 8 formatter: [ 9 function(s){ return String(s); }, 10 'api.formatter.uniqueKeyName' // <----------- HERE 11 }, 12 value: { 13 required: true, 14 formatter: function(s){ return String(s); }, 15 validator: function(s){ 16 if(s.length < 3){ return '`value` should be at least 3 letters long'; } 17 else{ return true; } 18 } 19 }, 20 }, 21 run: function(api, data, next){ 22 // ... 23 } 24};

And then you would define an initializer with your formatter:

1"use strict"; 2 3module.exports = { 4 initialize: function (api, next) { 5 api.formatter = { 6 uniqueKeyName: function (key) { 7 return key + "-" + this.connection.id; 8 }, 9 }; 10 11 next(); 12 }, 13};

Redis Client

There are so many ways to configure redis these days… handling the config options for all of them (sentinel? cluster?) is a pain… so lets just let the users configure things directly. It will be so much simpler!

This will be a breaking change

  • in config/redis.js, you now define the 3 redis connections you need explicitly rather than passing config options around:
1var host = process.env.REDIS_HOST || "127.0.0.1"; 2var port = process.env.REDIS_PORT || 6379; 3var database = process.env.REDIS_DB || 0; 4 5exports["default"] = { 6 redis: function (api) { 7 var Redis = require("ioredis"); 8 return { 9 _toExpand: false, 10 // create the redis clients 11 client: Redis.createClient(port, host), 12 subscriber: Redis.createClient(port, host), 13 tasks: Redis.createClient(port, host), 14 }; 15 }, 16};
  • move api.config.redis.channel to api.config.general.channel
  • move api.config.redis. rpcTimeout to api.config.general. rpcTimeout
  • throughout the code, use api.config.redis.client rather than api.redis.client

Quickly after releasing version 14.0.0 we realized that there was a problem with the new way we handled the redis config.

Actionhero loads its configuration recursively. We do this so that you can reference config directives from one file inside another. If you attempt to reference something that isn’t yet defined, we’ll skip over the file in question, load the rest of the config and try again. Under the hood, that means that any individual file is potentially required and exported many times. This is fine when you are building up a hash object, but terrible if you are creating a new connection to redis at each run. A new actionhero project ended up creating 27 connections to redis.

The good news is that now the redis configuration is now in user-space. It was a simple change to check if the redis connections exist already, and if they do, disconnect the old ones. This isn’t yet an ideal solution (as booting up will connect and disconnect a number of times), but it’s an improvement.

1var host = process.env.REDIS_HOST || "127.0.0.1"; 2var port = process.env.REDIS_PORT || 6379; 3var database = process.env.REDIS_DB || 0; 4var password = process.env.REDIS_PASS || null; 5 6exports["default"] = { 7 redis: function (api) { 8 var Redis; 9 var client; 10 var subscriber; 11 var tasks; 12 13 // cleanup if we are rebooting or looing in config load 14 if (api.config.redis) { 15 if (api.config.redis.client) { 16 api.config.redis.client.quit(); 17 } 18 if (api.config.redis.subscriber) { 19 api.config.redis.subscriber.quit(); 20 } 21 if (api.config.redis.tasks) { 22 api.config.redis.tasks.quit(); 23 } 24 } 25 26 if ( 27 process.env.FAKEREDIS === "false" || 28 process.env.REDIS_HOST !== undefined 29 ) { 30 Redis = require("ioredis"); 31 client = new Redis({ 32 port: port, 33 host: host, 34 password: password, 35 db: database, 36 }); 37 subscriber = new Redis({ 38 port: port, 39 host: host, 40 password: password, 41 db: database, 42 }); 43 tasks = new Redis({ 44 port: port, 45 host: host, 46 password: password, 47 db: database, 48 }); 49 } else { 50 Redis = require("fakeredis"); 51 client = Redis.createClient(port, host, { fast: true }); 52 subscriber = Redis.createClient(port, host, { fast: true }); 53 tasks = Redis.createClient(port, host, { fast: true }); 54 } 55 56 return { 57 _toExpand: false, 58 // create the redis clients 59 client: client, 60 subscriber: subscriber, 61 tasks: tasks, 62 }; 63 }, 64};

I’ll keep working on this…

Hi, I'm Evan

I write about Technology, Software, and Startups. I use my Product Management, Software Engineering, and Leadership skills to build teams that create world-class digital products.

Get in touch