Compile-time configuration (#371)

This PR moves all compile-time configuration to a single module,
simplifying documentation and access to these features.

Upcomfing features may be enabled either individually, or through a new
`chronosPreviewV4` catch-all designed to allow code to be prepared for
increased strictness in future chronos releases.

`-d:chronosDebug` may be used to enable the existing debugging helpers
together.
This commit is contained in:
Jacek Sieka 2023-03-31 07:35:04 +02:00 committed by GitHub
parent 1394c9e049
commit 229de5f842
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 126 additions and 56 deletions

View File

@ -333,6 +333,12 @@ Known `async` backends include:
``none`` can be used when a library supports both a synchronous and
asynchronous API, to disable the latter.
### Compile-time configuration
`chronos` contains several compile-time [configuration options](./chronos/config.nim) enabling stricter compile-time checks and debugging helpers whose runtime cost may be significant.
Strictness options generally will become default in future chronos releases and allow adapting existing code without changing the new version - see the [`config.nim`](./chronos/config.nim) module for more information.
## TODO
* Pipe/Subprocess Transports.
* Multithreading Stream/Datagram servers

View File

@ -32,10 +32,11 @@ proc run(args, path: string) =
task test, "Run all tests":
for args in [
"-d:useSysAssert -d:useGcAssert",
"-d:chronosStackTrace -d:chronosStrictException",
"-d:debug -d:chronosDebug",
"-d:debug -d:chronosPreviewV4",
"-d:debug -d:chronosDebug -d:useSysAssert -d:useGcAssert",
"-d:release",
"-d:release -d:chronosFutureTracking",
"-d:release -d:chronosPreviewV4",
]: run args, "tests/testall"
task test_libbacktrace, "test with libbacktrace":

View File

@ -10,7 +10,7 @@
import std/sequtils
import stew/base10
import ./srcloc
import "."/[config, srcloc]
export srcloc
when defined(nimHasStacktracesModule):
@ -24,7 +24,7 @@ const
LocCreateIndex* = 0
LocCompleteIndex* = 1
when defined(chronosStackTrace):
when chronosStackTrace:
type StackTrace = string
type
@ -41,11 +41,11 @@ type
mustCancel*: bool
id*: uint
when defined(chronosStackTrace):
when chronosStackTrace:
errorStackTrace*: StackTrace
stackTrace: StackTrace ## For debugging purposes only.
when defined(chronosFutureTracking):
when chronosFutureTracking:
next*: FutureBase
prev*: FutureBase
@ -54,7 +54,7 @@ type
# How much refactoring is needed to make this a regular non-ref type?
# Obviously, it will still be allocated on the heap when necessary.
Future*[T] = ref object of FutureBase ## Typed future.
when defined(chronosStrictException):
when chronosStrictException:
closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError], gcsafe.}
else:
closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.}
@ -80,23 +80,27 @@ type
tail*: FutureBase
count*: uint
var currentID* {.threadvar.}: uint
currentID = 0'u
when chronosFutureId:
var currentID* {.threadvar.}: uint
else:
template id*(f: FutureBase): uint =
cast[uint](addr f[])
when defined(chronosFutureTracking):
when chronosFutureTracking:
var futureList* {.threadvar.}: FutureList
futureList = FutureList()
template setupFutureBase(loc: ptr SrcLoc) =
new(result)
currentID.inc()
result.state = FutureState.Pending
when defined(chronosStackTrace):
when chronosStackTrace:
result.stackTrace = getStackTrace()
when chronosFutureId:
currentID.inc()
result.id = currentID
result.location[LocCreateIndex] = loc
when defined(chronosFutureTracking):
when chronosFutureTracking:
result.next = nil
result.prev = futureList.tail
if not(isNil(futureList.tail)):
@ -160,7 +164,7 @@ proc done*(future: FutureBase): bool {.inline.} =
## This is an alias for ``completed(future)`` procedure.
completed(future)
when defined(chronosFutureTracking):
when chronosFutureTracking:
proc futureDestructor(udata: pointer) =
## This procedure will be called when Future[T] got finished, cancelled or
## failed and all Future[T].callbacks are already scheduled and processed.
@ -188,7 +192,7 @@ proc checkFinished(future: FutureBase, loc: ptr SrcLoc) =
msg.add("\n " & $future.location[LocCompleteIndex])
msg.add("\n Second completion location:")
msg.add("\n " & $loc)
when defined(chronosStackTrace):
when chronosStackTrace:
msg.add("\n Stack trace to moment of creation:")
msg.add("\n" & indent(future.stackTrace.strip(), 4))
msg.add("\n Stack trace to moment of secondary completion:")
@ -212,7 +216,7 @@ proc finish(fut: FutureBase, state: FutureState) =
item = default(AsyncCallback) # release memory as early as possible
fut.callbacks = default(seq[AsyncCallback]) # release seq as well
when defined(chronosFutureTracking):
when chronosFutureTracking:
scheduleDestructor(fut)
proc complete[T](future: Future[T], val: T, loc: ptr SrcLoc) =
@ -240,7 +244,7 @@ proc fail[T](future: Future[T], error: ref CatchableError, loc: ptr SrcLoc) =
if not(future.cancelled()):
checkFinished(FutureBase(future), loc)
future.error = error
when defined(chronosStackTrace):
when chronosStackTrace:
future.errorStackTrace = if getStackTrace(error) == "":
getStackTrace()
else:
@ -258,7 +262,7 @@ proc cancelAndSchedule(future: FutureBase, loc: ptr SrcLoc) =
if not(future.finished()):
checkFinished(future, loc)
future.error = newCancelledError()
when defined(chronosStackTrace):
when chronosStackTrace:
future.errorStackTrace = getStackTrace()
future.finish(FutureState.Cancelled)
@ -472,7 +476,7 @@ proc `$`(stackTraceEntries: seq[StackTraceEntry]): string =
return exc.msg # Shouldn't actually happen since we set the formatting
# string
when defined(chronosStackTrace):
when chronosStackTrace:
proc injectStacktrace(future: FutureBase) =
const header = "\nAsync traceback:\n"
@ -500,7 +504,7 @@ proc internalCheckComplete*(fut: FutureBase) {.
raises: [Defect, CatchableError].} =
# For internal use only. Used in asyncmacro
if not(isNil(fut.error)):
when defined(chronosStackTrace):
when chronosStackTrace:
injectStacktrace(fut)
raise fut.error

