Description ----------- This script provides a content filter program for the Stalker CommuniGate Pro mail server that provides an ability to cascade many different content filters. I wrote this to make it easier to run many filters easily and safely. The features include: - You can create a simple list of filters that all must succeed or you can create a complex heierarchical dependency tree of filters, where some filters depend on the results of others, - All filters are run under a manager process, and if any filter does not respond within a defined amount of time it is automatically restarted. - One filter hanging will never break your mail system. Instead, deliveries will fail safe in those situations. - An upper bound time limit of how long one request will take is implemented, regardless of how many filters you are using. This way a request will never take longer than this defined limit before sending a response back to CG. Design ------ The filter is able to communicate with CG as fast as possible, and will never have its input blocked and not ready to be given a request. This is done by splitting the filter into 2 processes. The main process communicates with CG and queues up requests, and the 2nd (manager) process actually processes those requests. The manager spawns all of your filters and communicates with them directly. It works sequentially on all of they filters, defined by the heirarchical structure $config{filters}. It may not be able to handle requests immediately depending on how long each filter takes. This is why the main process will queue up requests with a FIFO in these situations, and always be ready for more requests from CG. The following are some problem scenarios and how they are handled: 1. If the manager takes too long with a request, it is assumed that something is wrong with it or one of its spawned filters. The request currently being handled by the manager fails safe to CG, and the manager is restarted. The manager in turn kills ALL of the spawned filters and restarts them. This is referred to as the request reaching the upper bound maximum time limit ($config{MAX_PENDING}). 2. If a specific filter takes longer than the defined timeout for that filter, that specific filter is restarted. This occurs when the upper bound described in #1 is not reached, but instead the specific filter timeout has been reached. 3. If processing the requests through your filters is 'slow', the FIFO queue held by the main process starts to backlog with requests. A timestamp is kept for all of these pending requests, and if they ever reach the upper bound maximum time limit, they fail safe to CG. These are requests that never had the chance of even getting to your filters because the filters are taking too long. Note that there is no error checking done on the output of the filters you define. Whatever the filters return will be sent back to CG, and the fact that they return something means they are working properly from this script's perspective. If the filters themselves don't work properly, this script will not detect such problems. Serial vs Burst Mode -------------------- When you have dependency filters defined, the script will send requests serially to each filter, waiting for a reply from each filter before proceeding to the next. A consequence of this is that any multi-threaded filters that you'd like to run are essentially made single-threaded. This can be a big performance bottleneck. However, if the following requirements are in place: - ALL of your filters are multi-threaded or they are able to handle many requests at once - You have no 'dependency' definitions in your filter list - Your filter policy is 'ContinueUntilReject' You can run the script in a different mode called 'Burst Mode'. This mode alters the way requests are sent to each filter. Instead of sending requests serially, they are sent in parallel to ALL filters. Also, multiple requests are sent to each filter without waiting for answers to previous requests. In 'Burst Mode', the manager process is more complicated. The manager itself keeps track of what requests are sent to which filters. All of the above features still apply, but the manager process does some extra managing of the requests aside from the main process in order to keep track of which filter is handling which requests. This mode speeds up requests by making use of the multi-threaded subfilters. Also, the first filter that rejects a message will cause a reject sent to CommuniGate immediately, regardless of what the other filters send. Requirements ------------ This has been tested on Linux and Solaris with: perl5 (5.0 patchlevel 5 subversion 3) There should be no extra modules required. Your mileage may vary on other systems, but please let me know your results. The only really lowlevel stuff is select() calls, which I believe PERL implements in all OSes. Setup ----- $config{filters} Configuration is done at the top of the script. You must at least define a proper $config{filters} definition. This is a heierarchical description of what filters to run. The format is such that it is processed recursively, and you can define any number of filters and dependencies. First, lets start with a very simple configuration. Let's say you want to run 2 filters, filterA.pl and filterB.pl. You want processing to work as follows: 1. All messages are first scanned by filterA.pl. If filterA.pl returns OK, then filterB.pl should be scan the message. The return value for filterB.pl is sent back to CG. 2. If filterA.pl returns non-OK, the return value from filterA.pl is sent back to CG and no more processing is performed. 3. If filterB.pl returns non-OK (and consequently filterA.pl returned OK), the return value of filterB.pl is sent back to CG and no more processing is performed. This is really just the same case as #1. 4. If any filter fails to respond in 60 seconds, restart it. Here is the $config{filters} data structure for this: $config{filters} = { policy => 'ContinueUntilReject', filter_array => [ { path => 'filterA.pl', timeout => 60 }, { path => 'filterB.pl', timeout => 60 } ] }; Note that filterA.pl and filterB.pl should be FULL PATHS. This is a complex structure in PERL, whose fields should be straightforward. Just be careful to include the proper {} and []'s in the right places. The policy can be one of: ContinueUntilReject ContinueUntilAccept Normally you will use ContinueUntilReject for just running multiple filters and want to reject the message to CG if any one of them rejects it. Now for a more elaborate example. Say I want to configure 4 filters, myfilt1.pl through myfilt4.pl, each with 60 second timeouts, and described by the following decision tree: message from CG -> first goes to myfilt2.pl myfilt2.pl -> if OK, send to myfilt1.pl -> if non-OK, send to myfilt3.pl myfilt3.pl -> if OK, send to myfilt1.pl -> if non-OK, send to myfilt4.pl myfilt1.pl -> if OK, send to myfilt4.pl -> if non-OK, reject message to CG myfilt4.pl -> if OK, OK message to CG -> if non-OK, reject message to CG This looks very complicated when described, but when you look at the data structure it may be easier to understand. Basically we are making myfilt1.pl 'depend' on the results of myfilt2.pl and myfilt3.pl: $config{filters} = { policy => 'ContinueUntilReject', filter_array => [ { path => 'myfilt1.pl', timeout => 60, dependencies => { policy => 'ContinueUntilAccept', filter_array => [ { path => 'myfilt2.pl', timeout => 60 }, { path => 'myfilt3.pl', timeout => 60 }, ] } }, { path => 'myfilt4.pl', timeout => 60 } ] }; You can see that myfilt1.pl has dependencies defined. These dependencies are simply a recursive structure of filter definitions. You can do this as deep as needed (and before it gets too ugly to read). Most of the times you won't need such elaborate processing, but it's there just in case. $config{MAX_PENDING} = 120 This is the maximum time in seconds that a request should take. It is the upper bound on all requests. $config{LOGFILE} = "/tmp/cascade_filter.out" The logfile. $config{BURST_MODE} = 0 See discussion above on 'Serial vs Burst Mode'. Set this to 1 to enable. Bugs and Suggestions -------------------- If you have problems, please set $config{DEBUG} to 10 and see if that helps pinpoint the problem. When filters die, they are supposed to be restarted. In some cases certain strange errors happen. If you are testing this, please forcibly kill your filters and let me know your results. I have tested with simple filters without issues. Logging is not properly done at this time, as both the manager and main process could very well write to the log at the exact same time. This results in corrupt logs, but I found it rarely happens. If you find any problems or have suggestions, please let me know at valankar@bigfoot.com.