Friday, October 30, 2009

Item 13: Build in administration support











 < Day Day Up > 





Item 13: Build in administration support



One common facet of enterprise applications is that, unlike most "shrink wrap" commercial off-the-shelf software, enterprise applications typically require some kind of administrative control over their behavior and/or functionality. For example, it's not uncommon for administrators to need to perform a variety of tasks, ranging from the ubiquitous "add or remove users from the list of users authorized to use the system," to system-specific tasks like "find the expense report in a particular user's workflow inbox and redistribute it to somebody else," to simple system configuration like "what image should show up in the upper-left corner?"



All of these problems have potential solutions already: system administrators can wade directly into the database to make changes there, or we can simply replace the existing image file with another file and keep the names identical. The problem is, system administrators having to do administration this way need both an intimate knowledge of the application's architecture and the skill to make those modifications as necessary�they need to know enough SQL to be able to replace or modify the data in the tables, for example. And we're assuming that they're perfect while doing this and will never make a mistake with the raw, live data.



The fact of the matter is, if this application is going to live in a production environment for any length of time, we need to provide administrators with some kind of administrative "console" to enable them to make those updates as necessary without having to enter them directly into the data storage layer itself. Specifically, we need to consider giving administrators some kind of high-level console for two areas: configuration and control.



Configuration here refers to the various "we don't want that value hard-coded" values that show up periodically within your application. A classic example is the e-mail address of the person or persons who should be e-mailed in the event of a system failure of some kind (see Item 7). Normally, we don't want that data hard-coded inside of the application; even if you establish an e-mail alias (e.g., systempanic@yourcompany.com), what happens when your company is acquired and that domain name suddenly changes? Configuration data could be thought of as data that changes infrequently in response to stimuli entirely outside of the application domain itself. (The distinction is necessary to differentiate it from data that's modified due to user action; without that, there would be very little to differentiate configuration data from any other sort of data in the system.)



Unfortunately, Java has traditionally not had a very strong configuration story. Starting with JDK 1.0, configuration of Java applications has traditionally come in a variety of different flavors, all of which have their strengths, but also some serious disadvantages as well.



The first mechanism is the ubiquitous "Properties file," a text file containing name-value pairs that are read in by your Java code, typically using the java.util.Properties class. This mechanism has the positive benefits of being absurdly easy to understand from a programmer's position, and since the "data storage" format here is nothing more complex than a text file, it's also absurdly easy to modify the data. Properties suffer from some severe drawbacks, however, most notably that text data is also absurdly easy to get wrong, most often due to typos. In addition, because the data is stored as a file on the filesystem, your code requires access to the filesystem to get that data (which is denied you in EJB). Attackers might be able to sniff out additional useful information if they can figure out how to look at that file via a file-path canonicalization bug or if the file is somehow visible via HTTP request. And some helpful "power user" may come along and "fix" what are "obviously" errors in that file if he or she can get to it. Couple this with several facts�that only strings can be used as either key or value in the configuration data, that remote access to a file on a production server may not be as easy as you might think, that there's no notification mechanism so the application can reread the values when they change, and that the keys are a simple flat namespace requiring work on your part (concatenating or parsing) if you want to hierarchically arrange them�and you suddenly have a mechanism that isn't nearly "good enough" for production J2EE use. Oh, I'll admit, we can get by, but we can do so much better than this.



A second option in widespread use is to put all that data into the database. We solve a number of the problems with the properties mechanism, but we're back to a fundamental bootstrapping problem: Where do we put the database configuration data, like JDBC URL and/or user ID and password, to use to connect to the database? (Ideally, you shouldn't always connect to the database via a single user ID anyway, but it's such a common idiom that pretending otherwise is foolish.) We could put everything but the database configuration data into the database and put the database configuration data itself in a Properties file, but now we're looking at two different locations to store configuration data, which gives us additional issues. Plus, even when storing configuration data in the database, we're still faced with problems, like that of the hierarchical arrangement of keys (difficult to do in a relational model) and that of updating the configuration data (difficult if your system administrators aren't comfortable with SQL, or you're not comfortable with them wandering around in there, which I wouldn't be).