View File

@ -14,9 +14,9 @@ else:
{.push raises: [].}
from nativesockets import Port
import std/[tables, strutils, heapqueue, lists, options, deques]
import std/[tables, strutils, heapqueue, options, deques]
import stew/results
import "."/[osdefs, osutils, timer]
import "."/[config, osdefs, osutils, timer]
export Port
export timer, results
@ -1263,7 +1263,7 @@ proc getTracker*(id: string): TrackerBase =
let loop = getThreadDispatcher()
result = loop.trackers.getOrDefault(id, nil)
when defined(chronosFutureTracking):
when chronosFutureTracking:
iterator pendingFutures*(): FutureBase =
## Iterates over the list of pending Futures (Future[T] objects which not
## yet completed, cancelled or failed).

View File

@ -193,12 +193,12 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
# here the possibility of transporting more specific error types here
# for example by casting exceptions coming out of `await`..
let raises = nnkBracket.newTree()
when not defined(chronosStrictException):
raises.add(newIdentNode("Exception"))
else:
when chronosStrictException:
raises.add(newIdentNode("CatchableError"))
when (NimMajor, NimMinor) < (1, 4):
raises.add(newIdentNode("Defect"))
else:
raises.add(newIdentNode("Exception"))
closureIterator.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
@ -328,5 +328,5 @@ macro async*(prc: untyped): untyped =
result.add asyncSingleProc(oneProc)
else:
result = asyncSingleProc(prc)
when defined(nimDumpAsync):
when chronosDumpAsync:
echo repr result

