New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Thread safety changes #2
Conversation
some more docu fixes fatel -> fatal changed the way of calling some rework some better comments and win32.mak fix more documentation trying to figure out why win32 fails test before you commit another try to work around windows files and processes maybe this time maybe this merges update 1 mostly soenke MultiLogger and Logger have names more docu unittest fix some better comments and win32.mak fix Conflicts: std/logger.d antoher doc fix less code dup and more log functions docs are now generated some more fixing speedup some threading forgot some better docu gen and more testing two new LogLevel, most functions are now at least @trusted Tracer functionality fatal delegate some phobos style fixes another bug bites the dust version disable enhancements default global LogLevel set to LogLevel.all logLevel compare bugfix delete of dead code tab to whitespace bugfixes, reduandency removal, and docu multilogger was logging to all it childs without checking there LogLevel in relation to the LogLevel of the LoggerPayload. Some constructors where redundant. more examples and more documentation some fixes NullLogger and moduleName wrong doc I splitted the multi logger implementations out loglevelF to loglevelf in order to make phobos style think writefln document building win makefile fixes some optimizations thanks to @elendel- some whitespace some updates from the github logger project * stdio- and filelogger now use a templatelogger base to reduce code makefile fixes makefile fixed the unittest made sure the stdiologger is set up at the end some comment fixes finding the filelogger segfault closed another file a lookup fix darwin unittest fail output more diagnostics * more documentation for the templatelogger and multilogger * it breaks the log function api * log and logf now behave as their write and writef counterparts * for logging with an explicit LogLevel call logl loglf * for logging with an explicit condition call logc logcf * for logging with an explicit LogLevel and explicit condition call * loglc loglcf * the log function with an explicit LogLevel like info, warning, ... * can be extended into c, f or cf and therefore require a condition * and/or are printf functions something is still interesting rebase and lazy Args saver file handling whitespace some updates tracer is gone and more doc updates
* StdIOLogger prints now threadsafe * concurrentcy hang fix
* makefile fix
That was my concern as well. I understand the original design was intended to be simple, with loggers invoked essentially directly with configuration happening essentially statically. Such a model comes with a number of design restrictions if it is intended that the client can be agnostic about which logger is being used (a reasonable expectation in most scenarios as logger configuration frequently occurs through configuration files and runtime selection rather than compile-time.) It's fine to have such restrictions in the design as long as they are deliberate, well communicated and consistent. |
An example of limiting by design could be the following:
Using such a design greatly simplifies the kinds of inter-thread issues we have to deal with. I feel like that is more like what Robert had originally intended. Once you allow arbitrary reconfiguration of the logging system, re-entrancy or multiple loggers a-la the MultiLogger the system must become larger and more complex, perhaps beyond the original intentions. |
I know some web server folks wont be happy at all. But message passing is not really what I want to get my hands into. I have no experience in the internals of big logging frameworks like log4j and no understanding of web server requirements. As far as this PR is concerned logging is now thread-safe, which is already a net gain. |
yeah message passing is way to heavyweight. This is something that should be build on top of std.logger |
One question i have about the mutex in Logger - it appears its purpose is to protect unsynchronized access to two member variables: logLevel_ and fatalHandler_. As a result, all log methods must acquire this mutex in order to perform any logging since they all use both members. However, they hold this mutex long enough to make calls to user code (Logger impls.) This should not be necessary and can be tidied up. First, as it stands there is no guarantee of the state of the Logger at the time the log function is entered with respect to the content of logLevel_ or fatalHandler_. They are what they are once we enter the mutex. Further, they are not synchronized with respect to each other either, as they can be set individually and each setting takes the mutex on its own. With this behavior, acquiring the mutex for the whole duration of each logging call (spanning the Logger impl) is unnecessary - instead, hold the mutex just long enough to snapshot the logLevel_ and fatalHandler_ values into locals, then proceed with the impl calls without the mutex. This can be improved even further by moving logLevel_ and _fatalHandler into an immutable LoggerConfiguration class which may be exchanged any time the settings get updated. Again, interactions between the logging system on different threads do not have any ordering guarantees (nor can the logging system make such guarantees) so we are free to change that configuration at any time so long as within the logging functions we operate on only a single configuration (which we can snapshot simply by taking a reference to it.) Once you have done this, the only purpose mutex serves now is to prevent Logger impls (that is, begin, part and finish) from being entered simultaneously on several threads. To my mind this is functionality best deferred to the Logger derivation, which may have its own strategy for dealing with multithreading (a-la Robert's suggestion that those who need additional functionality can extend Logger.) This can be done by having beginLogMessage setup a context in which the rest of the message is accumulated and dispatched. Some D'ish pseudocode follows for the various logImpl()s : { (if I have misconstrued some D functionality please let me know) beginLogMsg is now responsible for setting up whatever synchronization may be necessary based on the requirements of the logger. The "standard" loggers could inherit from MutexSynchronizedLogger whose beginLogMsg call acquires a mutex and whose finishLogMsg call releases that mutex. Asynchornous loggers may return a context which simply accumulates the message parts then posts the completed request to the queue without acquiring any longer-standing mutex. By moving the calls to logMsgPart and finishLogMsg() to a returned context, Logger impls are free to efficiently support logging from multiple threads without necessarily requiring those threads to synchronize across each other for the duration of the call. Simultaneously anyone who does want this simpler behavior can simply derive from MutexSynchronizedLogger. A trivial logCtx implementation might either be a forwarder to the logger impl or simply a reference to the logger itself with those methods implemented. I think the above will give us all the simplicity of the original design without inadvertently burdening extensions with unnecessary synchronization. Thoughts? |
I'd be happy to try and create a pull request if that would be more useful/clearer... |
@@ -1228,14 +1212,14 @@ abstract class Logger | |||
l.log(1337); | |||
-------------------- | |||
*/ | |||
void log(int line = __LINE__, string file = __FILE__, | |||
final void log(int line = __LINE__, string file = __FILE__, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please elaborate why these methods should be final.
If the user tries to shoot his foot he should be able to, as this logger should be the bases for all possible impls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I previously replied to that in the conversation thread (#2 (comment)). There are several motivations for that modification:
- Final methods can get inlined by the compiler.
- Even if they don't, there is a small performance benefit from not going through the vftable of the Logger instance.
- Virtual methods should be used sparingly and documented as extension points. Making a final method virtual later is possible, but making a virtual method final is a breaking change, which became apparent when a method in std.zip that was not designed to be overridable, was changed to final and someone's code broke. To cite Walter from the "final by default" discussion: "The argument for final by default […] is a good one. Even Andrei agrees with it."
- As shown in FileLogger, bottom-up synchronization spread out over 3 methods is easy to get wrong. In this PR I followed the idea that thread-safety is easy when you lock all mutable state at the top most level. By making the public methods final and have them take the lock, the four protected, overridable methods are safely encapsulated.
log
and other methods of its kind encapsulate the basic behavior of a Logger as documented (checking its log level and the condition and calling the fault handler), which are customizable throughlogLevel
andfaultHandler
, and pass their arguments as is intobeginLogMsg
, which can be overridden. They don't contain anything worth overriding in my opinion, and this change alone should not preclude any use cases.
See also commit mleise@f757f2b and the PR description #2 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, I must have overlooked that
@burner It is explained in the pull request description. |
@ChronosWS Yes, that mutex protects unsynchronized access to logLevel_ and fatalHandler_, but also the other fields, from outside access and in descendants of Logger. That setup was intentional: Final methods that are called by the logging infrastructure lock out all other threads, so the overridable methods can freely use all object state including logLevel_, fatalHandler_, header and appender. Or to put the cart before the horse: If you don't span the mutex lock over the whole user code, you don't gain single-threaded performance. (I measured std.logger with and without my modifications to perform the same.) But you lose multi-threaded safety. In particular you have to build it from bottom up and here is why that is bad and I decided for pulling it up into those final methods:
protected void beginLogMsg(string file, int line, string
funcName, string prettyFuncName,
string moduleName, LogLevel
logLevel, Tid threadId, SysTime
timestamp, Logger logger) @trusted
{
// Don't lock anything if we statically disabled logs
static if (isLoggingActive())
{
// no 2nd thread may begin logging
this.mutex.lock();
// something may go wrong in super's method
scope (failure) this.mutex.unlock();
super.beginLogMsg(file, line, funcName,
prettyFuncName, moduleName,
logLevel, threadId,
timestamp, logger);
}
}
protected void logMsgPart(const(char)[] msg)
{
// don't mess with the mutex outside of enabled logging
static if (isLoggingActive())
{
// something may go wrong in super's method
scope (failure) this.mutex.unlock();
super.logMsgPart(msg);
}
}
protected void finishLogMsg()
{
// don't mess with the mutex outside of enabled logging
static if (isLoggingActive())
{
// in any case unlock the mutex afterwards
scope (exit) this.mutex.unlock();
super.finishLogMsg()
}
}
Do you think we can merge this now (since it also contains two or three bug fixes in the original code) and then iterate further on this? The discussion is really shifting into another iteration on the whole API and I would feel better if we could first get this iteration merged. All in all the changes are still simple and manageable, and they get thread-safety done now without hurting single-threaded performance. |
Sounds good to me. I'll have to review your original case against my proposal in more detail (in local code I expect.) Iterating on a later PR if needed sounds like a swell idea. |
Reviewing and testing is fine, but my fear is that you will end up with a counter-proposal and Robert ends up in a deadlock, where he has to decide which version to merge based on empathy more than on software engineering reasons. public synchronized void setErrorHandler(ErrorHandler eh) {
if(eh == null) {
LogLog.warn("You have tried to set a null error-handler.");
} else {
this.errorHandler = eh;
if(this.qw != null) {
this.qw.setErrorHandler(eh);
}
}
} (We could copy that assertion on So my goal with this PR and all threading code I write is to deliver 100% sequential correctness, in the transactional meaning of the expression. I think this was the original design goal for log4j as well. For this Logger that means, while the But, there have been several reports and questions about why log4j can only one log message in flight at any given time. How can anyone so stupid and place a global lock there? Well, it should be clear now, that it was the technically correct decision, but that it became impractical for some applications. [If we could agree on that part I'd be happy. That would mean we are on the same page now.] I am open now for cutting back on sequential consistency for the "greater good". But we should only do that where it actually helps implementing some use case. Let's assume in a first step, that I remove the global lock securing |
Lol, for a moment I was just considering making synchronization a public property. If set to off, the methods that would usually synchronize would to the same job bypassing the mutex. |
Or rather protected, so implementations can decide to only make it public when they are prepared to deal with loosing the "protection from above". class MyLogger : Logger
{
override protected @property bool defaultSynchronization()
{
// Yes, we want the ctor of Logger to set up the default mutex protection
// for our `writeLogMsg()` call.
return true;
}
...
} I think it is kind of neat: It only creates the Mutex when needed according to the overridable property |
Ah no, stupid idea. We still cannot read/write delegates atomically, so the mutex has to be used either way. |
…andard logging call in an exchange of sequential consinstency for throughput.
(The template constraint is not required in this case.)
@ChronosWS Sorry, I misunderstood "swell idea" as something negative. (Non-native speaker.) The urban dictionary cleared it up for me. So if you want to pick up and continue from here, I'm fine with that. I just saw my invested time over the weekend on this go down the drain and felt a little uneasy, especially since I also had fixed a few subtle bugs along the way. |
No problem. I happen to be rather passionate about asynchronous and lock-free programming, so any time I see a blocking synchronization point I want to try to eliminate it. My experience has been that every time you can do this you benefit (except for the very few cases where such locks are truly needed.) However, I am not willing to throw a fit to block this PR, and I'm not going to make someone else do it for me. It's best for me to give it a shot (since I need more practice with D anyway) and then submit the PR myself. Certainly the work you have done here is not wasted, whether or not I can come up with another proposal that satisfies all of the requirements and eliminates the locks. Iteration is the key to making good software quickly - I have no desire to sit here and spin our wheels designing ad infinitum. :) |
My attempt to achieve something similar: https://github.com/klamonte/logger/commit/4391484fcdbfaf69c1776da93a36c792c6ff405c It has the advantages of not synchronizing any of the functions, by way of removing the appender and header fields from Logger. However it feels a bit "non-D-like" in how it uses MsgRange to effectively act as a Java-style Interface. More discussion of this is issue is over at burner/logger#18 for anyone interested. I thought at the time that the path forward was to leave Loggers thread-unsafe and just have subclasses implement thread safety if they want to. Log4D's solution is to use thread-local Loggers that dispatch to global �log4j-like Appenders. and a global configuration that is applied once in each new Thread that calls Log4D.getLogger() or Log4D.getRootLogger(). stdlog is made thread-safe by specific overrides that use header and msgAppender fields keyed to the current Thread. It's a performance waste, but only for stdlog. |
@klamonte Of course I'm interested! I implemented point 3. in your list and performance for single-threaded logging stays the same even with the synchronization. The actual formatting and writing are much more heavy weight operations than locking an uncontended mutex. (Windows' critical sections and pthreads' mutexes are "lightweight", in that they can operate in userspace for the uncontended case.)
Looking at your implementation, it only addresses thread-safety in a rather narrow window (the appender). This PR is about fixing all the threading bugs (and some others I found while testing): http://forum.dlang.org/thread/vbotavcclttrgvzcjjia@forum.dlang.org?page=20#post-20140919073336.6b38ec45:40marco-leise.homedns.org.
In fact it doesn't work like that. If you don't turn MsgRange from a struct into an interface/class, you wont be able create new types of MsgRanges to remove the dependency on the GC, as Robert already pointed out. And if you do, you allocate a new object no the GC every time you call getMsgRange(). Catch 22.
You need to create a global mutex here: synchronized {
auto tlsAppender = tlsMsgAppender[Thread.getThis().toHash()];
tlsAppender.put(msg);
}
[…]
synchronized {
auto theHeader = tlsHeader[Thread.getThis().toHash()];
auto tlsAppender = tlsMsgAppender[Thread.getThis().toHash()];
theHeader.msg = tlsAppender.data;
this.writeLogMsg(theHeader);
} Otherwise you get races when one thread changes the global hash map, while another reads from it:
A hash map will call .toHash() on its own. If you pre-hash, the identity of the thread is lost (i.e. potential of two threads getting the same hash and overwriting each other's appenders). |
@mleise Thank you for the feedback, you've seen much more detail than I did. This is great, I'll get it fixed in Log4D. Do you think it would be fair to say that once this PR is figured out and merged, than any subclass of Logger could override ONLY writeLogMsg() and still get a valid (thread-safe) payload? (Even if the default uses the GC?) That's really the only thing I need for Log4D, my overrides on beginLogMsg/logMsgPart/finishLogMsg are just trying to ensure that races in std.logger haven't corrupted the LogEntry. |
@klamonte Yes, this PR splits Logger's methods in a set of final methods that perform the synchronization and then call the 4 overridable methods in this context. So whatever you override you are already fully protected from races. |
merged |
This pull request addresses the mentioned basic thread safety issues while keeping the API mostly the same. In particular I changed the following:
writeLogMsg
,beginLogMsg
,logMsgPart
andfinishLogMsg
) are nowprotected
. They are the ones who no longer need to implement thread-safety themselves and never get called by user code. They are only called by library code that synchronizes with the Logger's mutex before calling them. When implementing your own loggers, remember to not accidentally make the overrides public.stdlog
when set tonull
will swap the default logger back in.randomString()
no longer leak into user code.isLoggingActive
is now split into an compile time constant and a template (isLoggingActiveAt
) for a specific level. The return values are the same, but code & symbols for the functions are no longer emitted.A word on Logger chaining: A special method for logging chains has been added to replace the now inaccessible
writeLogMsg()
with the nameforwardMsg()
. As public a method it will do the proper synchronization before callingwriteLogMsg()
internally and not check theglobalLogLevel
again.Note: This pull request does not address the recursive logger call issue.