3

Has anyone used setting Thread.CurrentPrincipal for authentication within F# asynchronous code?

Was there any pit falls to watch out for, or did it all just work as expected?

mavnn
  • 9,101
  • 4
  • 34
  • 52

2 Answers2

4

That doesn't sound like a good idea, because Thread.CurrentPrincipal relies on the IPrincipal associated with a given thread, but inside an async workflow, you may not be running on the same thread all the time.

Consider this example:

open System.Threading

let collectThreadInformation jobId =
    async {
        let tId1 = Thread.CurrentThread.ManagedThreadId
        do! Async.Sleep 1000
        let tId2 = Thread.CurrentThread.ManagedThreadId
        return jobId, tId1, tId2 }

This is a simple async workflow that reports on the thread ID before and after sleeping for a second. Here's what it looks like running 10 of them in parallel:

> [1 .. 10]
  |> List.map collectThreadInformation
  |> Async.Parallel
  |> Async.RunSynchronously;;
val it : (int * int * int) [] =
  [|(1, 15, 15); (2, 14, 15); (3, 15, 15); (4, 15, 15); (5, 15, 15);
    (6, 15, 15); (7, 15, 15); (8, 14, 15); (9, 15, 15); (10, 15, 15)|]

As you can see, some of the thread IDs have changed from before and after the Sleep, e.g. the second tuple: (2, 14, 15). Before the Sleep it ran on thread 14, but after the sleep, it was resurrected on thread 15.

I think it would be safer to pass in an IPrincipal instance as a function argument.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 1
    Cool! I had trouble reproducing this at first; changing the sleep to `Async.sleep (100 + ((new System.Random()).Next(1, 10)))` helped. – Søren Debois Mar 26 '14 at 15:10
  • I suspected that might be the case, but [this answer](http://stackoverflow.com/questions/1058523/whats-the-difference-between-using-the-thread-pool-and-a-normal-thread) suggests Thread.CurrentPrincipal is propagated to worker threads. I may need to run some tests to see if that applies to async workflows as well. – mavnn Mar 26 '14 at 16:45
  • It tends to work that way when you spawn a new thread, but I don't know if it also happens in e.g. async workflows. However, unless it's documented behaviour, I wouldn't rely on it. – Mark Seemann Mar 26 '14 at 18:26
  • Several people suggested to me this should work as async runs on the thread pool. After some testing, it appears it does (see my answer). – mavnn Mar 27 '14 at 10:58
1

I had various conflicting responses to this in different places, so I actually built a test program. Running the code below suggests that as async workflows run on the ThreadPool, they do actually preserve the thread context.

module TestCurrentPrinciple

open System
open System.Security.Principal
open System.Threading

let rand = Random()

let spawnWorkflow userName = 
    async { 
        let identity = GenericIdentity(userName)
        let principle = GenericPrincipal(identity, [||])
        Thread.CurrentPrincipal <- principle
        for i in 1..10000 do
            let principleName = Thread.CurrentPrincipal.Identity.Name
            if principleName <> userName then 
                failwithf "Mismatch! Principle name %s does not match username %s (Iteration %d)"
                    principleName userName i
            do! Async.Sleep(rand.Next(10))
    }

let names = 
    [ "mavnn"; "ploeh"; "dsyme"; "biboudis"; "MattDrivenDev"; "fssnip"; "marprz_93"; "skillsmatter"; "thinkb4coding" ]

printfn "Starting"
names
|> List.map spawnWorkflow
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
printfn "Done"
Console.ReadLine() |> ignore

Which might be interesting for my current project.

mavnn
  • 9,101
  • 4
  • 34
  • 52
  • By default async thread jumps around in the thread pool every so many iterations, I think its set to something like 30 or 50 iterations. – 7sharp9 Mar 27 '14 at 12:42
  • Its actually 300, I just checked. – 7sharp9 Mar 27 '14 at 12:44
  • So every 300 binds the thread jumps for the next continuation. – 7sharp9 Mar 27 '14 at 12:47
  • So given the 10,000 binds (rather unscientific) test above, it sounds like it preserves the thread context as it jumps. – mavnn Mar 27 '14 at 13:18
  • I think so, SynchronizationContext is contained within the TrampolineHolder which calls Post on it. – 7sharp9 Mar 27 '14 at 13:28
  • Although only for full blown apps, If FSharp.Core is used from a portable context then FX_NO_SYNC_CONTEXT causes the Post to be omitted. – 7sharp9 Mar 27 '14 at 13:30
  • Keep on forgetting I can actually go and check the code myself! Interesting quick to know about portable, though. – mavnn Mar 27 '14 at 13:49
  • I think it will only affect CompactFramework 2.0 and 3.5 anyway. – 7sharp9 Mar 27 '14 at 14:07