57
chronos/config.nim Normal file
View File

@ -0,0 +1,57 @@
## Compile-time configuration options for chronos that control the availability
## of various strictness and debuggability options. In general, debug helpers
## are enabled when `debug` is defined while strictness options are introduced
## in transition periods leading up to a breaking release that starts enforcing
## them and removes the option.
##
## `chronosPreviewV4` is a preview flag to enable v4 semantics - in particular,
## it enables strict exception checking and disables parts of the deprecated
## API and other changes being prepared for the upcoming release
##
## `chronosDebug` can be defined to enable several debugging helpers that come
## with a runtime cost - it is recommeneded to not enable these in production
## code.
when (NimMajor, NimMinor) >= (1, 4):
const
chronosStrictException* {.booldefine.}: bool = defined(chronosPreviewV4)
## Require that `async` code raises only derivatives of `CatchableError` and
## not `Exception` - forward declarations, methods and `proc` types used
## from within `async` code may need to be be explicitly annotated with
## `raises: [CatchableError]` when this mode is enabled.
chronosStackTrace* {.booldefine.}: bool = defined(chronosDebug)
## Include stack traces in futures for creation and completion points
chronosFutureId* {.booldefine.}: bool = defined(chronosDebug)
## Generate a unique `id` for every future - when disabled, the address of
## the future will be used instead
chronosFutureTracking* {.booldefine.}: bool = defined(chronosDebug)
## Keep track of all pending futures and allow iterating over them -
## useful for detecting hung tasks
chronosDumpAsync* {.booldefine.}: bool = defined(nimDumpAsync)
## Print code generated by {.async.} transformation
else:
# 1.2 doesn't support `booldefine` in `when` properly
const
chronosStrictException*: bool =
defined(chronosPreviewV4) or defined(chronosStrictException)
chronosStackTrace*: bool = defined(chronosDebug) or defined(chronosStackTrace)
chronosFutureId*: bool = defined(chronosDebug) or defined(chronosFutureId)
chronosFutureTracking*: bool =
defined(chronosDebug) or defined(chronosFutureTracking)
chronosDumpAsync*: bool = defined(nimDumpAsync)
when defined(debug) or defined(chronosConfig):
import std/macros
static:
hint("Chronos configuration:")
template printOption(name: string, value: untyped) =
hint(name & ": " & $value)
printOption("chronosStrictException", chronosStrictException)
printOption("chronosStackTrace", chronosStackTrace)
printOption("chronosFutureId", chronosFutureId)
printOption("chronosFutureTracking", chronosFutureTracking)
printOption("chronosDumpAsync", chronosDumpAsync)

View File

@ -12,10 +12,10 @@ when (NimMajor, NimMinor) < (1, 4):
else:
{.push raises: [].}
import ./asyncloop
import "."/[asyncloop, config]
export asyncloop
when defined(chronosFutureTracking):
when chronosFutureTracking:
import stew/base10
const
@ -34,7 +34,7 @@ proc dumpPendingFutures*(filter = AllFutureStates): string =
## not yet finished).
## 2. Future[T] objects with ``FutureState.Finished/Cancelled/Failed`` state
## which callbacks are scheduled, but not yet fully processed.
when defined(chronosFutureTracking):
when chronosFutureTracking:
var count = 0'u
var res = ""
for item in pendingFutures():
@ -62,7 +62,7 @@ proc pendingFuturesCount*(filter: set[FutureState]): uint =
##
## If ``filter`` is equal to ``AllFutureStates`` Operation's complexity is
## O(1), otherwise operation's complexity is O(n).
when defined(chronosFutureTracking):
when chronosFutureTracking:
if filter == AllFutureStates:
pendingFuturesCount()
else:

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
suite "TransportAddress test suite":
test "initTAddress(string)":

View File