Along the same lines as using the relational database, we could use a JNDI-accessible layer to store the data, most notably an LDAP server. LDAP, certainly, solves the problem of storing keys in a hierarchical arrangement, but once again we have the bootstrapping issue with the relational database�JNDI requires some initial configuration parameters to know where the LDAP server is, and this itself is data that we don't want to hard-code, so we're back to the same story in that we still need some way to bootstrap the JNDI access into place.



The various J2EE specifications have also offered up their own variations on this story, using XML in the form of the deployment descriptor to store configuration options, which will be made available to the developer via various APIs of one form or another. For example, the Servlet Specification allows for init-param tags in the web.xml file, which can be obtained via calls to getInitParameter on both a per-servlet and per-ServletContext basis. Once deployed, many servlet containers then offer some kind of administrative GUI to change those values in the running Web application. Unfortunately, this mechanism carries its own share of problems. Redeploying a new version of the application will reset the existing configuration data to whatever's specified in the deployment descriptor, effectively forcing the system administrator to go back and reset everything by hand to the desired values. Plus, we still have the problems of hierarchical arrangement of keys and the fact that only strings can be stored in the deployment descriptor.



In following the XML theme, we could use an XML file instead of a Properties file to store our configuration data. While still suffering from some of the problems of a Properties file, XML does solve a good number of problems. Because XML is hierarchical in nature, the problem of hierarchical key arrangement becomes moot. XPath makes an awesome way to get at data without having to navigate the tree by hand. Unfortunately, it's still not perfect. We still face all of the problems that otherwise plague Properties files: we need a file on the filesystem and access to the filesystem, system administrators will need to edit the XML file itself to make changes (thus introducing the possibility that they could introduce a typo that makes the file ill-formed), and so on. But we're getting closer.



Starting with JDK 1.4, Sun has introduced a new API that could offer a long-term solution to this configuration dilemma: the java.util.prefs.Preferences (and related) classes. Although not formally part of the J2EE Specification,[8] the Preferences API offers a number of the advantages found in the other mechanisms, as well as a few new ones.

[8] Some will point out that J2EE implies dependence on, and therefore acceptance of, Java 2 Standard Edition (J2SE) and all of its associated APIs, such as the Preferences API. Unfortunately, the problem is that the Preferences API requires a particular security permission, a RuntimePermission, to be granted in order to access Preferences data, and that permission is not listed as part of the J2EE 1.3 Specification.



Using the Preferences API in its simplest form is straightforward:










// all Preferences classes come from java.util.prefs

//





// Read a configuration value

//

Preferences prefs =

Preferences.systemNodeForPackage(this.getClass());

String url = prefs.get("databaseURL", "");




A couple of things are happening here. First, the Preferences API differentiates between system data and user data�system configuration data is machine-wide, while user configuration data is held specific for each user and varies depending on which user is currently executing the code. For most enterprise applications running on a server, we'll want to use the system data, since our servlet and/or EJB code isn't typically associated with a single user. Second, preferences are arranged into hierarchical nodes, just as XML Infoset elements are. By arranging nodes in package-named hierarchies, configuration elements can be partitioned into manageable chunks for easier administration. To make things even simpler, the Preferences API has utility methods (like the one just shown) that navigate the Preferences tree to find the Preferences node that matches the package name of this class. Once we've found the right Preferences node, we can ask it for the value associated with the key "databaseURL"; in the event that no such key is found, the default value, passed as the second parameter to the call, is returned.



