[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
RE: [Orekit Developers] Multi-threading problems in OREKIT
Hello,
Here are some thoughts about the multi-threading issues and the
direction we are aiming at. We have tried to combine requests from
several different users (some have been expressed publicly in this
list or in the bug tracker, some have been directed directly to the
Orekit team). This is mainly a trade-off between various threads and
includes contributions and ideas from several persons.
I would like to present it here so everyone interested can give their
opinion about it.
The ideas are based on the reasons why we have some singletons in
Orekit. Singletons are used for caching and are a really important
feature. Without caching for example, the performances for Earth
precession-nutation computation would be really really poor.
Many server-based applications that use multi-threading now do not
handle Threads by themselves anymore (as was the case in the Java 4
era). They rather use Javas 5 ExecutorService, and in most cases the
implementation they use are thread pools provided by the factory
method Executors.newFixedThreadPool. Such thread pools do *not*
guarantee the same thread will serve all requests from a specific
client. In fact, when a request is submitted to the executor service,
a random idle thread is waken up to execute it and once completed the
thread is put again in the idle pool where it can be picked up later.
Threads are not started or stopped, they are recycled. There are no
correlations at all between threads and requests and they are bound
together almost at random. This clearly rules out ThreadLocal
singletons as their caching is very likely to be invalidated in this
case (see below). In fact, using ThreadLocal puts a very stringent
assumption on how the application handles its threads so it cannot be
reliably used in a general purpose library of intermediate level like
Orekit. It is safe only at application level or at dedicated support
library level where the global architecture is known in advance.
Some people have also proposed using memcached to handle such data.
Memcached is for sure a complete solution, but seems rather big and
would not scale down. It would also be a huge dependency for Orekit,
so this solution was also discarded.
So we have some shared data cache we can't remove, and we must assume
threads and requests are not bound together, and we don't want our
cache to be invalidated at each request.
However, there is still hope. We can assume that a server would serve
only a small number of remote clients (say a dozen or something like
that) and that each client will issue requests that do have some
temporal locality, i.e. they all correspond to some reduced time
range. One very important property shared by our singletons is that
they are all date based. This is the major trick we used in the
following design.
As an example, we consider the following use case. Lets consider a
typical operational ground system where several engineers work in
parallel. At some time, we may have application A performing some
computation for orbit determination on data centered around last week,
application B computing next cycle maneuvers for the next 6 months,
application C doing some history browsing on data covering last year,
and application D doing real time monitoring on current date. In this
case, we have four applications each using a different time range. If
these application use a shared central server to perform conversions
between Earth and inertial frames for example, the server will get
random requests in these four time ranges only, and will need to cache
very large Earth Orientation Parameter data in each case.
The current version of Orekit fails miserably in this case as
precession/nutation computations are cached in a singleton and the
cache covers only a few days. So when a conversion request from one
application is served just after a request from another application,
the new request appears to be out of current cache range, so cache is
invalidated and the complete cache must be recomputed (which is
computing intensive). Then the request is answered, and the cache may
be invalidated again just after that. We get cache misses all the time
and the cache finally hinders performances a lot instead of improving
them.
So what do we propose to solve this problem?
We propose to set up a cache dedicated to sequences of TimeStamped
objects that would handle a small number of disjoint ranges at the
same time (in our use case, four ranges would be needed). This cache
must be thread safe and it must store large data sets. The cache would
be created empty and be able to add new entries. The various ranges
would be allocated as needed up to the user-configured max number.
Ranges could be configured with a max number of points or max duration
span to avoid huge memory consumption. Typically, a real-time
monitoring could set up ranges limited to a few hours used as a
rolling range as time flows, but station-keeping simulators would ask
for almost month-long ranges as they go back and forth in the
station-keeping cycle trying to optimize maneuvers. Entries within a
single range would be invalidated in rolling cycles depending on
request dates (inserting points at one side would involve dropping
points at the other side, taking care time-locality is reversed
according to forward or backward propagation). When a new range is
needed, it would be allocated up to the max number without
invalidating other ranges, but if the max number of ranges is exceeded
(which would be a user configuration mistake), then another range
would be dropped, using some standard cache policy, typically LRU
(Least Recently Used).
We propose to use a single class for all these caches:
public class TimeStampedCache<T extends TimeStamped> {
}
The T parameter would be the type of data cached (UTCTAIOffset for the
UTC scale, or similar classes for EOP or planetary ephemeris). The
cache would only provide thread safety, ranges handling, entries
handling within the ranges. It would not provide any computation on
the entries.
This class would be used by higher level objects which would not store
the complete data, but only a few reference to the elements they
currently need. For example an object that would need simple or no
interpolation like UTC-TAI would store only two references to the
previous point and next point, whereas an object that would need
higher degree polynomial interpolation like precession/nutation would
store a sub-array of a dozen references. These objects being small,
they could be reallocated, copied, made immutable if desired,
depending on the need. They would rely on the TimeStampedCache methods
to retrieve the references. Three methods are foreseen for this in the
TimeStampedCache class:
T[] getNeighbors(final AbsoluteDate central, final int n)
throws OrekitException;
T getBefore(final AbsoluteDate date)
throws OrekitException;
T getAfter(final AbsoluteDate date)
throws OrekitException;
This is a follow-on on the idea to have small local data in small
objects that can exist in many instances, and to have the big cache in
a singleton. So the small objects could avoid handling multi-threading
if they are built to be immutable for example, or they could handle
simple synchronization or locking if they are mutable but rely on an
already thread-safe class to retrieve their data set.
In order for the TimeStampedCache class to populate its ranges as
requests arrive, it needs some way to compute the entry points. This
is done by providing the class with a generator at build time. A
generator is simply an implementation of the following interface:
public interface TimeStampedGenerator<T extends TimeStamped> {
/** Generate an entry to be cached.
* @param date date at which the entry should be generated
* (may be null if no entry can be generated)
* @exception OrekitException if entry generation is attempted but fails
*/
T generate(AbsoluteDate date) throws OrekitException;
}
Note that some dedicated logic is already foreseen to cope with time
ranges that can never be extended (like leap seconds which did not
exist prior to 1972) but should not trigger errors if a request is
made far in the past, and to also cope with time ranges that should
theoretically be extensible but due to missing data cannot provide
points at some requested datrd and should trigger an error. This is an
implementation detail we will not describe in this message.
With this setting, a cache for UTC-TAI would provide a generator that
is based on reading the UTC-TAI history file, a cache for planetary
ephemeris would provide a generator that is based on reading JPL or
IMCCE files, a cache for precession/nutation would provide a generator
that is a self-contained computation using IERS conventions.
Another point worth mentioning is that TimeStampedCache could handle
multi-threading using the standard ReentrantReadWriteLock from the
concurrency package instead of synchronzed blocks. Such locks allow
multiple read at the same time, which is OK and scales well. They
guard against multiple writes (i.e. cache extension or invalidation)
which should never happen simultaneously.
So here are our current thoughts about this. We would like to know
what other people think about this.
Best regards,
Luc
----------------------------------------------------------------
This message was sent using IMP, the Internet Messaging Program.