Skip to content

mqotel: otel_get_trace_after() raises UnboundLocalError with MsgHandle #20

@davzucky

Description

@davzucky

Summary

ibmmq 2.0.5 crashes in mqotel.otel_get_trace_after() when OpenTelemetry is enabled and a Queue.get() path uses a usable gmo.MsgHandle.

The failure is:

UnboundLocalError: cannot access local variable 'hc' where it is not associated with a value

The immediate problem is that hc is referenced before it is assigned:

removed = 0
mh = gmo.MsgHandle
if _is_usable_handle(mh):
    temp_msg_handle = MessageHandle(qmgr=hc, dup_handle=mh)

    ...

    tmp_ho = ho
    hc = ho.get_queue_manager()

In code/ibmmq/mqotel.py, hc is used in MessageHandle(qmgr=hc, dup_handle=mh) before hc = ho.get_queue_manager() is executed later in the same block.

When this is hit

This becomes reachable when all of the following are true:

  1. OpenTelemetry is importable, so mqotel is auto-enabled.
  2. A message is received through the OTel post-processing path.
  3. gmo.MsgHandle contains a usable handle, for example when MQGMO_PROPERTIES_IN_HANDLE is used.

A real-world example is a consumer that creates a message handle and assigns it to gmo.MsgHandle before Queue.get().

Minimal reproduction test

I added this focused unit test locally to reproduce the bug without needing a live queue manager or compiled extension. It loads mqotel.py directly with stubbed dependencies and exercises the exact failing branch.

"""Regression coverage for mqotel message-handle processing."""

import importlib.util
import sys
import types
import unittest
from pathlib import Path
from unittest import mock


MQOTEL_PATH = Path(__file__).resolve().parents[1] / "ibmmq" / "mqotel.py"


def _noop(*args, **kwargs):
    return None


class _FakeCMQC:
    MQGMO_PROPERTIES_FORCE_MQRFH2 = 0x0001
    MQGMO_PROPERTIES_IN_HANDLE = 0x0002
    MQGMO_NO_PROPERTIES = 0x0004
    MQGMO_PROPERTIES_COMPATIBILITY = 0x0008
    MQOO_INPUT_AS_Q_DEF = 0x0010
    MQOO_INPUT_SHARED = 0x0020
    MQOO_INPUT_EXCLUSIVE = 0x0040
    MQHM_NONE = 0
    MQHM_UNUSABLE_HMSG = -1
    MQHO_NONE = 0
    MQHO_UNUSABLE_HOBJ = -1
    MQIMPO_CONVERT_VALUE = 0x0080
    MQIMPO_INQ_FIRST = 0x0100
    MQRC_PROPERTY_NOT_AVAILABLE = 2492
    MQFMT_RF_HEADER_2 = b"MQHRF2  "


class _FakeMQMIError(Exception):
    def __init__(self, reason):
        super().__init__(reason)
        self.reason = reason


class _FakeMessageHandle:
    def __init__(self, qmgr=None, dup_handle=None):
        self.qmgr = qmgr
        self.dup_handle = dup_handle

    def inq(self, impo, pd, prop):
        raise _FakeMQMIError(_FakeCMQC.MQRC_PROPERTY_NOT_AVAILABLE)

    def get_handle(self):
        return self.dup_handle or 1

    def dlt(self):
        return None


class _FakePD:
    pass


class _FakeIMPO:
    def __init__(self):
        self.Options = 0


class _FakeOTelFunctions:
    disc = None
    open = None
    close = None
    put_trace_before = None
    put_trace_after = None
    get_trace_before = None
    get_trace_after = None


def _load_mqotel_module():
    invalid_span = object()

    fake_trace = types.ModuleType("opentelemetry.trace")
    fake_trace.INVALID_SPAN = invalid_span
    fake_trace.get_current_span = lambda: invalid_span

    fake_opentelemetry = types.ModuleType("opentelemetry")
    fake_opentelemetry.trace = fake_trace

    fake_mqlog = types.ModuleType("mqlog")
    for func_name in ("trace_entry", "trace_exit", "debug", "trace", "error"):
        setattr(fake_mqlog, func_name, _noop)

    fake_mqcommon = types.ModuleType("mqcommon")
    fake_mqcommon.OTelFunctions = _FakeOTelFunctions
    fake_mqcommon.__all__ = ["OTelFunctions"]

    fake_mqerrors = types.ModuleType("mqerrors")
    fake_mqerrors.MQMIError = _FakeMQMIError
    fake_mqerrors.__all__ = ["MQMIError"]

    fake_ibmmq = types.ModuleType("ibmmq")
    fake_ibmmq.CMQC = _FakeCMQC
    fake_ibmmq.MessageHandle = _FakeMessageHandle
    fake_ibmmq.Queue = type("Queue", (), {})
    fake_ibmmq.OD = type("OD", (), {})
    fake_ibmmq.PD = _FakePD
    fake_ibmmq.IMPO = _FakeIMPO
    fake_ibmmq.SMPO = type("SMPO", (), {})
    fake_ibmmq.RFH2 = type("RFH2", (), {})

    fake_modules = {
        "opentelemetry": fake_opentelemetry,
        "opentelemetry.trace": fake_trace,
        "mqlog": fake_mqlog,
        "mqcommon": fake_mqcommon,
        "mqerrors": fake_mqerrors,
        "ibmmq": fake_ibmmq,
    }

    spec = importlib.util.spec_from_file_location("mqotel_bug_repro", MQOTEL_PATH)
    module = importlib.util.module_from_spec(spec)

    with mock.patch.dict(sys.modules, fake_modules):
        spec.loader.exec_module(module)

    return module


class TestMQOTel(unittest.TestCase):
    def test_get_trace_after_with_msg_handle_returns_without_crashing(self):
        mqotel = _load_mqotel_module()
        ho = types.SimpleNamespace(
            get_handle=lambda: 37,
            get_queue_manager=lambda: types.SimpleNamespace(get_handle=lambda: 91),
        )
        gmo = types.SimpleNamespace(
            MsgHandle=1234,
            Options=_FakeCMQC.MQGMO_PROPERTIES_IN_HANDLE,
        )
        md = types.SimpleNamespace(Format=b"", CodedCharSetId=0, Encoding=0)

        removed = mqotel.otel_get_trace_after(ho, gmo, md, None, b"payload", False)

        self.assertEqual(0, removed)

Running it with:

python -m unittest discover -s code/tests -p 'test_mqotel.py'

produces:

ERROR: test_get_trace_after_with_msg_handle_returns_without_crashing
Traceback (most recent call last):
  File ".../code/tests/test_mqotel.py", line 139, in test_get_trace_after_with_msg_handle_returns_without_crashing
    removed = mqotel.otel_get_trace_after(ho, gmo, md, None, b"payload", False)
  File ".../code/ibmmq/mqotel.py", line 520, in otel_get_trace_after
    temp_msg_handle = MessageHandle(qmgr=hc, dup_handle=mh)
UnboundLocalError: cannot access local variable 'hc' where it is not associated with a value

Expected behavior

otel_get_trace_after() should not crash when gmo.MsgHandle is usable. It should obtain the queue manager handle before constructing the duplicate MessageHandle and continue processing normally.

Possible fix

Move:

hc = ho.get_queue_manager()

so it executes before:

temp_msg_handle = MessageHandle(qmgr=hc, dup_handle=mh)

Workaround

Setting MQIPY_NOOTEL=true avoids the broken OTel integration path, but that is only a workaround and disables MQ-specific OTel propagation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions