diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d88acfb..dc68fe6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ matrix.os }} env: # The oldest iyi this release supports (README, Requirements). - IYI_VERSION: 0.20.0 + IYI_VERSION: 0.21.0 steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bcae8b..cdfac92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## Unreleased + +Requires iyi 0.21.0. Parallel tasks are spelled `spawn(unit: :new)` there, +and 0.21.0 refuses raw memory and trust marks in a module that is not +marked `@[Unsafe("why")]` (SPEC.md III.13), a mark 0.20.x refuses above a +`module` header. + +* `db`, `db/pooling` and `db/testing/dummy_driver` are marked for the types + they hold `Share` on trust (`SharedBuilder`, `Pool` and `PoolSlot`, + `Made`), and `db/core` for the pool a `Database` leaves uninitialized + while it builds it, each mark saying why. +* The tests and manual programs start parallel tasks with + `spawn(unit: :new)`, and the tests read a string's bytes with + `Bytes.of`. + ## v0.4.0 Requires iyi 0.20.0: a `Database` shared by tasks on every worker needs diff --git a/README.md b/README.md index 65736a4..bb71366 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,12 @@ require: errors are values, types are closed, and tasks are structured. ## Requirements -iyi 0.20.0 or later: a `Database` shared by tasks on every worker needs -`spawn_parallel` (0.19.0), the `@[Share]` defs and `SharedProc` that check -the blocks it keeps (0.19.1), and 0.20.0's scheduler, on which a checkout -parked on a full pool is woken when a connection comes back. +iyi 0.21.0 or later: a `Database` shared by tasks on every worker needs +parallel tasks (`spawn_parallel` in 0.19.0, spelled `spawn(unit: :new)` +since 0.21.0, which is what the tests and manual programs write), the +`@[Share]` defs and `SharedProc` that check the blocks it keeps (0.19.1), +and 0.20.0's scheduler, on which a checkout parked on a full pool is woken +when a connection comes back. ## Installation @@ -95,7 +97,7 @@ choose how statements run. ### On every worker A `Database` is `Share`, so one opened once and kept in a constant serves -parallel tasks (`spawn_parallel`) on every worker, a web server's handlers +parallel tasks (`spawn(unit: :new)`) on every worker, a web server's handlers among them, with no `Share` error and no warning from the compiler: ```iyi @@ -106,7 +108,7 @@ def ask(n : Int64) : Int64 end group do |g| - 32.times { |t| g.spawn_parallel { ask(t.to_i64); nil } } + 32.times { |t| g.spawn(unit: :new) { ask(t.to_i64); nil } } end ``` @@ -192,7 +194,7 @@ crystal-db's. | `DB::MetadataValueConverter.arg_to_log` overloads | `CustomValue#to_log` | R-3 | | `Pool.new` and `Database.new` open the initial connections | `Pool#prefill` and `Database.open(...)` do; the constructors do no IO (a constructor has no error to answer) | III.1 | | `TopLevelTransaction.new` runs `BEGIN` | `Transaction#start` runs `BEGIN`/`SAVEPOINT`; `perform_commit`/`perform_rollback` are what a transaction subclass implements | III.1 | -| `Pool` guards itself with a `Mutex` | so does iyi-db's, around its bookkeeping, since tasks run on every worker (`spawn_parallel`); a connection goes in and out of the lock in a slot that is `Share` on trust, as no connection is `Share` | III.4.4, III.4.13 | +| `Pool` guards itself with a `Mutex` | so does iyi-db's, around its bookkeeping, since tasks run on every worker (`spawn(unit: :new)`); a connection goes in and out of the lock in a slot that is `Share` on trust, as no connection is `Share` | III.4.4, III.4.13 | | a pool's factory, `setup_connection`'s block and `def_around_query_or_exec`'s hooks may capture anything | they run on whichever worker needs them, so they may capture only `Share` values, checked where they are written (`@[Share]`) | III.4.4: data races are a compile error | | `disconnect!`-style names | no `!` in names | III.1.7 | | `DB::DriverSpecs.run do ... end` (implicit receiver) | `DriverSpecs.run do \|specs\| specs.... end` | no `with ... yield` | diff --git a/db.iyi b/db.iyi index b0e4323..6e35466 100644 --- a/db.iyi +++ b/db.iyi @@ -74,6 +74,7 @@ # # `derive db_serializable` (module `db/serializable`) maps the rows of a # result set to a struct or class. +@[Unsafe("SharedBuilder is taken as Share on trust: the pool calls a driver's builder from several workers at once, and a builder only reads what its URI was parsed into")] module db # The types live in `db/core`, `db/errors`, `db/value`, `db/pooling` and diff --git a/db/core.iyi b/db/core.iyi index 320f7fa..1a72504 100644 --- a/db/core.iyi +++ b/db/core.iyi @@ -24,6 +24,7 @@ # `tx.commit` before a `return` that should commit. # * Statement instrumentation is `Statement.def_around_query_or_exec { ... }`, # a hook registry, because there is no reopening `DB::Statement` here (R-3). +@[Unsafe("a database's pool is left uninitialized while it is built, so that the factory the pool keeps can capture the database, and nothing reads it before it is assigned")] module db/core import std/uri::{URI, Params} @@ -1275,7 +1276,7 @@ end # ## Every worker # # A `Database` is `Share`: one kept in a constant may be used from parallel -# tasks (`spawn_parallel`) on every worker. Its pool hands each connection +# tasks (`spawn(unit: :new)`) on every worker. Its pool hands each connection # to one task at a time (`Pool`), and the blocks it keeps - the connection # factory and `setup_connection`'s - run on whichever worker opens a # connection, so each is checked where it is written, as a parallel task's diff --git a/db/mapping_test.iyi b/db/mapping_test.iyi index b3477b8..490cba7 100644 --- a/db/mapping_test.iyi +++ b/db/mapping_test.iyi @@ -12,7 +12,7 @@ import db/testing/dummy_driver::* # crystal's `String#to_slice`, which iyi's prelude has not. def bytes_of(text : String) : Bytes - Bytes.new(text.to_unsafe, text.bytesize).dup + Bytes.of(text).dup end class SimpleMapping diff --git a/db/parallel_test.iyi b/db/parallel_test.iyi index 19dfa2d..2592539 100644 --- a/db/parallel_test.iyi +++ b/db/parallel_test.iyi @@ -28,7 +28,7 @@ end def ask_everything(db : Database) : Nil group do |g| 32.times do |t| - g.spawn_parallel do + g.spawn(unit: :new) do 2000.times do |k| n = t.to_i64 * 100000_i64 + k.to_i64 got = db.query_one("?", n, as: Int64).or_panic @@ -51,7 +51,7 @@ describe "a Database every worker shares (db#1)" do it "answers the issue's program, through a constant, with the rows each task asked for" do group do |g| 32.times do |t| - g.spawn_parallel do + g.spawn(unit: :new) do 2000.times do |k| n = t.to_i64 * 100000_i64 + k.to_i64 got = ask(n) @@ -83,7 +83,7 @@ describe "a Database every worker shares (db#1)" do most = Atomic(Int32).new(0) started = Time.monotonic group do |g| - g.spawn_parallel do + g.spawn(unit: :new) do a = db.checkout.or_panic b = db.checkout.or_panic go.close @@ -93,7 +93,7 @@ describe "a Database every worker shares (db#1)" do nil end 40.times do |t| - g.spawn_parallel do + g.spawn(unit: :new) do go.receive n = t.to_i64 got = db.query_one("?", n, as: Int64).or_panic @@ -124,7 +124,7 @@ describe "a Database every worker shares (db#1)" do refused = Atomic(Int32).new(0) group do |g| 32.times do |t| - g.spawn_parallel do + g.spawn(unit: :new) do k = 0 while true n = t.to_i64 * 100000_i64 + k.to_i64 @@ -156,7 +156,7 @@ describe "a Database every worker shares (db#1)" do db = ::Db.open("dummy://host?initial_pool_size=4&max_pool_size=8&max_idle_pool_size=8&retry_attempts=2&retry_delay=0").or_panic group do |g| 32.times do |t| - g.spawn_parallel do + g.spawn(unit: :new) do 200.times do |k| n = t.to_i64 * 100000_i64 + k.to_i64 case k % 4 diff --git a/db/pooling.iyi b/db/pooling.iyi index 3930b4e..274f46e 100644 --- a/db/pooling.iyi +++ b/db/pooling.iyi @@ -7,7 +7,7 @@ # pool knows the one it is given back by its `object_id`. # # **Every worker may share one pool.** Since iyi 0.19.0 a program's tasks -# run on a pool of worker threads (`spawn_parallel`), and a pool kept in a +# run on a pool of worker threads (`spawn(unit: :new)`), and a pool kept in a # constant is reached by tasks on all of them at once. v0.3.0 kept its # bookkeeping with no lock, from when iyi's tasks shared one thread, and two # workers checking out at once were handed one connection (db#1). Now: @@ -41,6 +41,7 @@ # when its arguments are - which `Pool(Connection)` is not, a connection not # being `Share`. The pool owns its resources as `Mutex(T)` owns its value: # a task reaches one only between a checkout and its release. +@[Unsafe("Pool and PoolSlot are taken as Share on trust: the book hands each slot, and the resource in it, to one task at a time behind its lock")] module db/pooling import std/uri::{Params} diff --git a/db/serializable_test.iyi b/db/serializable_test.iyi index 7da8c93..f870c30 100644 --- a/db/serializable_test.iyi +++ b/db/serializable_test.iyi @@ -13,7 +13,7 @@ import db/testing/dummy_driver::* # crystal's `String#to_slice`, which iyi's prelude has not. def bytes_of(text : String) : Bytes - Bytes.new(text.to_unsafe, text.bytesize).dup + Bytes.of(text).dup end class SimpleModel diff --git a/db/testing/dummy_driver.iyi b/db/testing/dummy_driver.iyi index 59ced43..020c552 100644 --- a/db/testing/dummy_driver.iyi +++ b/db/testing/dummy_driver.iyi @@ -10,8 +10,9 @@ # set of `"fail_close"` answers an error when it is closed. # # Its connections, statements and result sets may be made and used on any -# worker (`spawn_parallel`): what it counts is atomic, and what it keeps for +# worker (`spawn(unit: :new)`): what it counts is atomic, and what it keeps for # the tests to look at is behind a lock. +@[Unsafe("the double's Made is taken as Share on trust, since a test looks at one only in the task that used it or after that task has ended")] module db/testing/dummy_driver import std/uri::{URI, Params} diff --git a/manual/pool_concurrency.iyi b/manual/pool_concurrency.iyi index 8df92bd..1034549 100644 --- a/manual/pool_concurrency.iyi +++ b/manual/pool_concurrency.iyi @@ -2,7 +2,7 @@ # # % iyi run manual/pool_concurrency.iyi -- --options="max_pool_size=5" --duration=30 --concurrency=4 # -# The tasks are parallel (`spawn_parallel`), as crystal's fibers were under +# The tasks are parallel (`spawn(unit: :new)`), as crystal's fibers were under # `-Dpreview_mt`: they share one `Database` from every worker, as many # workers as `IYI_WORKERS` says, or one per core. module manual/pool_concurrency @@ -60,7 +60,7 @@ puts "Starting test for #{duration} seconds..." running = Atomic(Int32).new(1) group do |g| concurrency.times do - g.spawn_parallel do + g.spawn(unit: :new) do while running.get == 1 db.scalar "1" sleep 0