At this point, it's fair to ask what data storage mechanism the Preferences API is using under the hood�after all, that's going to be important when considering what we need to do to deploy the application. The short answer is, "It depends." Specifically, the Preferences API allows for different storage layers, called backing stores, to hold the raw data. The default backing store for a Preferences node depends on the system on which you're running the code; in the case of the Sun Win32 JVM, for example, the default backing store is the Windows Registry. (Don't automatically turn down the idea of using the Windows Registry to hold important configuration data. Having heard the horror stories of the Registry over the years, I can say with all seriousness that most of those stories came from COM programmers, who horribly abused the Registry in any number of ways. Used as it was intended�to provide a central repository for configuration information�it performs quite admirably.) In the case of a UNIX system, most JVMs make use of text files again, stored in either the user's home directory or the machine-wide /etc or /usr/etc directories, depending on the JVM and UNIX file system conventions for that system.



In addition to storing strings, the Preferences API can store byte arrays, which are convenient for storing Serializable objects (see Item 71 for more on Java Object Serialization), as well as several of the primitive types (Booleans, ints, longs, doubles, and floats). More importantly, the Preferences API also allows us to register event-callback instances (classes that implement the PreferencesChangeListener interface) that will be called in the event of a modification to the data stored in a Preferences backing store. This means that if the system administrator modifies the data, we can catch that and rescan the configuration data for the new values. This in turn means that changing a configuration value doesn't require a restart cycle of the server to pick up configuration data�in short, giving us the ability to "hot configure" an application.



Because the Preferences API uses different backing stores, if you really don't like storing data in the Windows Registry or the local filesystem (for the reasons mentioned), you can always write your own Preferences class that extends AbstractPreferences to store data in whatever storage medium you wish, including a relational database or LDAP server.



In the event that you really want to view your configuration data in a portable data format, the Preferences API can export preferences data to an XML format (whose document type definition is given in the Preferences documentation), as well as import preferences data from an XML format of similar structure. This makes transmitting preferences data from one machine to another quite simple.



The Preferences API has one major disadvantage, however, and that's because it is an API, with no non-programmatic interface (i.e., some kind of system administrator front end) defined for it. There's no way a system administrator can access that data without using tools to get back at the raw data, something we've already decided is less than optimal in most situations. This is where you as the developer for the project come in: you have to build that user interface.



At first blush, this sounds like it's adding more work to your already overloaded schedule, and that's not a particularly good thing. A couple of advantages come out of doing this, however. First, it eliminates the necessity for administrators to go wandering through the raw data to make configuration changes, and this can help eliminate a few of those late-night phone calls, since now it's not possible for system administrators to accidentally delete crucial data. (Hey, they're just as human as the rest of us.) More importantly, however, providing a user interface gives us the opportunity to do some kind of sanity-checking on the configuration data being passed in�for example, when fed a JDBC URL, we can actually try to open a connection to that URL to make sure the URL works before accepting it. We can screen out obviously invalid conditions (such as a blank e-mail address for system panic notifications), and even put some kind of help into the user interface so that we don't have to field phone calls from administrators who have forgotten what "pool size minimum" means.



If this still sounds like a lot of work, bear in mind that (1) you're still going to have to write an administrative user interface for control operations, discussed next, which can probably be combined with the configuration user interface, (2) there's usually not much configuration data that needs to be established (and the more there is, the greater the need for a clear and comprehensive user interface anyway), and (3) if this system is really going to "run forever," like many enterprise systems are supposed to, a little additional work up front will ultimately pay for itself many times over the system's lifetime.



As mentioned earlier, the other half of administrative needs, besides configuration, is control of the application and/or system as a whole. For example, in a canonical online e-commerce sales application, it's not uncommon for orders to "get lost" within the system. Typically, when this happens, the data will still be there, it just won't be in the state users expect or need to call up the order. Whether this happens due to a bug in the code, a flaw in the requirements, a missed step in the design, or modifications to the system at the eleventh hour is irrelevant�the fact is, the system administrator has a user or customer service rep screaming on the phone because "the system ate the order." Guess who's next on the phone call?



The system administrators, the ones who will be taking care of this particular system (who may in turn be different from those responsible for the servers, the network, and so on), will need some kind of ability to "reach in" to the system and manipulate the data in ways that may violate the standard business practices. Ideally, this power gets used only to help identify bugs, correct accidentally corrupted data, and so on, but we can't ignore the fact that it can also be used sometimes for business purposes, to violate business rules because "the CEO says we need to do it, just this once."



