2

Working with:

  • .NET 4.5.1
  • Web Forms
  • Entity Framework 6 (Context Per Request)
  • IIS 8, Windows 2012 Datacenter

Main concern: Thread safety and reliability.

The project consist of a closed system that will be used by numerous types of users which will execute various actions. In our current project I've decided to implement something that probably most developers find absolutely necessary.

There were serious problems in the past deriving from the lack of even the simplest logging system that would allow us to track some common user actions, especially manipulating data.

I know there are popular logging frameworks, but I want to achieve something relatively simple that will not block the main thread.

The idea was to pull all data I need while on the main thread, because it turned out some of the data was harder to access from a separate thread, and then create a task that will take care of the database insert. My knowledge of multi-threading is limited, and this is what I came-up with.

public static class EventLogger
    {
        public static void LogEvent(int eventType, string descr, Exception ex = null)
        {
            //Get the page that is currently being executed;
            var executingPage = HttpContext.Current.CurrentHandler as Page;
            string sourcePage = executingPage != null ? executingPage.AppRelativeVirtualPath : string.Empty;

            var eventToAdd = new Event()
            {
                Date = DateTime.Now,
                EventTypeId = eventType,
                isException = ex != null ? true : false,
                ExceptionDetails = ex != null ? ex.Message : string.Empty,
                Source = sourcePage,
                UserId = UserHelper.GetUserGuid(),
                UserIP = UserHelper.GetUserIP(),
                UserName = UserHelper.GetUserName(),
                Description = descr
            };

            Task.Factory.StartNew(() => LogEventAsync(eventToAdd));
        }

        private static void LogEventAsync(Event eventToAdd)
        {
            using (var context = new PlaceholderEntities())
            {
                context.Events.Add(eventToAdd);
                context.SaveChanges();
            }
        }
    }

Questions:

  • Would this be a good-enough way to log what I need and is it safe in a multi-user environment?
  • How would you do it if you didn't want to dig into logging frameworks?
  • 1
    If you want to serialize access to DbContext on the same thread, check this: http://stackoverflow.com/q/20993007/1768303 – noseratio Mar 26 '14 at 13:54

3 Answers3

0

No, its not. EF6 is not thread-safe (well, DBContext is not thread-safe, so if you have 1 DBcontext per thread you should be ok), so adding your log event can (ha! will) interfere with another thread writing a different log event.

What you will need to do is synchronise all the calls to EF in your task, what I'd do is add the log to a collection and then have another thread that pulls these logs off the collection and writes them to the DB, one at a time. The add/remove calls to the collection must be protected with a lock to stop 2 threads adding (or removing) an entry simultaneously.

Or I'd use log4net, which is thread-safe, so you can simply call it in your thread to do the writing or the log entries.

My other advice is to write the logs to a file rather than the DB. File-logging is quick so you can do it on the main thread with minimal performance impact (log4net is very efficient too, so just use that - and I know IIS will be writing access and error logs to file anyway) which will remove your issue with threading.

One thing I really do know is that if you're not too hot on multi-threading then you need to stop doing it until you are hot on it. No disrespect to you for trying, but thread bugs can be f***** nightmares to solve, you cannot reproduce them nor easily catch them in a debugger and their cause are often unrelated to the symptoms that are reported.

Community
  • 1
  • 1
