16 March 2009

Perversion Of Control

Once I had a chance to observe the quite funny picture: the young man fights with baggy pants without belt. Looking on his tremendous efforts to get the equilibrium when pants are still on him but sagging as low as it is possible, I tried to understand why he does this. It looks strange, isn’t it? If to do this for hour by hour, day by days, life may become difficult. What reason to do this? The reason (thanx my wife) was that it is very fashionable thing now. Fashion! People implies fashion often to explain an irrationality.


Obviously, in any area of activity, people very like the fashion and the software development is not an exception. My first thought is about fashion whenever I see the application overloaded by “fancy” tools and frameworks without any reason to do this. Yes, there are a lot valuable reasons that explain the importance of the fashion, even in software development. Quite often, popular things are really helpful and the fashion sometimes is only one thing that makes attractive a project for developers, especially in open source. But is it price to pay for? Sometimes an application become more like Christmas tree, especially after different kind of integrations due blind following to a fashion.

Recently I came across an issue with java.nio.channels.OverlappingFileLockException in application that uses Apache Camel. The details of this integration is not so important, just need to note that it was quite painful, and, even after some time, it is still require the different “crutches” just to prevent the system falling. But let’s do not distract on this for now, it is happened and it is only fact that we have. At this time, the re-locking of the data source occasionally happened and we should fix it. Even do not fix, just to mask somehow. Because, as it is note above, the transport layer was fatally sick and serious surgery is required, on which, unfortunately, there is neither the time nor any desire.

So, what we need to do originally, is silently skip the mentioned above exception and that is all. It will be enough, because it is really possible to get such ridiculous situation. It seems the Apache Camel provides the pooling resource only. While in our application, typically, the only one usage of constructed route required. But fair enough about reason, it would be expected, but do not provided by design. First look at the stack trace points to org.apache.camel.component.file.strategy.FileProcessStrategySupport#begin(FileEndpoint, FileExchange, File) :

Exception in thread "main" java.nio.channels.OverlappingFileLockException
        at sun.nio.ch.FileChannelImpl$SharedFileLockTable.checkList(Unknown Source)
        at sun.nio.ch.FileChannelImpl$SharedFileLockTable.add(Unknown Source)
        at sun.nio.ch.FileChannelImpl.lock(Unknown Source)
        at java.nio.channels.FileChannel.lock(Unknown Source)
        at org.apache.camel.component.file.strategy.FileProcessStrategySupport.begin(FileProcessStrategySupport.java:64)
        at org.apache.camel.component.file.FileConsumer.pollFile(FileConsumer.java:148)
        at org.apache.camel.component.file.FileConsumer.pollFileOrDirectory(FileConsumer.java:88)
        at org.apache.camel.component.file.FileConsumer.poll(FileConsumer.java:64)
        at org.apache.camel.impl.ScheduledPollConsumer.run(ScheduledPollConsumer.java:65)
        at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
        at java.util.concurrent.FutureTask$Sync.innerRunAndReset(Unknown Source)
        at java.util.concurrent.FutureTask.runAndReset(Unknown Source)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(Unknown Source)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.runPeriodic(Unknown Source)
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.lang.Thread.run(Unknown Source)

It is clear that the org.apache.camel.component.file.FileConsumer.pollFile is just trying to acquire lock for a specific file (by org.apache.camel.component.file.strategy.FileProcessStrategySupport). Close attention to FileConsumer class help us realize that on failure (if there is any) the raised exception is caught and handled in org.apache.camel.impl.DefaultConsumer.handleException(Throwable) which, in turn, has delegated the processing to the org.apache.camel.spi.ExceptionHandler. So, the idea to just override this handler looks pretty good, but, sometimes the simple things become very difficult. The FileConsumer is instantiated by FileEndpoint that is created by FileComponent that, in turn, instantiated by ReflectionInjector within the default Camel Context implementation (i.e. DefaultCamelContext). May be there is reason of such deep division of responsibility, it looks very nice but, why it is so difficult to specify the handler? Especially if DefaultConsumer has the accessor for this. But we will not be so strict, sometimes it happens: accessor was added but access to it was lost. Actually it does not wonder in such multi-level construction. Let’s look forward for another way to do so.

Looking through the code it seems the Apache Camel widely uses the extension idiom, at least for component and strategy implementations. It is mechanism for extension support, so, the provided functionality may be easily extended in future. All that is need to do, just provide the details on the required extension in the specific properties file in META-INF/services folder. Very useful idiom, and my first thought was I can re-define the file component implementation. This may exclude the Injector and CamelContext instances from this chain of responsibility: not so much, but not so bad. However, the file component was defined in Camel Core JAR. Hmm... Is it extension or not? According to common sense extension should not be in the core, is not? If it is not extension (i.e. the default component) why it is specified as extension? If it is extension, why it is specified in Core JAR? Hopefully there is reason to do so. It is not to keep, near at hand, how to do the extension. However whatever reason is, it will not help us to do what we should.

