Skip to content

Commit 3bf0621

Browse files
refactor(shim): harden async message handling and clean stub/utils
- Replace global handler state with per-instance state to avoid cross-stream bleed - Keep concurrent message handling but track task failures explicitly in logs - Fix missing await paths on error message writes - Remove wildcard import in stub and initialize safe defaults for creator/timestamp - Add explicit error for empty function payload in stub - Remove debug print side effect and unused import in utils Validation: - py_compile on handler/stub/utils - pytest tests/test_handler.py - smoke invoke/query for examples/ccaas/asset-transfer-basic - smoke create/transfer/query for examples/ccaas/asset-transfer-sbe Signed-off-by: kmilo <kmilo.denis.glez@yandex.com>
1 parent 486226a commit 3bf0621

3 files changed

Lines changed: 48 additions & 35 deletions

File tree

‎src/fabric_shim/handler.py‎

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,31 @@ class STATES:
3737
COMPOSITEKEY_NS = '\x00'
3838
EMPTY_KEY_SUBSTITUTE = '\x01'
3939

40-
STATE = STATES.CREATED
41-
42-
4340
class Handler:
4441
def __init__(self, cc_id: str, cc: Chaincode) -> None:
4542
self.chaincode_id = cc_pb2.ChaincodeID()
4643
self.chaincode_id.name = cc_id
4744
self.chaincode = cc
4845
self.msg_queue_handler = None
4946
self.context = None
47+
self.state = STATES.CREATED
48+
self._pending_tasks = set()
49+
50+
def _track_task(self, coro):
51+
"""Schedule message handling while surfacing task failures in logs."""
52+
task = asyncio.create_task(coro)
53+
self._pending_tasks.add(task)
54+
55+
def _done_callback(done_task):
56+
self._pending_tasks.discard(done_task)
57+
try:
58+
exc = done_task.exception()
59+
except asyncio.CancelledError:
60+
return
61+
if exc is not None:
62+
LOGGER.exception('Unhandled exception while processing peer message', exc_info=exc)
63+
64+
task.add_done_callback(_done_callback)
5065

5166
async def handle_stub_interaction(self, msg, action="Invoke"):
5267
"""handle_message calls the Init | Invoke function of the associated chaincode."""
@@ -118,65 +133,51 @@ async def handle_message_ready(self, msg):
118133
await self.handle_stub_interaction(msg, "Invoke")
119134
return
120135
else:
121-
self.context.write(new_error_msg(msg, STATE))
136+
await self.context.write(new_error_msg(msg, self.state))
122137

123-
def handle_message_established(self, msg):
138+
async def handle_message_established(self, msg):
124139
"""
125140
handle_message_established handles messages received from the peer when the handler is in the "established" state.
126141
"""
127-
global STATE
128142
if msg.type != ccshim_pb2.ChaincodeMessage.READY:
129143
LOGGER.error(f'Chaincode is in "established" state, can only process messages of type "ready", '
130144
f'but received "{msg.type}"')
131-
# write is an async coroutine on the context
132-
try:
133-
return asyncio.create_task(self.context.write(new_error_msg(msg, STATE)))
134-
except Exception:
135-
return
145+
await self.context.write(new_error_msg(msg, self.state))
136146
else:
137147
LOGGER.info('Successfully established communication with peer node. State transferred to "ready"')
138-
STATE = STATES.READY
148+
self.state = STATES.READY
139149

140-
def handle_message_created(self, msg):
150+
async def handle_message_created(self, msg):
141151
"""handle_message_created handles messages received from the peer when the handler is in the "created" state."""
142-
global STATE
143152
if msg.type != ccshim_pb2.ChaincodeMessage.REGISTERED:
144153
LOGGER.error(f'Chaincode is in "created" state, can only process messages of type "registered", '
145154
f'but received "{msg.type}"')
146-
try:
147-
return asyncio.create_task(self.context.write(new_error_msg(msg, STATE)))
148-
except Exception:
149-
return
155+
await self.context.write(new_error_msg(msg, self.state))
150156
else:
151157
LOGGER.info('Successfully registered with peer node. State transferred to "established"')
152-
STATE = STATES.ESTABLISHED
158+
self.state = STATES.ESTABLISHED
153159

154160
async def handle_message(self, msg: ccshim_pb2.ChaincodeMessage):
155161
"""handle_message message handles loop for shim side of chaincode/peer stream."""
156162
LOGGER.warning('-->> Look out!')
157-
global STATE
158163

159164
# TODO: ?
160165
if msg.type == ccshim_pb2.ChaincodeMessage.KEEPALIVE:
161166
LOGGER.info('-| KEEPALIVE')
162167
return
163168

