{
 "number": 36011,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36011",
 "title": "btcsignals: destroy callbacks on disconnect",
 "author": "ryanofsky",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-08-18T16:28:14Z",
 "updated_at": "2026-08-19T12:24:31Z",
 "age_days": 29,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "6a3d490d826a50da6ac812cbf204320d5780f7f3",
 "head_ref": "pr/sigdest",
 "head_repo": "ryanofsky/bitcoin",
 "head_history": [],
 "additions": 115,
 "deletions": 9,
 "changed_files": 2,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "blocked",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "maflcko"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "blocked",
  "last_author_activity": "2026-08-19T12:24:20Z",
  "last_reviewer_activity": "2026-08-19T10:26:38Z",
  "last_reviewer": "maflcko",
  "author_silent_days": 29,
  "waiting_on_author_days": 0,
  "days_since_update": 29
 },
 "refs": {
  "mentioned": [
   10102
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 10102,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Multiprocess bitcoin"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Currently callback functions connected to signals do not get freed when their `connection::disconnect` method is called or when their associated `scoped_connection` is destroyed. They live as long as the signal does, even if they will never be called, and can only get [garbage collected](https://github.com/bitcoin/bitcoin/blob/0fd515bbb7070a502dd2fe6d16f5f3cbac591283/src/util/btcsignals.h#L240-L241) if new connections are added.\n\nThis behavior is different that previous boost signals behavior and causes a [deadlock](https://github.com/bitcoin/bitcoin/actions/runs/31848571315/job/94919834743) on GUI shutdown in #10102 because when the node registers signal callbacks (`handleInitMessage`, etc) that get forwarded to the GUI, the callbacks own `mp::EventLoopRef` references, and if the callbacks aren't freed, the node process can't shut down.\n\nThis change restores boost signals behavior freeing callbacks when they are disconnected or when `scoped_connection` objects are destroyed to avoid this problem and make callback lifetimes more predictable.",
 "commits": [
  {
   "sha": "a16a941b429e2bb34443dacd82f2b970c6337da1",
   "date": "2026-08-18T13:25:33Z",
   "message": "test: characterize btcsignals callback destruction timing\n\nAdd tests pinning down when a callback object (and any state it owns)\nis destroyed. Currently disconnect() only disables the callback: the\nobject stays alive until a later connect() call on the same signal\ngarbage collects it and every connection handle referencing it has been\nreleased; with no connection handle held it lives until the signal\nitself is destroyed.\n\nThis differs from boost::signals2, which btcsignals aims to be\napi-compatible with: there, disconnecting destroys the slot's function\nobject as soon as no emission is using it.\n\nNo behavior change; these tests document the status quo so the next\ncommit changing the destruction timing has a clear before/after diff.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
  },
  {
   "sha": "c735a8ec4ac7c9457f9646fc1543b5d976d11865",
   "date": "2026-08-18T13:25:33Z",
   "message": "btcsignals: destroy callbacks eagerly on disconnect\n\nPreviously a disconnected callback and any state it owned could stay\nalive indefinitely: destruction required both a later connect() call on\nthe same signal to garbage collect it and release of every connection\nhandle referencing it. That made destruction timing unpredictable for\ncallbacks that own resources whose release has side effects, and it\ndiverges from boost::signals2, which destroys a disconnected slot's\nfunction object as soon as no emission is running it. (The concrete\nfallout was in multiprocess bitcoin-gui, where node notification\ncallbacks own IPC proxy objects: their deferred destruction kept\nbitcoin-node from exiting and hung interface_gui.py until timeout.)\n\nMake disconnect() destroy the callback, deferring only while a\nconcurrent emission is mid-call into it, matching boost::signals2\nsemantics. The previous commit's characterization tests are updated;\ntheir diff shows the ownership change.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
  },
  {
   "sha": "6a3d490d826a50da6ac812cbf204320d5780f7f3",
   "date": "2026-08-18T13:25:33Z",
   "message": "btcsignals: fall back to atomic_load/store without __cpp_lib_atomic_shared_ptr\n\nstd::atomic<shared_ptr<T>> is a C++20 library feature, but library\nimplementations are not required to ship all C++20 features at once;\n__cpp_lib_atomic_shared_ptr is the feature-test macro that indicates its\npresence. Without it, the generic std::atomic<T> requires trivially copyable\nT \u2014 shared_ptr is not, causing build failures on macOS (Xcode 16.2),\nFreeBSD 15.1, and the MSan/TSan custom libc++ builds in CI.\n\nFall back to the C++14 atomic_load/atomic_store free functions, which are\nspecifically overloaded for shared_ptr. The fallback is intended to be\ntemporary: those functions are deprecated in C++20 and will eventually be\nremoved, so the #else branch can be dropped once all CI platforms carry the\nC++20 specialization.\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-08-19T10:26:38Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Heh, I really loved the simple garbage collect logic. Too bad it doesn't work for the mp-gui. I guess this can slip the next major release, as there is no rush, right?"
  },
  {
   "t": "2026-08-19T12:24:20Z",
   "kind": "comment",
   "who": "ryanofsky",
   "assoc": "CONTRIBUTOR",
   "text": "Yeah no rush, and there are also some potential simplifications this change could allow. This PR is trying to make behavior of btcsignals more predictable and free the external callbacks after they can no longer be called. But internal improvements might be possible too:\n\n- `connection::liveness::m_connected` variable could maybe be dropped. It now seems redundant with the `m_owner` variable.\n- Maybe the garbage collection loop can be dropped and replaced with an `m_owner` custom deleter that deletes entries from `m_connections`\n- I'm not sure if `m_callback` is actually necessary. Maybe the callback pointer could be derived from `m_owner` with a cast."
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-18T22:29:02Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-19T10:40:17Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 4629,
 "text_tokens_estimate": 1157,
 "changed_paths": [
  "src/test/btcsignals_tests.cpp",
  "src/util/btcsignals.h"
 ],
 "files": [
  {
   "path": "src/test/btcsignals_tests.cpp",
   "add": 46,
   "del": 0
  },
  {
   "path": "src/util/btcsignals.h",
   "add": 69,
   "del": 9
  }
 ],
 "test_lines": 46,
 "git": {
  "head": "6a3d490d826a50da6ac812cbf204320d5780f7f3",
  "head_matches_backup": true,
  "base": "15a7a4ed7c4d0952ce966087e55a9a3e2f28ec1d",
  "commits": [
   {
    "sha": "a16a941b42",
    "subject": "test: characterize btcsignals callback destruction timing",
    "files": 1,
    "add": 59,
    "del": 0
   },
   {
    "sha": "c735a8ec4a",
    "subject": "btcsignals: destroy callbacks eagerly on disconnect",
    "files": 2,
    "add": 52,
    "del": 29
   },
   {
    "sha": "6a3d490d82",
    "subject": "btcsignals: fall back to atomic_load/store without __cpp_lib_atomic_shared_ptr",
    "files": 1,
    "add": 25,
    "del": 1
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "fcfe0bc21b52b506",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}