MBrace.Core icon indicating copy to clipboard operation
MBrace.Core copied to clipboard

Mutable variables maintain state across calls

Open isaacabraham opened this issue 9 years ago • 6 comments

I had a brief chat about this with @eiriktsarpalis but it's worth discussing here in more detail. Look at the following code sample: -

let mutable x = 5
let workflow = cloud {
    let! worker = Cloud.CurrentWorker
    x <- x + 1
    return worker.Id, x }
for _ in 1 .. 10 do
    workflow |> cluster.Run |> printfn "%A"

Running the above code produces something like the following: -

("mbrace://work-laptop:36553", 6)
("mbrace://work-laptop:36553", 7)
("mbrace://work-laptop:36553", 8)
("mbrace://work-laptop:36554", 6)
("mbrace://work-laptop:36554", 7)
("mbrace://work-laptop:36554", 8)
("mbrace://work-laptop:36554", 9)
("mbrace://work-laptop:36554", 10)
("mbrace://work-laptop:36554", 11)
("mbrace://work-laptop:36554", 12)

I would have expected the values to all be 6, whereas clearly the lifetime of a captured mutable variable is longer living. How does this behave?

What's even stranger is that if I then do

x <- 10

and rerun the workflow another ten times, I get the following output: -

("mbrace://work-laptop:36554", 11)
("mbrace://work-laptop:36554", 12)
("mbrace://work-laptop:36554", 13)
("mbrace://work-laptop:36554", 14)
("mbrace://work-laptop:36554", 15)
("mbrace://work-laptop:36555", 11)
("mbrace://work-laptop:36552", 11)
("mbrace://work-laptop:36552", 12)
("mbrace://work-laptop:36552", 13)
("mbrace://work-laptop:36552", 14)

Note that I haven't regenerated the workflow - I simple ran the loop again. So a mutable variable has some state of its own on each node, unless it's modified locally, in which case it gets reset.

By the way I'm not suggesting that this is a good pattern to adopt :-) But I didn't expect this behaviour at all.

isaacabraham avatar Jan 21 '16 17:01 isaacabraham

I'm pretty sure that I can create the same local observable phenomenon with async and ThreadStatic :)

palladin avatar Jan 21 '16 17:01 palladin

The behaviour you're seeing is related to two parameters:

  1. The way in which fsi compiles its top-level value bindings.
  2. A feature implemented in Vagabond in response to 1.

Fsi encodes all of its top-level value bindings as static fields. As such, they introduce what I call "implicit data dependencies" to cloud workflows that reference them, since they are not captured by their object graphs. So Vagabond includes functionality that manually replicates such dependencies (and any client-side mutations) across the cluster. If this didn't get done, the initial observed value on the remote process would always be uninitialized, i.e. 0 or null.

So what you're seeing is not entirely unexpected if you rephrase it in the following terms:

open System
let workflow = cloud {
    let! worker = Cloud.CurrentWorker
    let cwd = Environment.CurrentDirectory
    Environment.CurrentDirectory <- @"C:\"
    return worker.Id, cwd }
for _ in 1 .. 10 do
    workflow |> cluster.Run |> printfn "%A"

It's essentially just modifying global state in the context of the specific worker. Like @palladin said, same as mutating ThreadStatic fields in multi-threaded applications.

eiriktsarpalis avatar Jan 21 '16 17:01 eiriktsarpalis

Btw, representing repl values using static fields is not a necessity. The Roslyn C# repl generates code which uses session objects, effectively eliminating this weirdness you're seeing. We should consider doing the same with fsi codegen.

eiriktsarpalis avatar Jan 21 '16 17:01 eiriktsarpalis

Yeah. You can also "fix" this behaviour simply by putting it in a do block :-) It's probably worth documenting this somewhere about static fields in fsx + mutables.

isaacabraham avatar Jan 21 '16 23:01 isaacabraham

@eiriktsarpalis Re Fsi codegen - right, but as I understood from our discussions that comes at the cost of not allowing class-based members to access defined values. So in C# REPL scripting the equivalent of

let a = 1
let b = rnd()
type X() = static member M() = a + b // not allowed, "a" and "b" are not in scope?

is not allowed? (since in C# the "let" become instance declarations in an implied class, and the instance would need to be implicitly passed to M(), which is not done) So can we really adjust F# codegen to avoid statics?

dsyme avatar Jan 21 '16 23:01 dsyme

@dsyme Correct, class definitions in C# interactive cannot contain references to repl values at all. In F# the issue could be somewhat addressed by recovering the session object from a static location in such cases only.

eiriktsarpalis avatar Jan 22 '16 11:01 eiriktsarpalis