Simon Krueger

Batch Updating in React

This post talks about how React components perform updates for different types of events and how we can get these updates to behave in a way that is ideal when using AJAX actions that update multiple Flux stores.

SyntheticEvents

React components will batch their updates inside of Events. This means that when a component's setState function is called multiple times inside of an OnClick event, the component will only call render once (if shouldComponentUpdate returns true). This is exactly the behavior I want. I only want to interact with stores that are in a complete, stable, and ready state.

AJAX and timeout events

Unfortunately, AJAX and window.timeout events are not batched like SyntheticEvents. It is common for my code to update multiple stores from a single AJAX response. Bugs crop up by using the default behavior of re-rendering on each call to setState. Luckily, there is a pattern to batch these updates and it is pretty simple to do to. I batch the updates by using a single store that sets a isLoading boolean at the beginning of the AJAX event call and doesn't clear it until all the stores have finished their updates. The main react component will use this isLoading boolean inside of its shouldComponentUpdate function. The logic will say to not update if the current state and the next state is loading. This isLoading acts as a lock and batches all the stores updates into one single render call. This holds the invariant in our components that stores will be in a complete and stable state during render calls.

UI Lock Example

Here is a partially implemented version of a loading lock that performs batch updates for AJAX events.

AppDispatcher.dispatch({
  type: 'AJAX_ACTION',
});

_isLoading = false;
var AppStore = {
  isLoading() {
    return _isLoading;
  },
};

AppStore.dispatchToken = AppDispatcher.register(action => {
  switch (action.type) {
    case 'AJAX_ACTION':
      _isLoading = true;
      AppStore.emitChange();
      break;

    case 'AJAX_ACTION_SUCCESS':
      AppDispatcher.waitFor([OtherStore1.dispatchToken, OtherStore2.dispatchToken]);
      _isLoading = false;
      AppStore.emitChange();
      break;
  }
});

var OtherStore1 = {
  // Store state...
};

OtherStore1.dispatchToken = AppDispatcher.register(action => {
  switch (action.type) {
    case 'AJAX_ACTION_SUCCESS':
      // Update state
      OtherStore1.emitChange();
      break;
  }
});

var OtherStore2 = {
  // Store state...
};

OtherStore2.dispatchToken = AppDispatcher.register(action => {
  switch (action.type) {
    case 'AJAX_ACTION_SUCCESS':
      // Update state
      OtherStore2.emitChange();
      break;
  }
});

function getState() {
  return {
    storeState1: OtherStore1.getAll(),
    storeState2: OtherStore2.getAll(),
    isLoading: AppStore.isLoading(),
  };
}

var App = Reat.createClass({
  shouldComponentUpdate(nextProps, nextState) {
    var update = true;
    if (this.state.isLoading && nextState.isLoading) {
      update = false;
    }

    return update;
  },

  render() {
    // Stores are in a stable complete state invariant holds! \o/
  },
});

Encrypting secrets with knife-solo

We've had some secrets laying around in our database that needed to be changed and encrypted. We have been using chef to deploy our service. With chef data bags we can encrypt and decrypt our secrets. There is one problem though. It expects you to have a chef server. However, we are using a pretty simple setup with no chef server at the moment. Luckily there is knife-solo and knife-solo_data_bag.

Create a data bag named secrets with an item named database

$ knife solo data bag create secrets database --json-file data_bags/secrets/database.json --data-bag-path chef/data_bags/ --secret-file ~/secrets_key

Edit the existing data bag item.

$ knife solo data bag edit secrets database --data-bag-path chef/data_bags/ --secret-file ~/secrets_key

Logging django apps to syslog

I spent most of my day filling in the holes of my django/python logging knowledge. I've never done logging in python or a django app before. And thus our code base, had zero logging in it. A huge problem when it comes time to debug issues in production. I read a few articles that I found at Full Stack Python and also went through the Python doc logging tutorials. Taking Some Pain out of Python Logging was an eye opener when Hynek explained that syslog can capture and record the python log records. Syslog was exactly what I set out to do (I didn't know it was called syslog. I just knew about /var/logs, so today was a good learning experience). I am not doing exactly what Hynek does. But it accomplishes my goal of getting our logs into syslog.

Here is the final LOGGING dictionary in settings.py I went with.

# settings.py
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'formatters': {
        'verbose': {
            'format': '%(process)-5d %(thread)d %(name)-50s %(levelname)-8s %(message)s'
        },
        'simple': {
            'format': '[%(asctime)s] %(name)s %(levelname)s %(message)s',
            'datefmt': '%d/%b/%Y %H:%M:%S'
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'syslog': {
         'level': 'DEBUG',
         'class': 'logging.handlers.SysLogHandler',
         'facility': 'local7',
         'address': '/dev/log',
         'formatter': 'verbose'
       },
    },
    'loggers': {
        # root logger
        '':{
            'handlers': ['console', 'syslog'],
            'level': 'INFO',
            'disabled': False
        },
        'thingsforwork': {
            'handlers': ['console', 'syslog'],
            'level': 'DEBUG',
            'propagate': False,
        },
    },
}

Ubuntu 14.04 uses the rocket-fast system for log processing (rsyslog) and we need to tell it about our local7 facility. I got this one from serverfault.

# /etc/rsyslog.d/30-thingsforwork.conf
local7.*                                             /var/log/thingsforwork.log
& ~  # This stops local7.* from going anywhere else.

Do a sudo service rsyslog restart so rsyslog.d can pickup the new config. And ta-da!

# /var/log/thingsforwork.log
May 28 03:19:57 vagrant 11874 140156620121856 thingsforwork.dashboard.views.home                 DEBUG    we are in the home page, this is a debug
May 28 03:19:57 vagrant 11874 140156620121856 thingsforwork.dashboard.views.home                 INFO     We are logging django! e01be3ae-e9d1-49d7-97ac-00c5454f3026
May 28 03:20:02 vagrant 11874 140156620121856 requests.packages.urllib3.connectionpool           INFO     Starting new HTTP connection (1): dashboard.reviewpush.com
May 28 03:20:02 vagrant 11874 140156605413120 requests.packages.urllib3.connectionpool           INFO     Starting new HTTPS connection (1): tartan.plaid.com
May 28 03:20:02 vagrant 11874 140156392699648 googleapiclient.discovery                          INFO     URL being requested: GET https://www.googleapis.com/discovery/v1/apis/analytics/v3/rest
May 28 03:20:02 vagrant 11874 140156375914240 requests.packages.urllib3.connectionpool           INFO     Starting new HTTPS connection (1): quickbooks.api.intuit.com