|
4 | 4 | # file COPYING or http://www.opensource.org/licenses/mit-license.php. |
5 | 5 | """Test the IPC (multiprocess) interface.""" |
6 | 6 | import asyncio |
| 7 | +import http.client |
| 8 | +import re |
7 | 9 |
|
| 10 | +from contextlib import ExitStack |
8 | 11 | from test_framework.test_framework import BitcoinTestFramework |
9 | 12 | from test_framework.util import assert_equal |
10 | 13 | from test_framework.ipc_util import ( |
@@ -81,10 +84,104 @@ async def async_routine(): |
81 | 84 | assert_equal(e.type, "FAILED") |
82 | 85 | asyncio.run(capnp.run(async_routine())) |
83 | 86 |
|
| 87 | + def run_unclean_disconnect_test(self): |
| 88 | + """Test behavior when disconnecting during an IPC call that later |
| 89 | + returns a non-null interface pointer. Currently this behavior causes a |
| 90 | + crash as reported https://github.com/bitcoin/bitcoin/issues/34250, but a |
| 91 | + followup will change this behavior.""" |
| 92 | + node = self.nodes[0] |
| 93 | + self.log.info("Running disconnect during BlockTemplate.waitNext") |
| 94 | + timeout = self.rpc_timeout * 1000.0 |
| 95 | + disconnected_log_check = ExitStack() |
| 96 | + |
| 97 | + async def async_routine(): |
| 98 | + ctx, init = await make_capnp_init_ctx(self) |
| 99 | + self.log.debug("Create Mining proxy object") |
| 100 | + mining = init.makeMining(ctx).result |
| 101 | + |
| 102 | + self.log.debug("Create a template") |
| 103 | + opts = self.capnp_modules['mining'].BlockCreateOptions() |
| 104 | + template = (await mining.createNewBlock(ctx, opts)).result |
| 105 | + |
| 106 | + self.log.debug("Wait for a new template") |
| 107 | + waitoptions = self.capnp_modules['mining'].BlockWaitOptions() |
| 108 | + waitoptions.timeout = timeout |
| 109 | + waitoptions.feeThreshold = 1 |
| 110 | + with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2): |
| 111 | + promise = template.waitNext(ctx, waitoptions) |
| 112 | + await asyncio.sleep(0.1) |
| 113 | + disconnected_log_check.enter_context(node.assert_debug_log(expected_msgs=["IPC server: socket disconnected"], timeout=2)) |
| 114 | + del promise |
| 115 | + |
| 116 | + asyncio.run(capnp.run(async_routine())) |
| 117 | + |
| 118 | + # Wait for socket disconnected log message, then generate a block to |
| 119 | + # cause the waitNext() call to return a new template. This will cause a |
| 120 | + # crash and disconnect with error output. |
| 121 | + disconnected_log_check.close() |
| 122 | + try: |
| 123 | + self.generate(node, 1) |
| 124 | + except (http.client.RemoteDisconnected, BrokenPipeError, ConnectionResetError): |
| 125 | + pass |
| 126 | + node.wait_until_stopped(expected_ret_code=(-11, -6, 1, 66), expected_stderr=re.compile("")) |
| 127 | + self.start_node(0) |
| 128 | + |
| 129 | + def run_thread_busy_test(self): |
| 130 | + """Test behavior when sending multiple calls to the same server thread |
| 131 | + which used to cause a crash as reported |
| 132 | + https://github.com/bitcoin/bitcoin/issues/33923 and currently causes a |
| 133 | + thread busy error. A future change will make this just queue the calls |
| 134 | + for execution and not trigger any error""" |
| 135 | + node = self.nodes[0] |
| 136 | + self.log.info("Running thread busy test") |
| 137 | + timeout = self.rpc_timeout * 1000.0 |
| 138 | + |
| 139 | + async def async_routine(): |
| 140 | + ctx, init = await make_capnp_init_ctx(self) |
| 141 | + self.log.debug("Create Mining proxy object") |
| 142 | + mining = init.makeMining(ctx).result |
| 143 | + |
| 144 | + self.log.debug("Create a template") |
| 145 | + opts = self.capnp_modules['mining'].BlockCreateOptions() |
| 146 | + template = (await mining.createNewBlock(ctx, opts)).result |
| 147 | + |
| 148 | + self.log.debug("Wait for a new template") |
| 149 | + waitoptions = self.capnp_modules['mining'].BlockWaitOptions() |
| 150 | + waitoptions.timeout = timeout |
| 151 | + waitoptions.feeThreshold = 1 |
| 152 | + |
| 153 | + # Make multiple waitNext calls where the first will start to |
| 154 | + # execute, the second will be posted waiting to execute, and the |
| 155 | + # third will fail to execute because the execution thread is busy. |
| 156 | + with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2): |
| 157 | + promise1 = template.waitNext(ctx, waitoptions) |
| 158 | + await asyncio.sleep(0.1) |
| 159 | + with node.assert_debug_log(expected_msgs=["BlockTemplate.waitNext", "IPC server post request"], timeout=2): |
| 160 | + promise2 = template.waitNext(ctx, waitoptions) |
| 161 | + await asyncio.sleep(0.1) |
| 162 | + try: |
| 163 | + await template.waitNext(ctx, waitoptions) |
| 164 | + except capnp.lib.capnp.KjException as e: |
| 165 | + assert_equal(e.description, "remote exception: std::exception: thread busy") |
| 166 | + assert_equal(e.type, "FAILED") |
| 167 | + else: |
| 168 | + raise AssertionError("Expected thread busy exception") |
| 169 | + |
| 170 | + # Generate a new block to make the active waitNext calls return, then clean up. |
| 171 | + with node.assert_debug_log(expected_msgs=["IPC server send response"], timeout=2): |
| 172 | + self.generate(node, 1, sync_fun=self.no_op) |
| 173 | + await ((await promise1).result).destroy(ctx) |
| 174 | + await ((await promise2).result).destroy(ctx) |
| 175 | + await template.destroy(ctx) |
| 176 | + |
| 177 | + asyncio.run(capnp.run(async_routine())) |
| 178 | + |
84 | 179 | def run_test(self): |
85 | 180 | self.run_echo_test() |
86 | 181 | self.run_mining_test() |
87 | 182 | self.run_deprecated_mining_test() |
| 183 | + self.run_unclean_disconnect_test() |
| 184 | + self.run_thread_busy_test() |
88 | 185 |
|
89 | 186 | if __name__ == '__main__': |
90 | 187 | IPCInterfaceTest(__file__).main() |
0 commit comments