{{announcement.body}}
{{announcement.title}}

Writing a Java Library for 2 AM

DZone 's Guide to

Writing a Java Library for 2 AM

If you have to debug clever code at two in the morning, you are four times stupider than you need to be.

· Java Zone ·
Free Resource

Writing code for a Java library for 2 AM

Writing a Java library for 2 AM is easy with hrorm

We are spoiled by Java. For almost any programming task, you can imagine there are many freely-available, open-source projects you can choose from. If I was managing a team and they came to me suggesting that we write our own ORM framework, I would send them home and tell them to come back when they sobered up.

You may also like: Introducing Hrorm: A Simple, Declarative, Type-Checked ORM

Nevertheless, when I was working on a personal project beginning last year, I could not find what I wanted, and so I began hrorm. I've written some articles about hrorm (for example, here) that describe some aspects of hrorm. In this article, I want to talk about some of the questions I faced in writing hrorm, and why I made some of the choices I did.

Libraries and Frameworks

As an application developer, I prefer libraries to frameworks. I won't try to formally distinguish between the two, but I think most programmers have some idea about the distinction I am making. Libraries are imported by putting a jar file in your classpath; they provide clients with classes and methods; they define interfaces and then provide implementations of them. Frameworks require set-up and configuration and containers; they have interfaces they expect clients to implement; they start up threads and begin doing work before you even call them.

Frameworks can deliver a big bang for the buck, of course. With very little programming work (but perhaps rather a lot of research and learning work), frameworks can allow small teams of developers to build big applications quickly. But it comes with a cost.

Frameworks can be magical, and magic can be unpredictable, hard to debug, have confusing performance characteristics, and make seemingly simple things incredibly frustrating if the authors have not anticipated your particular needs. That's perhaps unfair, and many frameworks are terrific, but I like using libraries better. If something is going to venture into framework territory, it better give me something very valuable for the added complexity and effort a framework demands.

The paradigmatic example in my mind of a library is something like the Apache or Guava collections. A bunch of classes you can use (or not) by just putting a jar file in your classpath. No configuration or set-up needed. And the library just does what you ask of it. If you don't create instances of the classes or call their methods, it doesn't require a bunch of set-up or run threads in your application. And when you encounter a problem, the stack traces make sense; they don't flood your screen with lines of ConfigurationManagerProviderImplementationDirector objects called who knows how, since everything happens via reflection.

Hrorm is a library, not a framework. You put the jar in the classpath and you use what you want. If you just put it in a small corner of your application, hrorm stays there.

Cleverness

One of my favorite programming aphorisms is Brian Kernighan's comment on debugging:

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it."

My corollary to his theorem is that you are twice as stupid at two in the morning when called by the operations team than you are during the workday. And therefore, if you have to debug clever code at two in the morning, you are four times stupider than you need to be.

Hrorm is not a library that is built to impress when you are fully caffeinated and relaxing in the post-doughnut mid-morning surfing session on your dual 30-inch monitors. It's built for the dead of night when you're trying to sleep one-off and you don't want to figure out why a lazy sequence cannot be realized, why all the threads in the thread pool are simultaneously busy-waiting, or why a cache has over a billion entries when there are only a total of a million records in the database.

When you call one of hrorm's methods, it does something pretty simple. It generates some SQL, it passes it along a Connection to the database, and it parses the results into your object model. If something goes wrong, exceptions and stack traces are straight-forward and the SQL being executed is always exposed.

Laziness

Eagerness is simpler than laziness. If you call a select() method on a hrorm Dao<T> object, you get a fully-realized, eagerly created List<T>. There are plenty of times when that works fine, but not always.

One of hrorm's contributors has suggested that hrorm include a lazy-loading, java.util.stream.Stream-style interface. The utility of such an interface is easy to see. When doing an operation on items read from the database, you might not wish to have all the objects you plan to operate on a resident in memory simultaneously.

A method that returned Stream<Entity> could be backed by a javax.sql.Connection that read only some small number of items at a time, as required at runtime. One the stream was exhausted, and the calculation complete, closing the Stream could automatically close the Connection. It's a very reasonable design to solve a real problem. So, why have I resisted it?

"A sequence of elements supporting sequential and parallel aggregate operations."

That sounds somewhat like what we want in the lazy sequence of database elements, but there are some mismatches.

First of all, it's not going to support parallel operations. A Connection backed Stream will only support sequential operations. A good use for Stream is when we want to write some processing code that is impervious to how the underlying Stream works, so when appropriate, parallel operations can be performed transparently. This is certainly not the impression we want to leave the user with.

But that's not my main objection. What I really worry about is how laziness violates some deeply held assumptions most of us have about the locality and scoping when writing Java. Here's an example showing some code that looks like hrorm but uses a method that does not actually exist.

try {
    Connection connection = connectionFactory.get();
    Dao<Foo> fooDao = fooDaoBuilder.buildDao(connection);
    // this method does not exist
    Stream<Foo> fooStream = fooDao.lazySelect();
} catch(HrormException ex){
    // hrorm wraps all database errors with its own extension of RuntimeException
    handleDatabaseProblems(ex);
}
processStream(fooStream);


That code has a problem that many readers probably noticed: the processStream() method should be inside the try-catch block since it still uses the Connection and can throw database exceptions. And now the reader might object: sure, that's a problem, but people understand how live database streams work, and the docs can explain it, etc. Only a noob would make an error like that.

My answer is two parts. One is that I would rather write a library that is hard to use wrong than one that is possible to use right. But also, I think passing over bugs like the above as ones only bad developers would make is wrong. Sure, most of us will avoid a bug like that, working alone on a piece of code that we are designing, but that's not how applications are developed.

Instead, what would happen is something like this. Developer A writes some code using the lazy stream and does all the exception handling correctly. A few months later, Developer B needs to make some changes, and moves some of the stream processing to a different location, further distant from the bulk of the database interactions. Developer B is careful and scrupulous though and sees that the exception handling needs to be in place now in the new location. That works fine until Developer C works on the processing and just sees that there's a Stream<Foo> being handled. During refactoring, the annoying try/catch block is removed. All the tests pass, and even running the application several times shows no problems.

Finally, after everything has been in production for several months, one evening the network team is making some changes that cause the application and database servers to be temporarily partitioned. What would normally be properly handled by the database error handling code, even if it resulted in errors visible to the user, would be cleaned up and reported properly.

As it is, only the top-level application exception handler sees the problem, and it does not do the proper clean-up, just logs something and moves on. Now, the application's database connection pool is being poisoned, since exceptions are happening on the connections, and they are not being cleaned up. Eventually, the application is up and running, but becomes unresponsive, and won't allow users to login.

That's your 2 AM problem. Requests are made, but they just block and do not log any errors. The network team is paged, but they report their changes were completed and they can ping to and from all the relevant servers. The database engineers are also woken up, but they can connect to the database fine and have no reason to think anything is wrong.

Finally, the software engineers start looking at the server and cannot see anything wrong. There is nothing in the last few releases that could have changed to make something like this happen. The exceptions that led to the problem happened hours ago, and in any case, they don't look like they are anything particularly worrying: just database connection problems when the network was being reconfigured.

Eventually, a hard reboot solves the problem and everyone goes back to bed. In the morning, some of the developers half-heartedly look at the problem, but no one really understands what happened. A time bomb exists in the code. Maybe someone will find it eventually, but there are some real hurdles to overcome: the introduction of the bug to the program is distant in time from its discovery, it's hard to reproduce, the side-effects visible in the logs do not directly point to the root of the problem. Maybe everyone will just accept that whenever there are network changes, such-and-such an application needs to be rebooted.

Lazy streams in Java are a powerful abstraction that allows some programming tasks to be expressed in very elegant and expressive ways. But our intuitions about how Java code executes do not match their reality. Lazy objects break locality and happens-before relations. Like a Future, they seem to remove the guarantee that the semi-colon separator is a sequence point: dividing the code into two parts, the first of which is totally completed before anything happens in the second.

If Not Laziness, What?

The problem that a stream interface could solve is real. How to code against a selection of elements without putting them all into a fully realized collection at one time? Hrorm provides a method, foldingSelect, that accomplishes more or less the same thing as a Stream API would: The user can write a function that folds over the result set, accumulating a result while never realizing all the data from the database simultaneously.

It's true that for some developers using a fold does not feel like a natural operation (though if you use streams, it's more likely to), but you can have the benefits to memory usage that laziness brings without the added cognitive overhead.

Conclusion

Applications become hard to work on when they become too complicated and then developers start to lobby to replace them. It can happen long before the intrinsic complexity of the work the application is doing would make you think. Even if the developers working on the program are talented and produce decent code, sometimes applications collapse from the weight of the scaffolding that should be supporting them. Too many annotations, too much reflection, too much XML configuration, too many secret caches and delayed execution, and not enough expressive, easy-to-understand Java code.

I am too jaded to make claims about hrorm like "it's bug-free." But I think it tries to do what a developer would expect it to. When you call the insert() method on a Dao, it pulls the data from the entity object's getters, generates a SQL INSERT statement, and executes it. There is no magic, but sometimes, when you just want to save something to a database, magic is not what you need.

Topics:
java ,libraries ,laziness

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}