Anybody who familiar with the standard application class loader, may note that there is still ability to redefine the file components: provide the properties before Core JAR in application classpath: the first occurrence only will have effect. Not a big deal, but that was not option for us, because the application that we need to fix is building the classpath just by listing the JARs in folder, so there is no way to control their order. The Class-Path header field in the manifest requires changes in build that would look unconvincing for such purposes. Any attempt to override the default org.apache.camel.component.file.FileProcessStrategy implementation will not option for us with same reason.

However, let’s go forward, may be there is another way. While search another way to specify error handler I found the quite interesting stuff in org.apache.camel.impl.DefaultComponent#createEndpoint(String) that processes the URI and injects the properties specified by query parameters. I can remember time when was busy more with Web applications. Really, "all new things is just forgotten old". The context, parameters... there are, definitely, lack of attributes and session! Fortunately, due absence so important things, I left the idea to inject the error handler instance by query parameter. Curiously, just to imagine, how many properties you can provide in URI query? How long the URI should be do not look so madder? And, is it really good place for parameters? What about authentication credentials, they are specified in the URI too?! The basic authentication nervously smokes aside.

By the way, I was wondering how this URI is processed. It was fun to find the code from Sun’s URI class here. What it is doing there? Seems it was copy&pasted just to get rid the URISyntaxException. Why do not check validity on its instantiation in client? But, probably, the funniest stuff was observed in the thread stack trace for FileConsumer instantiation in Apache Camel 1.4. I skipped the full trace, just highlighted a small part:

at org.apache.camel.component.file.FileEndpoint.<init>(FileEndpoint.java:70)
        at org.apache.camel.component.file.FileComponent.createEndpoint(FileComponent.java:54)
        at org.apache.camel.impl.DefaultComponent.createEndpoint(DefaultComponent.java:79)
        at org.apache.camel.impl.DefaultCamelContext.getEndpoint(DefaultCamelContext.java:273)
        at org.apache.camel.util.CamelContextHelper.getMandatoryEndpoint(CamelContextHelper.java:52)
        at org.apache.camel.model.RouteType.resolveEndpoint(RouteType.java:99)
        at org.apache.camel.impl.DefaultRouteContext.resolveEndpoint(DefaultRouteContext.java:102)
        at org.apache.camel.impl.DefaultRouteContext.resolveEndpoint(DefaultRouteContext.java:108)
        at org.apache.camel.model.FromType.resolveEndpoint(FromType.java:73)
        at org.apache.camel.impl.DefaultRouteContext.getEndpoint(DefaultRouteContext.java:77)
            ...

Look, to create the endpoint, from the Camel context instance we delegate a call to the model particle "FromType". Though delegating back to context (forgot something?), after – to model particle that represents route (deja vĂș?), again back to context (definitely is it possible, to lost the brain on so long way!), though to corresponding component and on final (unbelievable, we did this!) the target instance is created. Tremendous work is performed. I mentioned version the Camel where I spot this because in newest one (at least in 1.6) it is fixed already. But who knows which other fun stuff was added there? Anyway, after this I decided to stop further research. Just got enough fun on this way of sliding into irrationality.

I do not like to make the conclusion. Usually, people listen only what they want to hear. I think, those who read this will make the conclusion without my help. Just as summary I only can add that last drop that fulfill the glass of water, behind the demonstration of "the transformation quantity into quality", just spills the water. To mind only the fashion when doing the integration, you may look like lad with baggy pants. So, what about solution? In fact we have the following options:

  • Change your application so, to fits the new requirements: usually it is the best way. Very often the problem is in the your code and / or design.
  • Patch the 3rd party tool or wait that somebody patched it for you: the thorny way, but sometimes only one option.

I choose the first way but in shorter variant: instead make changes in application loader to control the order of JARs in classpath I directly override the whole responsibility chain. It looks ugly but it does what should do:

final CamelContext context = new DefaultCamelContext() {
        @Override
        protected Injector createInjector() {
            return new ReflectionInjector() {
                @Override
                @SuppressWarnings({"unchecked"})
                public <T> T newInstance(final Class<T> type) {
                    if (FileComponent.class == type) {
                        return (T) new FileComponent() {
                            @Override
                            protected Endpoint<FileExchange> createEndpoint(
                                    final String uri,
                                    final String remaining,
                                    final Map parameters) throws Exception {
                                File file = new File(remaining);
                                FileEndpoint result = new FileEndpoint(file, uri, this) {
                                    @Override
                                    protected void configureConsumer(final Consumer<FileExchange> consumer)
                                            throws Exception {
                                        super.configureConsumer(consumer);
                                        if (consumer instanceof FileConsumer) {
                                            ((FileConsumer)consumer).setExceptionHandler(
                                                    new LoggingExceptionHandler(getClass()) {
                                                        @Override
                                                        public void handleException(final Throwable exception) {
                                                            if (exception instanceof OverlappingFileLockException) {
                                                                // ignore
                                                                return;
                                                            }
                                                            super.handleException(exception);
                                                        }
                                                    }
                                            );
                                        }
                                    }
                                };
                                setProperties(result, parameters);
                                return result;
                            }
                        };
                    }
                    return super.newInstance(type);
                }
            };
        }

    };

Hopefully at one day the Camel will sort out its IoC...

No comments: