Repository: open-telemetry/opentelemetry-python

Issue:
Replace 'backoff' dependency with a local implementation
The 'backoff' dependency can be replaced with a few lines of code so I propose we remove it.

**Is your feature request related to a problem?**
There have been issues with the backoff dependency in the past, in particular relating to the transition from v1 to v2 and we have code and tests to deal with this transition.

**Describe the solution you'd like**
Replace the dependency with a function.

**Describe alternatives you've considered**
Keeping the status quo would also work, but the dependency appears to be more trouble than it's worth.

**Additional context**
PR [here](https://github.com/open-telemetry/opentelemetry-python/pull/3679)


Discussion:

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py
--- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/__init__.py
@@ -15,9 +15,17 @@
 
 import logging
 from collections.abc import Sequence
-from typing import Any, Mapping, Optional, List, Callable, TypeVar, Dict
-
-import backoff
+from itertools import count
+from typing import (
+    Any,
+    Mapping,
+    Optional,
+    List,
+    Callable,
+    TypeVar,
+    Dict,
+    Iterator,
+)
 
 from opentelemetry.sdk.util.instrumentation import InstrumentationScope
 from opentelemetry.proto.common.v1.common_pb2 import (
@@ -37,7 +45,6 @@
 from opentelemetry.sdk.trace import Resource
 from opentelemetry.util.types import Attributes
 
-
 _logger = logging.getLogger(__name__)
 
 _TypingResourceT = TypeVar("_TypingResourceT")
@@ -113,7 +120,6 @@ def _get_resource_data(
     resource_class: Callable[..., _TypingResourceT],
     name: str,
 ) -> List[_TypingResourceT]:
-
     resource_data = []
 
     for (
@@ -134,14 +140,36 @@ def _get_resource_data(
     return resource_data
 
 
-# Work around API change between backoff 1.x and 2.x. Since 2.0.0 the backoff
-# wait generator API requires a first .send(None) before reading the backoff
-# values from the generator.
-_is_backoff_v2 = next(backoff.expo()) is None
-
-
-def _create_exp_backoff_generator(*args, **kwargs):
-    gen = backoff.expo(*args, **kwargs)
-    if _is_backoff_v2:
-        gen.send(None)
-    return gen
+def _create_exp_backoff_generator(max_value: int = 0) -> Iterator[int]:
+    """
+    Generates an infinite sequence of exponential backoff values. The sequence starts
+    from 1 (2^0) and doubles each time (2^1, 2^2, 2^3, ...). If a max_value is specified
+    and non-zero, the generated values will not exceed this maximum, capping at max_value
+    instead of growing indefinitely.
+
+    Parameters:
+    - max_value (int, optional): The maximum value to yield. If 0 or not provided, the
+      sequence grows without bound.
+
+    Returns:
+    Iterator[int]: An iterator that yields the exponential backoff values, either uncapped or
+    capped at max_value.
+
+    Example:
+    ```
+    gen = _create_exp_backoff_generator(max_value=10)
+    for _ in range(5):
+        print(next(gen))
+    ```
+    This will print:
+    1
+    2
+    4
+    8
+    10
+
+    Note: this functionality used to be handled by the 'backoff' package.
+    """
+    for i in count(0):
+        out = 2**i
+        yield min(out, max_value) if max_value else out


Test patch:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_backoff.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_backoff.py
new file mode 100644
--- /dev/null
+++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_backoff.py
@@ -0,0 +1,46 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from unittest import TestCase
+
+from opentelemetry.exporter.otlp.proto.common._internal import (
+    _create_exp_backoff_generator,
+)
+
+
+class TestBackoffGenerator(TestCase):
+    def test_exp_backoff_generator(self):
+        generator = _create_exp_backoff_generator()
+        self.assertEqual(next(generator), 1)
+        self.assertEqual(next(generator), 2)
+        self.assertEqual(next(generator), 4)
+        self.assertEqual(next(generator), 8)
+        self.assertEqual(next(generator), 16)
+
+    def test_exp_backoff_generator_with_max(self):
+        generator = _create_exp_backoff_generator(max_value=4)
+        self.assertEqual(next(generator), 1)
+        self.assertEqual(next(generator), 2)
+        self.assertEqual(next(generator), 4)
+        self.assertEqual(next(generator), 4)
+        self.assertEqual(next(generator), 4)
+
+    def test_exp_backoff_generator_with_odd_max(self):
+        # use a max_value that's not in the set
+        generator = _create_exp_backoff_generator(max_value=11)
+        self.assertEqual(next(generator), 1)
+        self.assertEqual(next(generator), 2)
+        self.assertEqual(next(generator), 4)
+        self.assertEqual(next(generator), 8)
+        self.assertEqual(next(generator), 11)
diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/test_otlp_trace_exporter.py
@@ -28,7 +28,6 @@
 from opentelemetry.attributes import BoundedAttributes
 from opentelemetry.exporter.otlp.proto.common._internal import (
     _encode_key_value,
-    _is_backoff_v2,
 )
 from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
     OTLPSpanExporter,
@@ -460,23 +459,6 @@ def test_otlp_headers(self, mock_ssl_channel, mock_secure):
             (("user-agent", "OTel-OTLP-Exporter-Python/" + __version__),),
         )
 
-    @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff")
-    @patch("opentelemetry.exporter.otlp.proto.grpc.exporter.sleep")
-    def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff):
-        # In backoff ~= 2.0.0 the first value yielded from expo is None.
-        def generate_delays(*args, **kwargs):
-            if _is_backoff_v2:
-                yield None
-            yield 1
-
-        mock_backoff.expo.configure_mock(**{"side_effect": generate_delays})
-
-        add_TraceServiceServicer_to_server(
-            TraceServiceServicerUNAVAILABLE(), self.server
-        )
-        self.exporter.export([self.span])
-        mock_sleep.assert_called_once_with(1)
-
     @patch(
         "opentelemetry.exporter.otlp.proto.grpc.exporter._create_exp_backoff_generator"
     )
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/metrics/test_otlp_metrics_exporter.py
@@ -15,13 +15,12 @@
 from logging import WARNING
 from os import environ
 from unittest import TestCase
-from unittest.mock import MagicMock, Mock, patch
+from unittest.mock import MagicMock, Mock, call, patch
 
 from requests import Session
 from requests.models import Response
 from responses import POST, activate, add
 
-from opentelemetry.exporter.otlp.proto.common._internal import _is_backoff_v2
 from opentelemetry.exporter.otlp.proto.common.metrics_encoder import (
     encode_metrics,
 )
@@ -298,17 +297,8 @@ def test_serialization(self, mock_post):
         )
 
     @activate
-    @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff")
     @patch("opentelemetry.exporter.otlp.proto.http.metric_exporter.sleep")
-    def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff):
-        # In backoff ~= 2.0.0 the first value yielded from expo is None.
-        def generate_delays(*args, **kwargs):
-            if _is_backoff_v2:
-                yield None
-            yield 1
-
-        mock_backoff.expo.configure_mock(**{"side_effect": generate_delays})
-
+    def test_exponential_backoff(self, mock_sleep):
         # return a retryable error
         add(
             POST,
@@ -323,7 +313,9 @@ def generate_delays(*args, **kwargs):
         metrics_data = self.metrics["sum_int"]
 
         exporter.export(metrics_data)
-        mock_sleep.assert_called_once_with(1)
+        mock_sleep.assert_has_calls(
+            [call(1), call(2), call(4), call(8), call(16), call(32)]
+        )
 
     def test_aggregation_temporality(self):
 
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py
@@ -16,13 +16,12 @@
 
 import unittest
 from typing import List
-from unittest.mock import MagicMock, Mock, patch
+from unittest.mock import MagicMock, Mock, call, patch
 
 import requests
 import responses
 
 from opentelemetry._logs import SeverityNumber
-from opentelemetry.exporter.otlp.proto.common._internal import _is_backoff_v2
 from opentelemetry.exporter.otlp.proto.http import Compression
 from opentelemetry.exporter.otlp.proto.http._log_exporter import (
     DEFAULT_COMPRESSION,
@@ -169,17 +168,8 @@ def test_exporter_env(self):
         self.assertIsInstance(exporter._session, requests.Session)
 
     @responses.activate
-    @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff")
     @patch("opentelemetry.exporter.otlp.proto.http._log_exporter.sleep")
-    def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff):
-        # In backoff ~= 2.0.0 the first value yielded from expo is None.
-        def generate_delays(*args, **kwargs):
-            if _is_backoff_v2:
-                yield None
-            yield 1
-
-        mock_backoff.expo.configure_mock(**{"side_effect": generate_delays})
-
+    def test_exponential_backoff(self, mock_sleep):
         # return a retryable error
         responses.add(
             responses.POST,
@@ -192,7 +182,9 @@ def generate_delays(*args, **kwargs):
         logs = self._get_sdk_log_data()
 
         exporter.export(logs)
-        mock_sleep.assert_called_once_with(1)
+        mock_sleep.assert_has_calls(
+            [call(1), call(2), call(4), call(8), call(16), call(32)]
+        )
 
     @staticmethod
     def _get_sdk_log_data() -> List[LogData]:
diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py
--- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py
+++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py
@@ -14,12 +14,11 @@
 
 import unittest
 from collections import OrderedDict
-from unittest.mock import MagicMock, Mock, patch
+from unittest.mock import MagicMock, Mock, call, patch
 
 import requests
 import responses
 
-from opentelemetry.exporter.otlp.proto.common._internal import _is_backoff_v2
 from opentelemetry.exporter.otlp.proto.http import Compression
 from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
     DEFAULT_COMPRESSION,
@@ -205,17 +204,8 @@ def test_headers_parse_from_env(self):
 
     # pylint: disable=no-self-use
     @responses.activate
-    @patch("opentelemetry.exporter.otlp.proto.common._internal.backoff")
     @patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.sleep")
-    def test_handles_backoff_v2_api(self, mock_sleep, mock_backoff):
-        # In backoff ~= 2.0.0 the first value yielded from expo is None.
-        def generate_delays(*args, **kwargs):
-            if _is_backoff_v2:
-                yield None
-            yield 1
-
-        mock_backoff.expo.configure_mock(**{"side_effect": generate_delays})
-
+    def test_exponential_backoff(self, mock_sleep):
         # return a retryable error
         responses.add(
             responses.POST,
@@ -239,7 +229,9 @@ def generate_delays(*args, **kwargs):
         )
 
         exporter.export([span])
-        mock_sleep.assert_called_once_with(1)
+        mock_sleep.assert_has_calls(
+            [call(1), call(2), call(4), call(8), call(16), call(32)]
+        )
 
     @patch.object(OTLPSpanExporter, "_export", return_value=Mock(ok=True))
     def test_2xx_status_code(self, mock_otlp_metric_exporter):