164-
if STATE == STATES.READY:
169+
if self.state == STATES.READY:
165170
await self.handle_message_ready(msg)
166-
elif STATE == STATES.ESTABLISHED:
167-
self.handle_message_established(msg)
168-
elif STATE == STATES.CREATED:
169-
self.handle_message_created(msg)
171+
elif self.state == STATES.ESTABLISHED:
172+
await self.handle_message_established(msg)
173+
elif self.state == STATES.CREATED:
174+
await self.handle_message_created(msg)
170175
else:
171-
try:
172-
asyncio.create_task(self.context.write(new_error_msg(msg, STATE)))
173-
except Exception:
174-
LOGGER.exception('Failed to write error message to context')
176+
await self.context.write(new_error_msg(msg, self.state))
175177

176178
async def chat_with_peer(self, stream: AsyncIterable[ccshim_pb2.ChaincodeMessage], context: grpc.aio.ServicerContext):
177179
"""chat stream for peer-chaincode interactions post connection"""
178-
global STATE
179-
STATE = STATES.CREATED
180+
self.state = STATES.CREATED
180181

181182
self.context = context
182183
self.msg_queue_handler = MsgQueueHandler(self)
@@ -197,13 +198,18 @@ async def chat_with_peer(self, stream: AsyncIterable[ccshim_pb2.ChaincodeMessage
197198
return ccshim_pb2.ChaincodeMessage(
198199
type=ccshim_pb2.ChaincodeMessage.ERROR, payload=err_str.encode(encoding='utf-8'))
199200
else:
200-
asyncio.create_task(self.handle_message(receive_message))
201+
# Keep handling asynchronous so the stream can continue receiving
202+
# response frames needed by in-flight request futures.
203+
self._track_task(self.handle_message(receive_message))
201204

202205
LOGGER.info(f'->>>> proposal {receive_message.proposal}')
203206
LOGGER.info(f'->>>> payload {receive_message.payload}')
204207
LOGGER.info(f'->>>> channel ID {receive_message.channel_id}')
205208
LOGGER.info(f'->>>> Tx ID {receive_message.txid}')
206209

210+
if self._pending_tasks:
211+
await asyncio.gather(*self._pending_tasks, return_exceptions=True)
212+
207213
async def handle_get_state(self, collection, key, channel_id, tx_id):
208214
msg_pb = ccshim_pb2.GetState()
209215
msg_pb.key = key

‎src/fabric_shim/stub.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
# contributors. All rights reserved.
33
# SPDX-License-Identifier: Apache-2.0
44
from src.fabric_shim.interfaces import ChaincodeStubInterface
5-
from src.fabric_shim.utils import *
5+
from src.fabric_shim.utils import (
6+
COMPOSITEKEY_NS,
7+
MIN_UNICODE_RUNE_VALUE,
8+
validate_composite_key_attribute,
9+
)
610
from fabric_protos_python.peer import chaincode_pb2 as pb
711
from fabric_protos_python.common import common_pb2 as cm_pb
812
from fabric_protos_python.peer import proposal_pb2 as pr_pb
@@ -23,6 +27,9 @@ def __init__(self, client, channel_id, tx_id, cc_input, signed_proposal_pb):
2327
self.tx_id = tx_id
2428
self.cc_input = cc_input
2529
self.signed_proposal_pb = signed_proposal_pb
30+
self.creator = {}
31+
self.tx_timestamp = None
32+
self.proposal = None
2633
self.validationParameterMetakey = VALIDATION_PARAMETER
2734

2835
if self.signed_proposal_pb:
@@ -104,6 +111,8 @@ def get_txid(self):
104111
def get_function_and_parameters(self):
105112
"""Get function name and parameters of the chaincode calling transaction"""
106113
args = [arg.decode() for arg in self.cc_input.args]
114+
if len(args) == 0:
115+
raise Exception('no function name provided in transaction payload')
107116
function: str = args[0]
108117
params = args[1:]
109118

‎src/fabric_shim/utils.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Auxiliary tools
2-
from fabric_protos_python.peer import chaincode_shim_pb2 as ccshim_pb2
32

43

54
def enum_type(*sequential, **named) -> type:
@@ -18,7 +17,6 @@ def enum_type(*sequential, **named) -> type:
1817
named: Collects all the keyword arguments in a dictionary.
1918
2019
"""
21-
print(sequential, ": ", named)
2220
enums = dict(zip(sequential, range(len(sequential))), **named)
2321
return type('Enum', (), enums)
2422

0 commit comments

Comments
 (0)