Salut,
Emmanuel Lecharny wrote:
Hi, I'm currently reviewing the Filter chain we are using internally.
This is one of the most appealing feature we have, as it allows us to
design a versatile handling of incoming and outgoing messages. However,
in many respects, the current implementation is pretty complicated, and
could benefit from some refactoring.
I will first try to summarize what we need, and then, from these needed
features, I will try to define what should remain, and what could be
change and improved.
1) What we need
---------------
- Incoming requests must be handled in a pipeline, where filters are
executed one after the other, in a predefined order.
- This order is defined by the protocol designer
- It can change during the processing, for instance if we need to set up
some logging for debug purpose
- Another case would be that some filter can be added or removed
depending on the current message state (a good example could be a
multi-layer protocol decoder)
- This pipeline is a two-way pipeline : it handles incoming requests,
and outgoing responses
- The filter chain is not static, but it should be thread safe, so any
kind of modification must *not* lead to some exception (NPE or
inconsistent state)
Can you made this behavior configurable? In a project I'm working on :-)
we made this configurable. e.g. a filters can be thread safe or not.
When not safe, we internally pool filters. That might sound a weird
design, but we have seen application that needed to write
'statefull'/thread unsafe filter. The performance penalty is limited to
WorkerThread that poll for instance of those filters...which is not that
significant.
- Passing to the next filter should be possible in the middle of a
filter processing (ie, in a specific filter, you can do some
pre-processing, call the next filter, and do some post-processing)
Would it make it "too" complicated for users? What I did in Grizzly was
to split the task into 2 operations (Filter.execute(),
Filter.postExecute()). The execute() method return a boolean to telling
the chain to invoke the next filter or not. Then, in reverse order, we
invoke the postExecute() on the previously invoked filters. But I might
be wrong here :-)
- We should be able to use different pipelines depending on the service
(filters can be arranged differently on the same server for 2 different
services)
That one sound interesting. I'm curious to find how you will detect
which pipeline to invoke. You will need some Mapper right?
- Even for two different sessions, we may have different chain of
filters (one session might use a Audit filter while the next is just
fine without this filter)
- We want to decouple the message processing from the socket processor,
using a special filter which use an executor to process the message in
its own thread
Yes that one will for sure improve performance :-)
(I wish I didn't forgot anything)
and, ultimately :
- It should be easy to debug
- it should be FAST... :)
All those features describe the perfect system :). How what we have fits
this beautiful picture ?
2) Possible improvements
------------------------
The first big point is that we currently have one single chain, when
having two could help a lot. The current chain is two-ways, with forward
links and backwards link. As we have to handle incoming requests but
also write outgoing response, it has to be two-ways. But one single
chain is certainly not the best way to handle this. There is no reason
why the outgoing chain should be the same than the incoming chain. We
may even not use the same filters !
Proposition (a) : Let's create two chains instead of one : a
reader-chain/incoming-chain and a writer-chain/outgoing-chain.
One other big issue is the way the chain is created and used. Here is,
for a very simple chain, the stack trace we obtain (we just have a
ProtocolCodecFilter into the chain, nothing else) when an incoming
message is being processed, from the bottom to the top (o.a.m stands for
org.apache.mina in this trace)
Thread [NioProcessor-1] (Suspended)
o.a.d.agent.ldap.LdapConnectionImpl.messageReceived(o.a.m.core.session.IoSession,
java.lang.Object) line: 597 <------ here, we call the handler ...
------------->
o.a.m.core.filterchain.DefaultIoFilterChain$TailFilter.messageReceived(o.a.m.core.filterchain.IoFilter$NextFilter,
o.a.m.core.session.IoSession, java.lang.Object) line: 755
o.a.m.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(o.a.m.core.filterchain.IoFilterChain$Entry,
o.a.m.core.session.IoSession, java.lang.Object) line: 440
o.a.m.core.filterchain.DefaultIoFilterChain.access$5(o.a.m.core.filterchain.DefaultIoFilterChain,
o.a.m.core.filterchain.IoFilterChain$Entry, o.a.m.core.session.IoSession,
java.lang.Object) line: 435
o.a.m.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(o.a.m.core.session.IoSession,
java.lang.Object) line: 835 <------ here, we call the next filter in
the chain, which is the Tail filter ------------->
o.a.m.filter.codec.ProtocolCodecFilter$ProtocolDecoderOutputImpl.flush()
line: 539
o.a.m.filter.codec.ProtocolCodecFilter.messageReceived(o.a.m.core.filterchain.IoFilter$NextFilter,
o.a.m.core.session.IoSession, java.lang.Object) line: 265 <------
here, we jump to the ProtocolCodec filter ------------->
o.a.m.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(o.a.m.core.filterchain.IoFilterChain$Entry,
o.a.m.core.session.IoSession, java.lang.Object) line: 440
o.a.m.core.filterchain.DefaultIoFilterChain.access$5(o.a.m.core.filterchain.DefaultIoFilterChain,
o.a.m.core.filterchain.IoFilterChain$Entry, o.a.m.core.session.IoSession,
java.lang.Object) line: 435
o.a.m.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(o.a.m.core.session.IoSession,
java.lang.Object) line: 835
o.a.m.core.filterchain.DefaultIoFilterChain$HeadFilter(o.a.m.core.filterchain.IoFilterAdapter).messageReceived(o.a.m.core.filterchain.IoFilter$NextFilter,
o.a.m.core.session.IoSession, java.lang.Object) line: 121 <------
here, we enter into the chain starting on the Head filter
------------->
o.a.m.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(o.a.m.core.filterchain.IoFilterChain$Entry,
o.a.m.core.session.IoSession, java.lang.Object) line: 440
o.a.m.core.filterchain.DefaultIoFilterChain.fireMessageReceived(java.lang.Object)
line: 432
o.a.m.transport.socket.nio.NioProcessor(o.a.m.core.polling.AbstractPollingIoProcessor<T>).read(T)
line: 588
o.a.m.transport.socket.nio.NioProcessor(o.a.m.core.polling.AbstractPollingIoProcessor<T>).process(T)
line: 549
o.a.m.transport.socket.nio.NioProcessor(o.a.m.core.polling.AbstractPollingIoProcessor<T>).process()
line: 541
o.a.m.core.polling.AbstractPollingIoProcessor<T>.access$7(o.a.m.core.polling.AbstractPollingIoProcessor)
line: 538
o.a.m.core.polling.AbstractPollingIoProcessor$Processor.run() line:
873 o.a.m.util.NamePreservingRunnable.run() line: 65
java.util.concurrent.ThreadPoolExecutor$Worker.runTask(java.lang.Runnable)
line: 650 java.util.concurrent.ThreadPoolExecutor$Worker.run() line:
675 java.lang.Thread.run() line: 595
As we can see, for a very simple example, we have a 13 line stack trace
just to go from the read() method where the incoming bytes are received
up to the Ldap handler, traversing 3 filters in the mean time, out of
which the Head and Tail filters does not seems to be that necessary...
We should not have to traverse such a high number of methods in order to
process the filters. Not only this is time consuming (as we have to
invoke as many methods as we have lines in the stack trace, with all the
arguments to pass), but when it comes to debug filters, it's an absolute
nightmare. And we have injected ONE single filter in the chain !
Agree :-)
Proposition (b) : Remove the Head filter. Currently, the Head filter is
just used to gather some statistics, and to inject the outgoing message
to a blocking queue. Statistics belong to a specific filter, we don't
have to compute them for every single incoming message. Writing the
outgoing message to a queue is not something we should do in a filter :
it's the result of all the chain processing, and the method which
invoked the chain has to do this job.
Proposition (c) : Remove the Tail filter. For the very same reason, this
filter is useless. It gather statistics (not it's job...), and them
forward to a handler (cf farther about this handler).
When we have processed the incoming message, we call a ProtocolHandler,
which is the terminaison point for this processing. There is no special
reason this should be considered differently than a standard filter
Proposition (d) The Protocolhandler should be a filter, like any other one.
We had the same discussion in Grizzly and we came with the same
conclusion :-)
Last, not least, we are using a ChainBuilder to define the filter chain.
This structure is a fake FilterChain, which render the manipulation
tricky, and leads to confusion. I'm fine with the idea of having a
ChainBuilder, but it should be consistent. Another big issue with this
class is that it implements many useless methods (like the ones which
let you add or remove a filter giving its class, or name, or the filter
itself. One single method is enough : we manipulate filters by their
name...
Proposition (e) Simplify the chainBuilder to avoid any risk of
confusion, and removing all the useless methods, just keeping those
which deal with the filter's name (this is true for the IoFilterChain
manipulation itself).
All those changes should not be complicated to implement, and should
also not break the internal logic. It's just a matter of modifying a bit
the existing interfaces and the way we process the chain internally.
From the user perspective, it won't change the way filters are added to
the chain, and for those who develop new filters, or who have
implemented some filters, the modification will be quite minimal.
PS : All those changes need to be validated, as I may have missed some
points. I also suggest that some prototype be written, in a sandbox.
This will be the best way to check if those ideas are sane or insane,
and also to correctly evaluate the impact on existing code.
So, wdyt, guys ?
From an outlier view, that looks promising (and dangerous for the
outlier project :-))
-- Jeanfrancois
Thanks !