gbjbaanb
  • 51,617
  • 12
  • 104
  • 148
  • Can you elaborate on why Entity Framework is not thread-safe, I might be wrong but I see it this way -> Create a new thread, create a new and separate context, do database work -> have a cup of coffee. Shouldn't this be thread safe, because those are two different and separate context? –  Mar 26 '14 at 12:38
  • Did some research on that after my comment, it appears that creating multiple context is the way to go and should not cause any issues. Trying to use the same context from several threads causes issues as you suggested. –  Mar 26 '14 at 12:44
  • https://entityframework.codeplex.com/wikipage?title=Task-based%20Asynchronous%20Pattern%20support%20in%20EF. says they explicitly didn't try to enable thread safety in EF6. Lots of contexts could be a bit of a perf problem especially if all your threads don't complete quickly and you keep on adding more and more. A thread pool is the solution to that problem - but then you're back to my suggestion to use a queue. – gbjbaanb Mar 26 '14 at 13:25
  • I'll consider the queue solution then, just need to wrap my mind on how to implement it properly. If not, I do not expect more than 200 concurrent users in the system, so creating threads and separate contexts for logging might be sufficient for my case if I fail to implement properly what you've suggested. Also for the hot or not too hot with multi-threading, I'm aware of the eventual problems that can be cause from the lack of experience, but then again if I don't experiment and meet those problems, how am I supposed to learn? –  Mar 26 '14 at 14:48
  • @Null learn.. by making mistakes :) just remember to lock both reads and writes of your queue so it will always be in a sane state. I would also add a perfmon counter to track outstanding queued requests so you can see if they are being written quickly or backing up. I'd also ensure that the queue empties before stopping the service (or you'll lose the un-written logs) and perhaps catch *all* exceptions in the main thread so the log thread has time to write the log message that tells you why something crashed... though I wouldn't do any of that - I'd log to file immediately. good luck – gbjbaanb Mar 26 '14 at 16:30
  • -1 from me. Creating separate contexts per thread (and this is what he does) should not bring any issues. If it would, using ef in a server environment would not be possible at all. What each call does is it inserts a single record. Nothing can go wrong here. From your answer one could conclude that this is the opposite. – Wiktor Zychla Apr 05 '14 at 13:03
  • @WiktorZychla he's going to create a single context per log write. *per log write*! How many connections do you think this will generate during normal operation, with a couple of hundred concurrent users? – gbjbaanb Apr 05 '14 at 14:47
  • Depends on how often he logs. Also, database connections are pooled. I would say creating a new task could be more costly than using a connection from the pool to insert a single record. You recommend log4net but it's not asynchronous but blocking. I like the idea of a single queue but still think that your answer could possibly be unnecessarily confusing. – Wiktor Zychla Apr 05 '14 at 15:13
  • If it's of any importance, the expected concurrent users are less than 150, the project is *very* niche and it will be used by a small number of tutors. That doesn't really matter for finding the best way to log, and wrong is wrong, no matter the scale. However, after some research and input from other people I found as well that no matter the low quality of my code, it should be thread safe, hence why I didn't choose this as an answer. Your other suggestions were solid. If you consider what you first said a mistake, please edit, I don't want to edit posts of people with superior knowledge. –  Apr 10 '14 at 14:26
  • @Null ok, I edited it to put in a citation. DBContexts are not thread safe, but they are a large part of "the component known as EF6" so I've left the text in as a scary warning. The number of concurrent users isn;t so much the problem as the rate of logging - if you're auditing login/out for example, you should be ok. If you're logging every operation they do, it might get a bit excessive. Just look out for performance issues and queue them up if necessary. Cheers. – gbjbaanb Apr 10 '14 at 15:42
0

As mentioned by @gbjbaanb you should implement this as a queue with one or more threads actually doing the database work via EF. You can make use of BlockingCollection<T> backed by a ConcurrentQueue<T> to do this in a producer/consumer fashion. Basic premise is each caller logging something simply adds to the queue (i.e. producer). You can then have one or more threads pulling information off the queue and persisting to the database (i.e. consumer). You'd need a separate context for each thread.

There's a reasonable example on the BlockingCollection<T> documentation pages.

Dean Ward
  • 4,793
  • 1
  • 29
  • 36
0

Yes, it is safe in a multithread environment. Because you are creating a new instance of the database context everytime you insert an event into the database, you are safe. EF is not thread-safe only if you would try to reuse the same context instance across threads.

The only possible issue is that doing this in an async way possibly means that multiple connections are opened concurrently and the connection pool could be depleted. The more often you log, the higher is the possibility that this happens.

Having said this, I still recommend you use log4net or any existing logging infrastructure and just find a way to log asynchronously. People blog often how to do this with log4net, take a look here for example

http://www.ben-morris.com/using-asynchronous-log4net-appenders-for-high-performance-logging

Note that issues related to async logging are also discussed there. To workaround some issues like the possibility of misordering entries, you could possibly have a queue in such custom appender. Sticking with existing framework lets you reuse a lot of ideas that are already developed. Going on your own sooner or later will stop you for longer, when requirements change.

Wiktor Zychla
  • 47,367
  • 6
  • 74
  • 106