0

I have some async calls that runs and I'm setting a timeout for all via the context.

ctxWithTimeout, cancel := context.WithTimeout(ctx, getTimeoutDuration())
defer cancel()

go func1(ctxWithTimeout, outputChan1, param1)
go func2(ctxWithTimeout, outputChan2, param2)
go func3(ctxWithTimeout, outputChan3)

outputChan1Result := <-outputChan1
Some logic...
outputChan2Result := <-outputChan2
Some logic...
outputChan3Result := <-outputChan3
Some logic...

getTimeoutDuration will return "1" (nanosecond for tests) and 60 seconds for other cases. I made sure I have the correct values when running the tests.

func1 and the other 2 have some logic inside and some other calls that I'm using mocks for.

The code works when I run my service and execute the call via a postman call, if I hit the timeout I see the correct code is being executed and I get the correct response in postman. The timeout can be identified by

ctx.Err() == context.DeadlineExceeded

I wrote a test and wanted to reach timeout. The execution of the 3 functions takes ~130µs and the code runs without hitting the 1 nanosecond timeout. The result is also as if I just managed to run and execute all the code under the time limitation.

Any idea why the timeout is not being triggered or how to make sure it will be triggered?

Itay Gal
  • 10,706
  • 6
  • 36
  • 75
  • 1
    To make sure it's triggered, call `cancel()` before you call `func1`, `func2`, and `func3`. – Jonathan Hall Apr 27 '22 at 13:37
  • I'm using `defer cancel()`, forgot to add it to the example. Added now – Itay Gal Apr 27 '22 at 21:54
  • That's not what you want. Defer runs cancel after everything. You want to run it before everything. – Jonathan Hall Apr 28 '22 at 09:29
  • Why would I want to run cancel before I even know if I need to cancel or not... – Itay Gal Apr 28 '22 at 09:33
  • Because your goal is to test a canceled context. – Jonathan Hall Apr 28 '22 at 11:16
  • This is not the test, this is the actual code. The test only calls the function that contains this code and check the return value of that function. I'm setting a minimal timeout in the test so the timeout will be reached – Itay Gal Apr 28 '22 at 11:49
  • Right. In the test, you need to call `cancel()` first, if you want to test a cancelled context. – Jonathan Hall Apr 29 '22 at 08:03
  • You didn't understand my intention. The function can't change, I want to testing what happens when I get a timeout in the real world. The idea is to set a minimal timeout in test mode so the timeout mechanism will work and be triggered. – Itay Gal Apr 29 '22 at 11:54
  • Two things: 1) How do you fix bugs if you can't change the function? And if you can't fix bugs, why are you bothering with testing? 2) I do understand your intention. You want to test a cancelled context. You do that by using a cancelled context. Where does `ctx` come from? I expect you're passing `context.Background()` or similar from your test. Just pass a cancelled context in that case instead. – Jonathan Hall Apr 30 '22 at 08:07

2 Answers2

0

According to your example, the code does not work as you expect. And you should wait channels and context deadline simultaneously in the select.

ctxWithTimeout, cancel := context.WithTimeout(ctx, getTimeoutDuration())

go func1(ctxWithTimeout, outputChan1, param1)
go func2(ctxWithTimeout, outputChan2, param2)
go func3(ctxWithTimeout, outputChan3)
select {
    case outputChan1Result := <-outputChan1:
    case outputChan2Result := <-outputChan2:
    case outputChan3Result := <-outputChan3:
    case <-ctxWithTimeout.Done():
}

Furthermore, you should wait all results somehow: waitGroup or counter e.g.

Dirty example: https://go.dev/play/p/-eJcjAItMMu

isavinof
  • 132
  • 5
  • each output channel is blocking, inside each function I close the channel with `defer close(outputChannel)`. I'm waiting only for the first one, and then I can make another call that depends on the return data, and only then I'll keep waiting for the other calls. It works as expected, I see it when I'm debugging my code not via test – Itay Gal Apr 27 '22 at 21:43
0

You have not included your complete code, but I'm guessing the function you wish to test looks something like this:

func foo(ctx context.Context) {
    ctxWithTimeout, cancel := context.WithTimeout(ctx, getTimeoutDuration())
    defer cancel()

    go func1(ctxWithTimeout, outputChan1, param1)
    go func2(ctxWithTimeout, outputChan2, param2)
    go func3(ctxWithTimeout, outputChan3)
}

And your test call looks someting like:

    foo(context.Background())`

Change your test to:

    ctx, cancel := context.WithCancel(context.Background())
    cancel()
    foo(ctx)
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189