Skip to content

Commit e84b047

Browse files
jeremydvossCopilot
andauthored
Remove fixedint (#43659)
* Remove fixedint * changelog * fixedint in dev reqs * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> * Update sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 7ff7224 commit e84b047

File tree

6 files changed

+212
-18
lines changed

6 files changed

+212
-18
lines changed

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
### Other Changes
1818
- Unpinned fixedint dependency
1919
([#43475](https://github.com/Azure/azure-sdk-for-python/pull/43475))
20+
- Remove fixedint dependency
21+
([#43659](https://github.com/Azure/azure-sdk-for-python/pull/43659))
2022

2123
## 1.0.0b44 (2025-10-14)
2224

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
HTTP_CLIENT_REQUEST_DURATION,
1010
HTTP_SERVER_REQUEST_DURATION,
1111
)
12-
# pylint:disable=no-name-in-module
13-
from fixedint import Int32
1412
from azure.core import CaseInsensitiveEnumMeta
1513

1614

@@ -319,8 +317,8 @@ class _RP_Names(Enum):
319317

320318
_SAMPLE_RATE_KEY = "_MS.sampleRate"
321319
_SAMPLING_HASH = 5381
322-
_INTEGER_MAX: int = Int32.maxval
323-
_INTEGER_MIN: int = Int32.minval
320+
_INT32_MAX: int = 2**31 - 1 # 2147483647
321+
_INT32_MIN: int = -2**31 # -2147483648
324322

325323
# AAD Auth
326324

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@
2121
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
2222
from opentelemetry.util.types import Attributes
2323

24-
# pylint:disable=no-name-in-module
25-
from fixedint import Int32
26-
2724
from azure.monitor.opentelemetry.exporter._constants import _SAMPLE_RATE_KEY
2825

2926
from azure.monitor.opentelemetry.exporter._constants import (
3027
_SAMPLING_HASH,
31-
_INTEGER_MAX,
32-
_INTEGER_MIN,
28+
_INT32_MAX,
29+
_INT32_MIN,
3330
)
3431

3532

@@ -342,27 +339,29 @@ def _get_url_for_http_request(attributes: Attributes) -> Optional[str]:
342339

343340
def _get_DJB2_sample_score(trace_id_hex: str) -> float:
344341
# This algorithm uses 32bit integers
345-
hash_value = Int32(_SAMPLING_HASH)
342+
hash_value = _SAMPLING_HASH
346343
for char in trace_id_hex:
347344
hash_value = ((hash_value << 5) + hash_value) + ord(char)
345+
# Correctly emulate signed 32-bit integer overflow using two's complement
346+
hash_value = ((hash_value + 2**31) % 2**32) - 2**31
348347

349-
if hash_value == _INTEGER_MIN:
350-
hash_value = int(_INTEGER_MAX)
348+
if hash_value == _INT32_MIN:
349+
hash_value = int(_INT32_MAX)
351350
else:
352351
hash_value = abs(hash_value)
353352

354-
# divide by _INTEGER_MAX for value between 0 and 1 for sampling score
355-
return float(hash_value) / _INTEGER_MAX
353+
# divide by _INT32_MAX for value between 0 and 1 for sampling score
354+
return float(hash_value) / _INT32_MAX
356355

357356
def _round_down_to_nearest(sampling_percentage: float) -> float:
358357
if sampling_percentage == 0:
359358
return 0
360359
# Handle extremely small percentages that would cause overflow
361-
if sampling_percentage <= _INTEGER_MIN: # Extremely small threshold
360+
if sampling_percentage <= _INT32_MIN: # Extremely small threshold
362361
return 0.0
363362
item_count = 100.0 / sampling_percentage
364363
# Handle case where item_count is infinity or too large for math.ceil
365-
if not math.isfinite(item_count) or item_count >= _INTEGER_MAX:
364+
if not math.isfinite(item_count) or item_count >= _INT32_MAX:
366365
return 0.0
367366
return 100.0 / math.ceil(item_count)
368367

sdk/monitor/azure-monitor-opentelemetry-exporter/dev_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
../../core/azure-core-tracing-opentelemetry
44
-e ../../identity/azure-identity
55
aiohttp>=3.0; python_version >= '3.7'
6+
fixedint<1.0.0,>=0.1.6

sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@
8484
install_requires=[
8585
"azure-core<2.0.0,>=1.28.0",
8686
"azure-identity~=1.17",
87-
# TODO: Remove fixedint
88-
"fixedint<1.0.0,>=0.1.6",
8987
"msrest>=0.6.10",
9088
"opentelemetry-api~=1.35",
9189
"opentelemetry-sdk~=1.35",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import unittest
5+
from unittest import mock
6+
7+
from azure.monitor.opentelemetry.exporter.export.trace._utils import (
8+
_get_DJB2_sample_score,
9+
)
10+
# fixedint was removed as a source dependency. It is used as a dev requirement to test sample score
11+
from fixedint import Int32
12+
from azure.monitor.opentelemetry.exporter._constants import (
13+
_SAMPLING_HASH,
14+
_INT32_MAX,
15+
_INT32_MIN,
16+
)
17+
18+
19+
class TestGetDJB2SampleScore(unittest.TestCase):
20+
"""Test cases for _get_DJB2_sample_score function."""
21+
22+
def test_empty_string(self):
23+
"""Test with empty string."""
24+
result = _get_DJB2_sample_score("")
25+
# With empty string, hash should remain _SAMPLING_HASH (5381)
26+
# Result should be 5381 / _INT32_MAX
27+
expected = float(5381) / _INT32_MAX
28+
self.assertEqual(result, expected)
29+
30+
def test_single_character(self):
31+
"""Test with single character."""
32+
result = _get_DJB2_sample_score("a")
33+
# hash = ((5381 << 5) + 5381) + ord('a')
34+
# hash = (172192 + 5381) + 97 = 177670
35+
expected_hash = ((5381 << 5) + 5381) + ord('a')
36+
expected = float(expected_hash) / _INT32_MAX
37+
self.assertEqual(result, expected)
38+
39+
def test_typical_trace_id(self):
40+
"""Test with typical 32-character trace ID."""
41+
trace_id = "12345678901234567890123456789012"
42+
result = _get_DJB2_sample_score(trace_id)
43+
44+
# Manually calculate expected result
45+
hash_value = Int32(_SAMPLING_HASH)
46+
for char in trace_id:
47+
hash_value = ((hash_value << 5) + hash_value) + ord(char)
48+
49+
if hash_value == _INT32_MIN:
50+
hash_value = int(_INT32_MAX)
51+
else:
52+
hash_value = abs(hash_value)
53+
54+
expected = float(hash_value) / _INT32_MAX
55+
self.assertEqual(result, expected)
56+
57+
def test_hex_characters(self):
58+
"""Test with valid hex characters (0-9, a-f)."""
59+
test_cases = [
60+
"0123456789abcdef",
61+
"fedcba9876543210",
62+
"aaaaaaaaaaaaaaaa",
63+
"0000000000000000",
64+
"ffffffffffffffff"
65+
]
66+
67+
for trace_id in test_cases:
68+
with self.subTest(trace_id=trace_id):
69+
result = _get_DJB2_sample_score(trace_id)
70+
self.assertIsInstance(result, float)
71+
self.assertGreaterEqual(result, 0.0)
72+
self.assertLessEqual(result, 1.0)
73+
74+
def test_int32_overflow_handling(self):
75+
"""Test that Int32 overflow is handled correctly."""
76+
# Create a string that should cause overflow
77+
long_string = "f" * 100 # 100 'f' characters should cause overflow
78+
result = _get_DJB2_sample_score(long_string)
79+
80+
self.assertIsInstance(result, float)
81+
self.assertGreaterEqual(result, 0.0)
82+
self.assertLessEqual(result, 1.0)
83+
84+
def test_int32_minimum_value_handling(self):
85+
"""Test handling when hash equals INTEGER_MIN."""
86+
# This is tricky to test directly since we need to find a string
87+
# that results in exactly _INT32_MIN. Instead, let's test the logic.
88+
89+
# We'll use a mock to simulate this condition
90+
91+
92+
def mock_djb2_with_min_value(trace_id_hex):
93+
# Call original to get the structure, then simulate _INT32_MIN case
94+
hash_value = Int32(_SAMPLING_HASH)
95+
for char in trace_id_hex:
96+
hash_value = ((hash_value << 5) + hash_value) + ord(char)
97+
98+
# Simulate the case where we get _INT32_MIN
99+
if str(trace_id_hex) == "test_min":
100+
hash_value = Int32(_INT32_MIN)
101+
102+
if hash_value == _INT32_MIN:
103+
hash_value = int(_INT32_MAX)
104+
else:
105+
hash_value = abs(hash_value)
106+
107+
return float(hash_value) / _INT32_MAX
108+
109+
# Test the _INT32_MIN case
110+
result = mock_djb2_with_min_value("test_min")
111+
expected = float(_INT32_MAX) / _INT32_MAX
112+
self.assertEqual(result, expected)
113+
114+
def test_negative_hash_conversion(self):
115+
"""Test that negative hash values are converted to positive."""
116+
# Find a string that produces a negative hash
117+
test_string = "negative_test_case_string"
118+
result = _get_DJB2_sample_score(test_string)
119+
120+
# Result should always be positive (between 0 and 1)
121+
self.assertGreaterEqual(result, 0.0)
122+
self.assertLessEqual(result, 1.0)
123+
124+
def test_deterministic_output(self):
125+
"""Test that same input always produces same output."""
126+
trace_id = "abcdef1234567890abcdef1234567890"
127+
128+
# Call multiple times with same input
129+
results = [_get_DJB2_sample_score(trace_id) for _ in range(5)]
130+
131+
# All results should be identical
132+
self.assertTrue(all(r == results[0] for r in results))
133+
134+
def test_different_inputs_different_outputs(self):
135+
"""Test that different inputs produce different outputs."""
136+
trace_ids = [
137+
"12345678901234567890123456789012",
138+
"12345678901234567890123456789013", # Last digit different
139+
"22345678901234567890123456789012", # First digit different
140+
"abcdef1234567890fedcba0987654321", # Completely different
141+
]
142+
143+
results = [_get_DJB2_sample_score(tid) for tid in trace_ids]
144+
145+
# All results should be different
146+
self.assertEqual(len(results), len(set(results)))
147+
148+
def test_boundary_values(self):
149+
"""Test with boundary values and edge cases."""
150+
test_cases = [
151+
"0", # Single minimum hex digit
152+
"f", # Single maximum hex digit
153+
"00000000000000000000000000000000", # All zeros
154+
"ffffffffffffffffffffffffffffffff", # All f's (32 chars)
155+
]
156+
157+
for trace_id in test_cases:
158+
with self.subTest(trace_id=trace_id):
159+
result = _get_DJB2_sample_score(trace_id)
160+
self.assertIsInstance(result, float)
161+
self.assertGreaterEqual(result, 0.0)
162+
self.assertLessEqual(result, 1.0)
163+
164+
def test_constants_used_correctly(self):
165+
"""Test that the function uses the expected constants."""
166+
# Verify that _SAMPLING_HASH is 5381 (standard DJB2 hash initial value)
167+
self.assertEqual(_SAMPLING_HASH, 5381)
168+
169+
# Verify Int32 constants
170+
self.assertEqual(_INT32_MAX, 2147483647) # 2^31 - 1
171+
self.assertEqual(_INT32_MIN, -2147483648) # -2^31
172+
173+
def test_algorithm_correctness(self):
174+
"""Test that the DJB2 algorithm is implemented correctly."""
175+
# Test with known input and manually calculated expected output
176+
trace_id = "abc"
177+
178+
# Manual calculation:
179+
# Start with 5381
180+
# For 'a' (97): hash = ((5381 << 5) + 5381) + 97 = 177670
181+
# For 'b' (98): hash = ((177670 << 5) + 177670) + 98 = 5823168
182+
# For 'c' (99): hash = ((5823168 << 5) + 5823168) + 99 = 191582563
183+
184+
expected_hash = _SAMPLING_HASH
185+
for char in trace_id:
186+
expected_hash = Int32(((expected_hash << 5) + expected_hash) + ord(char))
187+
188+
if expected_hash == _INT32_MIN:
189+
expected_hash = int(_INT32_MAX)
190+
else:
191+
expected_hash = abs(expected_hash)
192+
193+
expected_result = float(expected_hash) / _INT32_MAX
194+
actual_result = _get_DJB2_sample_score(trace_id)
195+
196+
self.assertEqual(actual_result, expected_result)

0 commit comments

Comments
 (0)