@ -10,7 +10,7 @@ import bearssl/[x509]
import ../chronos
import ../chronos/streams/[tlsstream, chunkstream, boundstream]
when defined(nimHasUsed): {.used.}
{.used.}
# To create self-signed certificate and key you can use openssl
# openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes \

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
suite "Asynchronous issues test suite":
const HELLO_PORT = 45679

View File

@ -9,7 +9,7 @@ import std/[strutils, net]
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
suite "Datagram Transport test suite":
const

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos, ../chronos/unittest2/asynctests
when defined(nimHasUsed): {.used.}
{.used.}
suite "Future[T] behavior test suite":
proc testFuture1(): Future[int] {.async.} =

View File

@ -10,7 +10,7 @@ import unittest2
import ../chronos, ../chronos/apps/http/[httpserver, shttpserver, httpclient]
import stew/base10
when defined(nimHasUsed): {.used.}
{.used.}
# To create self-signed certificate and key you can use openssl
# openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes \

View File

@ -11,7 +11,7 @@ import ../chronos, ../chronos/apps/http/httpserver,
../chronos/apps/http/httpcommon
import stew/base10
when defined(nimHasUsed): {.used.}
{.used.}
suite "HTTP server testing suite":
type

View File

@ -9,7 +9,7 @@ import unittest2
import macros
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
type
RetValueType = proc(n: int): Future[int] {.async.}

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos/transports/[osnet, ipnet]
when defined(nimHasUsed): {.used.}
{.used.}
suite "Network utilities test suite":

View File

@ -6,7 +6,9 @@
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import unittest
{.used.}
import unittest2
import ../chronos
import ../chronos/ratelimit

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
suite "Server's test suite":
type

View File

@ -10,7 +10,7 @@ import unittest2
import ../chronos, ../chronos/apps/http/shttpserver
import stew/base10
when defined(nimHasUsed): {.used.}
{.used.}
# To create self-signed certificate and key you can use openssl
# openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes \

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
when not defined(windows):
import posix

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
suite "callSoon() tests suite":
const CallSoonTests = 10

View File

@ -9,7 +9,7 @@ import std/[strutils, os]
import unittest2
import ".."/chronos, ".."/chronos/osdefs
when defined(nimHasUsed): {.used.}
{.used.}
when defined(windows):
proc get_osfhandle*(fd: FileHandle): HANDLE {.

View File

@ -8,7 +8,7 @@
import unittest2
import ../chronos
when defined(nimHasUsed): {.used.}
{.used.}
suite "Asynchronous sync primitives test suite":
var testLockResult {.threadvar.}: string

View File

@ -9,7 +9,7 @@ import std/os
import unittest2
import ../chronos, ../chronos/timer
when defined(nimHasUsed): {.used.}
{.used.}
static:
doAssert Moment.high - Moment.low == Duration.high

View File

@ -6,12 +6,12 @@
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import unittest2
import ../chronos
import ../chronos, ../chronos/config
when defined(nimHasUsed): {.used.}
{.used.}
suite "Asynchronous utilities test suite":
when defined(chronosFutureTracking):
when chronosFutureTracking:
proc getCount(): uint =
# This procedure counts number of Future[T] in double-linked list via list
# iteration.
@ -21,7 +21,7 @@ suite "Asynchronous utilities test suite":
res
test "Future clean and leaks test":
when defined(chronosFutureTracking):
when chronosFutureTracking:
if pendingFuturesCount(WithoutFinished) == 0'u:
if pendingFuturesCount(OnlyFinished) > 0'u:
poll()
@ -33,7 +33,7 @@ suite "Asynchronous utilities test suite":
skip()
test "FutureList basics test":
when defined(chronosFutureTracking):
when chronosFutureTracking:
var fut1 = newFuture[void]()
check:
getCount() == 1'u
@ -65,7 +65,7 @@ suite "Asynchronous utilities test suite":
skip()
test "FutureList async procedure test":
when defined(chronosFutureTracking):
when chronosFutureTracking:
proc simpleProc() {.async.} =
await sleepAsync(10.milliseconds)