From 8b17e5b71e43a05b2160f0837d676e34f22fafa5 Mon Sep 17 00:00:00 2001 From: findstr Date: Sat, 5 Sep 2026 20:13:50 +0800 Subject: [PATCH 1/2] task: quarantine dirty tasks instead of recycling A failed yield can leave a task in WAIT state. Recycling such a task allows its coroutine to be reused by a new task while a stale timer or socket event can still resume it, causing the event to wake the wrong coroutine. Quarantine non-RUN tasks on recycle and log the failure. Route time.sleep and hive.invoke through task.wait so all suspensions are tracked, and remove the task._yield export. --- .github/workflows/docker.yml | 4 ++++ lualib/silly/hive.lua | 4 ++-- lualib/silly/task.lua | 16 +++++++++++++-- lualib/silly/time.lua | 4 ++-- test/testhive.lua | 38 +++++++++++++++++++++++++++++------- test/testssl.lua | 10 ++++++++-- test/testtask.lua | 37 +++++++++++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 15 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cbded04bc..4cc067b13 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -73,6 +73,10 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # Aliyun ACR rejects the OCI 1.1 provenance attestation manifest + # ("unknown manifest class for application/vnd.oci.empty.v1+json"). + # Disabling here keeps the push working on both ghcr.io and ACR. + provenance: false clean: needs: build if: github.ref == 'refs/heads/master' || github.event_name == 'release' diff --git a/lualib/silly/hive.lua b/lualib/silly/hive.lua index 1e809a649..dc6b1c9ff 100644 --- a/lualib/silly/hive.lua +++ b/lualib/silly/hive.lua @@ -9,7 +9,7 @@ local pack = table.pack local unpack = table.unpack local task_running = task.running -local task_yield = task._yield +local task_wait = task.wait local task_resume = task._resume local M = {} @@ -39,7 +39,7 @@ function M.invoke(worker, ...) local t = task_running() local id = c.push(worker, ...) working[id] = t - local ok, dat = task_yield("HIVE") + local ok, dat = task_wait() if not ok then error(dat[1]) end diff --git a/lualib/silly/task.lua b/lualib/silly/task.lua index 9731146c9..5f2df43d9 100644 --- a/lualib/silly/task.lua +++ b/lualib/silly/task.lua @@ -110,6 +110,19 @@ local function task_create(f) local running = corunning() while true do local ret + local status = task_status[running] + if status ~= "RUN" then + --A swallowed yield error (e.g. yield across a C-call + --boundary) leaves the status dirty. Such a task may + --have leaked registrations pointing at itself, so it + --must never go back to the pool. + local info = debug.getinfo(f, "S") + log_error("[silly] task exit with stat:", tostring(status), + info.short_src, info.linedefined) + task_traceid[running] = nil + task_status[running] = nil + return + end f = NIL task_traceid[running] = nil task_status[running] = nil @@ -178,7 +191,7 @@ function task.wait() local t = task_running local status = task_status[t] if status ~= "RUN" then - error("BUG: wait on task stat:" .. status) + error("BUG: wait on task stat:" .. tostring(status), 2) end task_status[t] = "WAIT" return task_yield("WAIT") @@ -223,7 +236,6 @@ end task._create = task_create task._resume = task_resume -task._yield = task_yield function task._dispatch_wakeup() while true do diff --git a/lualib/silly/time.lua b/lualib/silly/time.lua index ebecec877..1c7346fac 100644 --- a/lualib/silly/time.lua +++ b/lualib/silly/time.lua @@ -6,7 +6,7 @@ local type = type local task_running = task.running local task_create = task._create local task_resume = task._resume -local task_yield = task._yield +local task_wait = task.wait local timeafter = c.after local timercancel = c.cancel @@ -23,7 +23,7 @@ function M.sleep(ms) local t = task_running() local session = timeafter(ms) sleep_session_task[session] = t - task_yield("SLEEP") + task_wait() end ---@param ms integer diff --git a/test/testhive.lua b/test/testhive.lua index 8926f97b9..bee1cf4c3 100644 --- a/test/testhive.lua +++ b/test/testhive.lua @@ -1,4 +1,5 @@ local time = require "silly.time" +local task = require "silly.task" local hc = require "silly.hive.c" local hive = require "silly.hive" local waitgroup = require "silly.sync.waitgroup" @@ -68,7 +69,30 @@ do testaux.asserteq(c, 9, "Case 4: result[3]") end --- Test 5: Thread Pool Expansion +-- Test 5: Task status is WAIT while invoking +--(placed before the timing-sensitive thread scaling tests, +-- so it always runs even if they fail) +do + local worker = hive.spawn([[ + return function() + return true + end + ]]) + local invoker + task.fork(function() + invoker = task.running() + local ok = hive.invoke(worker) + testaux.asserteq(ok, true, "Case 5: invoke result") + end) + task.fork(function() + --runs while the invoker is blocked waiting for the worker + testaux.asserteq(task.status(invoker), "WAIT", + "Case 5: invoking task status is WAIT") + end) + time.sleep(100) +end + +-- Test 6: Thread Pool Expansion do hive.limit(2, 4) -- After previous tests, threads might be at min. Let's run one task to ensure the pool is active. @@ -78,7 +102,7 @@ do hive.invoke(pre_worker) local initial_threads = hive.threads() - testaux.asserteq(initial_threads, 1, "Case 5: initial threads") + testaux.asserteq(initial_threads, 1, "Case 6: initial threads") local wg = waitgroup.new() for i = 1, 10 do @@ -91,21 +115,21 @@ do end ]]) local ok = hive.invoke(worker) - testaux.asserteq(ok, true, "Case 5: task result for i="..i) + testaux.asserteq(ok, true, "Case 6: task result for i="..i) end) end wg:wait() - testaux.asserteq(hive.threads(), 4, "Case 5: threads scaled up") + testaux.asserteq(hive.threads(), 4, "Case 6: threads scaled up") end --- Test 6: Thread Pool Pruning +-- Test 7: Thread Pool Pruning do -- Ensure pool is scaled up first from previous test - testaux.asserteq(hive.threads(), 4, "Case 6: threads before prune") + testaux.asserteq(hive.threads(), 4, "Case 7: threads before prune") print("sleep 6 seconds for idle threads") time.sleep(6000) prune() local threads = hive.threads() - testaux.asserteq(threads, 2, "Case 6: threads scaled down") + testaux.asserteq(threads, 2, "Case 7: threads scaled down") end diff --git a/test/testssl.lua b/test/testssl.lua index 65cd0bc61..3b642e8e0 100644 --- a/test/testssl.lua +++ b/test/testssl.lua @@ -9,15 +9,21 @@ local testaux = require "test.testaux" local errno = require "silly.errno" local ETIMEDOUT = errno.TIMEDOUT local EEOF = errno.EOF +local EPIPE = errno.PIPE +local ECONNRESET = errno.CONNRESET local is_iocp = silly.multiplexer == "iocp" local function assert_eof(dat, err, msg_data, msg_err) + -- Abrupt close (no close_notify) races: clean FIN -> EOF, + -- RST -> CONNRESET, RST with undrained server writes (TLS 1.3 + -- NewSessionTicket) -> EPIPE. All mean the peer is gone. + local gone = err == EEOF or err == EPIPE or err == ECONNRESET if is_iocp then - local ok = (dat == nil) and err == EEOF + local ok = (dat == nil) and gone testaux.asserteq(ok, true, msg_err) else - testaux.asserteq(err, EEOF, msg_err) + testaux.asserteq(gone, true, msg_err) testaux.asserteq(dat, nil, msg_data) end end diff --git a/test/testtask.lua b/test/testtask.lua index 096cc6bcb..5706fb906 100644 --- a/test/testtask.lua +++ b/test/testtask.lua @@ -470,4 +470,41 @@ testaux.case("Test 17: task.hook", function() testaux.asserteq(#created_tasks, before_count, "Test 17.9: hook cleared successfully") end) +testaux.case("Test 18: status is WAIT while sleeping", function() + local sleeper + task.fork(function() + sleeper = task.running() + time.sleep(100) + end) + task.fork(function() + --runs while the sleeper is blocked in time.sleep + testaux.asserteq(task.status(sleeper), "WAIT", + "Test 18.1: sleeping task status is WAIT") + end) + time.sleep(200) +end) + +testaux.case("Test 19: task with dirty status is not recycled", function() + collectgarbage("stop") + local dump = task._dump() + local dirty + task.fork(function() + dirty = task.running() + --yield across a C-call boundary, error swallowed by pcall + local ok = pcall(string.gsub, "x", "x", function() + time.sleep(10) + end) + testaux.asserteq(ok, false, "Test 19.1: yield across C-call boundary fails") + end) + task.fork(function() + --runs after the dirty task exited + testaux.asserteq(dump.task_status[dirty], nil, + "Test 19.2: dirty task status cleaned") + local probe = task.fork(function() end) + testaux.assertneq(probe, dirty, + "Test 19.3: dirty coroutine is not recycled") + end) + time.sleep(50) --let the leaked timer fire +end) + collectgarbage("restart") \ No newline at end of file From 9bf469cd4338265294bcf9e129781120ede73b5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:53:52 +0000 Subject: [PATCH 2/2] build(deps): bump undici from 7.28.0 to 7.29.1 in /docs Bumps [undici](https://github.com/nodejs/undici) from 7.28.0 to 7.29.1. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v7.28.0...v7.29.1) --- updated-dependencies: - dependency-name: undici dependency-version: 7.29.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 392657d91..5b5777a2d 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -6167,9 +6167,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": {