| A signal is not a failure abb8e36 nandi yesterday | 1 | ## Telling a signal apart from a failure. |
| 2 | ## |
| 3 | ## Nothing in this suite raises a real EINTR — a test binary has no signals |
| 4 | ## worth the name, which is exactly why the app hit this and the tests never |
| 5 | ## did. So the errors here are built by hand, and what is checked is the |
| 6 | ## judgement: which ones are retried, which are not, and that a retry gives up |
| 7 | ## rather than spinning. |
| 8 | |
| 9 | import std/[os, unittest] |
| 10 | import std/posix as p |
| 11 | import frq/eintr |
| 12 | |
| 13 | proc osError(code: cint, msg: string): ref OSError = |
| 14 | result = newException(OSError, msg) |
| 15 | result.errorCode = code.int32 |
| 16 | |
| 17 | suite "interrupted": |
| 18 | test "an OSError carrying EINTR is only a signal": |
| 19 | check interrupted(osError(p.EINTR, "Interrupted system call")) |
| 20 | test "any other OSError is a real failure": |
| 21 | check not interrupted(osError(p.ECONNRESET, "Connection reset by peer")) |
| 22 | check not interrupted(osError(p.EPIPE, "Broken pipe")) |
| 23 | test "and so is an error that is not an OSError at all": |
| 24 | check not interrupted(newException(ValueError, "nonsense")) |
| 25 | test "but the words count where the code was lost": |
| 26 | # `httpclient` catches an OSError in places and re-raises its text, which |
| 27 | # keeps the message and drops the code. |
| 28 | check interrupted(newException(IOError, "Interrupted system call")) |
| 29 | |
| 30 | suite "retrying": |
| 31 | test "a body that works runs once": |
| 32 | var runs = 0 |
| 33 | retrying 3: |
| 34 | runs.inc |
| 35 | check runs == 1 |
| 36 | |
| 37 | test "one cut short by a signal is run again": |
| 38 | var runs = 0 |
| 39 | retrying 3: |
| 40 | runs.inc |
| 41 | if runs < 3: raise osError(p.EINTR, "Interrupted system call") |
| 42 | check runs == 3 |
| 43 | |
| 44 | test "a real failure is raised at once, not retried": |
| 45 | var runs = 0 |
| 46 | expect OSError: |
| 47 | retrying 5: |
| 48 | runs.inc |
| 49 | raise osError(p.ECONNREFUSED, "Connection refused") |
| 50 | check runs == 1 |
| 51 | |
| 52 | test "and a signal that never stops gives up rather than spinning": |
| 53 | var runs = 0 |
| 54 | expect OSError: |
| 55 | retrying 4: |
| 56 | runs.inc |
| 57 | raise osError(p.EINTR, "Interrupted system call") |
| 58 | check runs == 4 |
| Restartable syscalls, because retrying cannot win dcd31d9 nandi 16h ago | 59 | |
| 60 | suite "restartableSyscalls": |
| 61 | test "adds SA_RESTART without disturbing the handler": |
| 62 | # The flag says what the kernel does to an interrupted syscall. It must |
| 63 | # not change which function runs, or whose signal it is — the profiler |
| 64 | # whose handler this finds is somebody else's. |
| 65 | proc handler(sig: cint) {.noconv.} = discard |
| 66 | var wanted: Sigaction |
| 67 | discard sigemptyset(wanted.sa_mask) |
| 68 | wanted.sa_handler = handler |
| 69 | wanted.sa_flags = 0 |
| 70 | check p.sigaction(SIGALRM, wanted, nil) == 0 |
| 71 | |
| 72 | restartableSyscalls() |
| 73 | |
| 74 | var got: Sigaction |
| 75 | check sigactionOf(SIGALRM, got) |
| 76 | check (got.sa_flags and p.SA_RESTART) != 0 |
| 77 | check got.sa_handler == handler |
| 78 | |
| 79 | test "and leaves one that already has it alone": |
| 80 | proc handler(sig: cint) {.noconv.} = discard |
| 81 | var wanted: Sigaction |
| 82 | discard sigemptyset(wanted.sa_mask) |
| 83 | wanted.sa_handler = handler |
| 84 | wanted.sa_flags = p.SA_RESTART |
| 85 | check p.sigaction(SIGALRM, wanted, nil) == 0 |
| 86 | restartableSyscalls() |
| 87 | var got: Sigaction |
| 88 | check sigactionOf(SIGALRM, got) |
| 89 | check got.sa_handler == handler |
| 90 | check (got.sa_flags and p.SA_RESTART) != 0 |