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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions