[dancer-users] Advent 2015, medium-scale Dancer, part 6: Hot Code Reloading
Warren Young
wyml at etr-usa.com
Tue Nov 10 22:23:40 GMT 2015
I intended to stop the series with 5 parts, but I just thought of another important topic that fits into the series: hot code reloading.
Hopefully, I’m done now. I’ll be accepting comments for another few days, though, if someone wants to see some changes in these articles before they’re published.
Sawyer, to publish these, do I need to modify a fork of the Advent web site code, or will someone from the developer core publish them for me? If the latter, I’ve made significant refinements to all of the articles since posting each to the list for comment.
-------------------
Many other web frameworks have a hot code reloading feature: simply by changing a source file on disk, the new code in that file runs for each subsequent web hit that calls that code. This makes development quicker by reducing the reload time in the edit/reload/test/debug cycle.
Dancer does not include this feature out of the box, but the power of Perl makes it fairly easy to add to your app.
There are, in fact, numerous CPAN modules to do this already, such as `Module::Reload`, but it turns out that rolling your own can provide some serious benefits. There are details you want to take care of manually during the reloading process which rule out the canned solutions.
Consider this Dancer-aware reloader based on Hack 30 in [Perl Hacks](http://shop.oreilly.com/product/9780596526740.do):
package App::Reloader;
use Dancer2 appname => 'App';
my $g_last_reload_check_time = 0;
my %g_module_times;
INIT {
while (my ($module, $path) = each %INC) {
$g_module_times{$module} = -M $path;
}
}
sub reload {
my ($min_delta) = @_;
my $now = time;
return if ($now - $g_last_reload_check_time) < $min_delta;
$g_last_reload_check_time = $now;
while (my ($module, $time) = each %g_module_times) {
my $mt = -M $INC{$module};
next if $mt == $time;
next if $module eq 'App/Const.pm'; # can't redefine constants
next if $module eq 'App/Reloader.pm'; # self-reload breaks things
delete $INC{$module};
eval {
# Suppress complaints about sub redefinition
no warnings 'redefine';
local $SIG{__WARN__} = sub {};
# Reload the module, unloaded above
require $module;
debug "Reloaded changed module $module.";
# Only mark it updated if that didn't throw an exception.
$g_module_times{$module} = $mt;
};
error "Reloading changed module $module: $@!" if $@;
}
}
1;
You also need to merge the following into the module containing your Dancer hooks, `lib/App.pm` by default:
use App::Reloader;
use Readonly;
Readonly::Scalar my $min_reload_check_time =>
(config->{environment} eq 'development' ? 1 : 15);
hook before => sub {
# do checks that cause an immediate client rejection first
App::Reloader::reload($min_reload_check_time);
# now we can do other stuff that requires up-to-date modules
};
This mechanism gives us a number of features you don't get from the canned solutions:
**Logging**
We can use Dancer's logging mechanism to report what the reloader is doing.
Third-party loaders are typically either silent, which makes debugging more difficult, or they use the `STDOUT` or `STDERR` streams. Using `STDOUT` with Dancer is a serious problem, since that ends up injecting the message into the rendered route output, which typically breaks its client-side handling. Using `STDERR` isn't much better, since your Dancer app typically runs in the background; unless you redirect the app's `STDERR` to a second log file, you won't see these complaints.
**Performance**
Our custom reloader needn't check for reloadable modules on every web hit. Chances are, the code we loaded from the modules on disk for the last hit are still good. The example above checks at most once a second in development mode, and once every 15 seconds in production. You can adjust the timing to suit your purposes.
`Module::Reload` exposes a `check()` function similar to `reload()` above, so you could wrap it in your own timing logic, but I've moved that internal to the reloader, so you only need to pass in the time constant.
Other reloaders add a constant burden to the system, so they're recommended only for development. While reloaders are exceedingly useful during development, it's also nice to be able to do an app upgrade in production without restarting the app. We therefore want a reloader that we aren't reluctant to run full-time.
**Easy Exclusions**
In the first article in this series, I proposed that you might want an `App::Const` module for holding app-wide constants. If you use the CPAN module `Readonly`, demonstrated above, you will find that you get an error from Perl about redefining constants if you let `App::Reloader` reload that file. We need to make the reloader aware of this.
Generic reloaders typically won't cope with cases like this, or they expose a somewhat clumsy API for customizing the set of modules it monitors. It's simpler to just add an exclusion rule to this file when you come across a module that can't be reloaded for one reason or another.
This is a good reason for having an app-wide "constants" module, by the way: by aggregating all of the constants into a single location, we cause only that one module to be un-reloadble. If you scatter `Readonly` calls throughout your other modules instead, they'll be un-reloadable, too.
More information about the dancer-users
mailing list