turion / rhine

Haskell Functional Reactive Programming framework with type-level clocks
http://hackage.haskell.org/package/rhine
124 stars 21 forks source link

Parallel composition of FixedStep clocks evaluates in unexpected order #365

Open DISTEL100 opened 1 month ago

DISTEL100 commented 1 month ago

There seems to be a problem with the parallel clock when composing two pure clocks in ScheduleT. The merged output is not ordered by the timestamps.

The following is a minimal example to reproduce the issue:


f300 :: (MonadIO m) => Rhine (ScheduleT Integer m) (FixedStep 300) a Integer
f300 =  absoluteS @@ FixedStep

f500 :: (MonadIO m) => Rhine (ScheduleT Integer m) (FixedStep 500) a Integer
f500 =  absoluteS @@ FixedStep

f :: IO ()
f = runScheduleIO $ flow $ (f300 +@+ f500) @>-^ arrM (liftIO . print)

The output is:

ghci> f
Left 300
Left 600
Right 500
Left 900
Left 1200
Right 1000
Left 1500
Left 1800
Right 1500
Left 2100
Left 2400
Right 2000
Left 2700
Right 2500
Left 3000
Left 3300
^CInterrupted.

I would expect it to be

ghci> f
Left 300
Right 500
Left 600
Left 900
Right 1000
Left 1200
Left 1500
Right 1500
Left 1800
Right 2000
Left 2100
Left 2400
Right 2500
Left 2700
Left 3000
Left 3300
^CInterrupted.
turion commented 1 month ago

Thanks for finding this! I can sort of reproduce, but I'm getting a different (yet still incorrect) scheduling:

ghci> runScheduleIO $ flow $ (f300 +@+ f500) @>-^ arrM (liftIO . print)
Left 300
Right 500
Left 600
Left 900
Left 1200
Right 1000
Left 1500
Left 1800
Left 2100
Right 1500
Left 2400
Right 2000
Left 2700
Left 3000
^CInterrupted.
turion commented 1 month ago

I have an inkling that this might be an upstream issue in https://github.com/turion/monad-schedule. Let me investigate.

turion commented 1 month ago

The problem is not related to the clock structures, since it can be reproduced with automata:

ghci> a300 = Data.Automaton.constM (wait 300 >> pure 300) >>> accumulateWith (+) 0
ghci> a500 = Data.Automaton.constM (wait 500 >> pure 500) >>> accumulateWith (+) 0
ghci> runScheduleIO $ Data.Automaton.reactimate $ schedulePair a300 a500 >>> arrM (liftIO . print)
300
500
600
900
1200
1000
1500
1800
1500
2100
2400
2000
2700
3000
^CInterrupted.
turion commented 1 month ago

Which versions of rhine and monad-schedule are you using? (I'm using rhine-1.4.0.1 and monad-schedule-0.2)

DISTEL100 commented 1 month ago

i have rhine ==1.4.0.1 and monad-schedule 0.2.0.1 and i just ran it again and had the same output like you

Thanks for finding this! I can sort of reproduce, but I'm getting a different (yet still incorrect) scheduling:

ghci> runScheduleIO $ flow $ (f300 +@+ f500) @>-^ arrM (liftIO . print)
Left 300
Right 500
Left 600
Left 900
Left 1200
Right 1000
Left 1500
Left 1800
Left 2100
Right 1500
Left 2400
Right 2000
Left 2700
Left 3000
^CInterrupted.
turion commented 1 month ago

Yes, it unfortunately depends on how fast the GHC IO machine is running. Basically, if there is an IO delay that is too big then an action may fail to even start before the other finishes. In that case, the second one cannot judge whether it should have waited for the first one.

turion commented 1 month ago

I don't have a good solution for this other than deprecating the MonadSchedule IO instance. I'll have to think about this. For the time being, your workaround should be replacing runScheduleIO by runFreeAsync . runScheduleIO everywhere.

DISTEL100 commented 1 month ago

ok, thanks for the fast replies!

turion commented 1 month ago

I'm afraid this is a known and poorly documented issue in monad-schedule. A workaround would be using https://hackage.haskell.org/package/monad-schedule-0.2.0.1/docs/Control-Monad-Schedule-FreeAsync.html.

For example:

ghci> runFreeAsync $ runScheduleIO $ Data.Automaton.reactimate $ scheduleList (a300 :| [a500]) >>> arrM (liftIO . print)
300 :| []
500 :| []
600 :| []
900 :| []
1000 :| []
1200 :| []
1500 :| [1500]

The issue is that the MonadSchedule IO instance is not very well behaved.

turion commented 1 month ago

Hah I was mistaken luckily. There is a rare race condition in schedulePair when two clocks are supposed to tick at exactly the same time. I'll publish a fix later.

turion commented 3 weeks ago

I had hoped that #343 had fixed this. @DISTEL100 What ghc version are you on? Can you clone the rhine repository and run cabal test all?

DISTEL100 commented 3 weeks ago

i am using ghc 9.4.8 and the tests are all passing