Again, while other approaches�usually those that involve going directly into the database or other data and making changes to the raw data set itself�are possible, they fall into the same traps and disadvantages as configuration. More importantly, however, when working with "live" data, we need to take additional precautions against manipulating the "raw" data because a system administrator may not be as careful with doing so as we would be from code. Think about this for a moment. When was the last time you opened up SQL*Plus (the Oracle database console), the MySQL terminal, iSQL, or any other database console to do your wandering through the system and did all your work under a transaction? Complete with BEGIN TRANSACTION at the start and COMMIT or ROLLBACK at the end? While it's a great trick to know, few database administrators, system administrators, or developers use it when navigating and "fixing" things in the raw tables. So now, if our system administrator suddenly finds that we have three SQL statements that need to be run to fix the latest problem, what are the chances that they will be done under a transaction, isolated from the hundreds or possibly thousands of other transactions going on simultaneously? In a high-load system, direct table navigation and manipulation is a really quick way to really corrupt data, which most people consider to be a Bad Thing, even if it is "just" preferences/configuration information.



Which brings us back to the same conclusion as for configuration options: we need to think about what sort of administrative control features the system administrators will need and provide them via some kind of user interface console (properly protected by well-guarded authorization checks, of course�see Item 63). This console doesn't have to be a marvel of modern user interface design; keep it as simple as necessary, and don't spend a whole lot of time making it user-friendly. In fact, some would argue that because we don't want the average user to be able to use it should they accidentally wander into it, maybe it shouldn't be easy to use at all. It does need to provide system administrators with enough control that they can correct for any foreseeable problem, bug, or system flaw that arises.



Unfortunately, unlike configuration data, identifying the control options a system administrator needs is much more difficult than applying a simple heuristic. One facet that almost all systems have to at least some degree is user management�administrators need some way to add users, remove users, and change user options (passwords, roles, and so on) as well as any role management that goes along with a role-based authorization system (as discussed in Item 63). We could create a simple SQL data console that allows system administrators to execute SQL statements against the database(s) used by the system, but that essentially just grants them another back door into the raw table structure of the database, something we've already decided isn't a great idea. Instead, however, we can build a higher-level model, where we can pull items out of the tables, manipulate them, and put them back under a strictly controlled user interface (so as to preserve appropriate transaction boundaries). This way, administrators can make the necessary data modifications to the live data without transactional "leakage" to worry about.



Another common item that typically falls under administrative control is that of running reports on the database behind the system�unless end users are required to execute one or more reports as part of their business process, the act of running a weeks-end or months-end report typically falls on the shoulders of the operations staff. Some reports are canned, meaning we can hard-code the SQL for the report directly into the code, but most of the time, a report needs some kind of dynamic data variable or criteria selection. You have two choices: let administrators write their own ad hoc SQL queries to get the data back that they want, or bury the SQL behind a rich-enough user interface so that administrators don't need to know SQL to run the report. (Frequently, as it turns out, you'll want both choices, since no matter how rich you make the user interface, there will always be somebody who asks how to run a report based on criteria that's not part of your user interface.)



By the way, in case you were wondering whether you could get away with not providing this sort of functionality or user interface, keep something in mind: if you don't have this user interface, and fixing an application-domain problem, like removing the user credentials and configuration for a terminated employee, requires going into the raw data, chances are likely that you're the one who's going to be doing it, not the system administrators. Barring that, you're going to have to write something to provide that user interface shortly after the system ships, anyway, so you might as well do it up front and get it over with, and maybe earn a few "oohs" and "aahs" from your operations staff (and, dare we say it, upper management?) in the process.



Remember, like deployment and monitoring, careful attention to detail here can not only improve your relationship with your system administrators but also spare you some unwanted late-night interruptions.













     < Day Day Up > 



    No comments:

    Post a Comment