As most readers of this site know, I’ve been busy lately working on the Wallaby configuration service, which aims to make it painless to manage configurations for entire Condor pools. In this post, I’m going to discuss some of the issues of application configuration from the other side of the problem: the semantics and interface to the configuration subsystem from within the application itself. These issues and concerns are taken from Condor but are, I suspect, generally applicable to substantial configurable applications in general. (I am currently collaborating with Pete Keller from Wisconsin on a redesign of Condor’s configuration subsystem that should address these problems.)
Problems with application configuration generally fall into several categories: value-gap problems, default value problems, type-safety problems for the configuration parameter values themselves, and type-safety problems of the programmatic interface to the configuration. We’ll discuss each of these in turn.
Truth-value gaps are a problem of classical logic: we would like some way to reason about propositions that are apparently neither true nor false (e.g. Aristotle’s example that “there will be a sea battle tomorrow or there will not be a sea battle tomorrow” is true), but we cannot encode an unknown or indeterminate truth value as “true” or “false.” (The three-valued logic Ł3 of Jan Łukasiewicz is one such system that addresses this problem.)
We can generalize the problem of truth-value gaps to many situations that come up in programming, for example:
- Imagine a sparse array of Java-style references. Does a null value correspond to an explicit null that we are interested in tracking, or merely to the absence of a value that we care about?
- Consider C preprocessor macros: many a novice C programmer has been frustrated by the distinction between #ifdef FOO and #if FOO — but only after setting FOO to 0 fails to have the desired effect.
- Similarly, applications that are configured through environment variables may act on the mere existence of a variable in the process’s environment or on the existence of a variable and some property of its actual value.
In Condor, these sorts of problems arising from the configuration subsystem are typically handled in an ad hoc manner. In some places, the mere definition of a configuration parameter is enough to enable a feature; in others, the parameter must be defined and its string representation must include something that corresponds to “true” (one example is “the first character is either ‘t’ or ‘T’”). The current configuration API also makes it inconvenient (but not impossible) to determine if a parameter is undefined or set to false; in any case, the burden for sensibly treating value gaps falls to the programmer. (I see an interface that does not depend on programmer discipline to be a win over one that does.)
Default value problems
Configuration parameters should have sensible defaults. Unfortunately, the process of assigning default values may not be straightforward. A default value may not merely be a value but it may be the result of evaluating a function at runtime. Alternatively, defaults may depend on context: parameter FOO may have one value in subsystem X on platform Y, but another in subsystem Z or on platform W. (All but the last of these variable names have no deeper significance.)
In Condor, these defaults are handled in two ways: for some parameters whose defaults that are consistent across subsystems and platforms, the default is specified in the generic configuration file shipped with Condor. (About 275 parameters are given default values in this file.) For other parameters that require context-sensitive defaults, the default values are supplied as an extra parameter to configuration API functions at each call site; these may also be conditionally compiled, so that, for example, whether thread-based parallelism is available is defined to be true within the condor_collector on all platforms except Windows, but false everywhere else.
A configuration subsystem that supported specifying rich defaults — including immediate values as well as expressions that would evaluate to the correct default depending on the context in which they were evaluated — would free application programmers from the tedious and error-prone work of encoding defaults in every call to param().
Popular discussion of types and type safety is fraught with handwaving, imprecision, and nonsense (e.g. the ridiculous and contradictory appellation ”dynamically typed,” which is often applied to untyped languages). For the remainder of this discussion, I will be using definitions adapted from Luca Cardelli’s survey chapter on type systems: namely, a type is the upper bound on a range of values, and a typed language is one in which nontrivial types can be ascribed to variables.
It should be clear that imposing a sufficiently expressive type system on configuration variables is generally desirable. A sufficiently expressive type system for an application like Condor would include not merely the classic types of low-level languages (e.g. unsigned int, boolean, character string, etc.), but also types that encode application-specific information (e.g. email addresses, hostnames, Pascal-style ranges, typed dictionaries). However, since configuration variables in Condor may be defined in terms of macro-expanded values or (in the case of some parameters) as ClassAd expressions to be evaluated later, it may be difficult to typecheck configuration variables a priori.
Another type-safety problem deals with how (or how frequently) values are computed. Some configuration parameters have fixed values for the life of a process, but others (e.g. “the current time” or “the result of taking a random element from this list”) may change every time that they are evaluated. The idea of using type systems to track how an expression is evaluated (and not merely the shape of its result) is not new: Lucassen and Gifford proposed effect systems for functional languages in 1988, and computer music languages like Max/MSP and SAOL have long distinguished between expressions that are evaluated once and expressions whose values could change at various intervals (like control signals, which can change hundreds of times per second, or audio signals, which vary tens of thousands of time per second). In the context of configuring Condor, it is sufficient to track whether a parameter’s definition is to be evaluated (at least) once or every time its value is requested from the configuration subsystem. The values of parameter definitions that need only be evaluated once can be memoized; currently, Condor’s configuration subsystem doesn’t support this sort of type information and any memoization is performed on an ad hoc basis in application code.
One of the main concerns behind thinking about improving Condor’s configuration API is to reduce the potential avenues for error by developers. Adding a typed interface to typed configuration data greatly simplifies application code that uses the configuration subsystem and eliminates ad hoc, per-call-site typechecking of configuration values. (It shouldn’t be acceptable to support a high-level configuration API in 2010 that might require clients to manually coerce a user-supplied string into a value of some other type!) A typed interface could also automatically handle memoization of non-varying configuration variables, and provide methods to inspect whether and where parameters are defined, rather than merely a value (that might represent a value gap).
As I mentioned earlier, these issues are the subject of active design and development. Currently, some of our notes on our solution to these problems in Condor are in a ticket on the Condor wiki; ideally, an implementation will be part of a developer release of Condor in the next few months. Of course, I welcome feedback on these ideas and on the concerns that they are meant to address.