python-etcd3-async 0.1.6__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/.cnb.yml +7 -0
  2. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/.coverage +0 -0
  3. python_etcd3_async-0.2.1/.gitignore +18 -0
  4. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/AGENTS.md +16 -2
  5. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/CLAUDE.md +19 -7
  6. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/PKG-INFO +29 -6
  7. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/README.md +9 -3
  8. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/README_CN.md +9 -3
  9. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/getting-started/installation.md +6 -0
  10. python_etcd3_async-0.2.1/docs/en/guide/advanced_queries.md +101 -0
  11. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/configuration.md +7 -1
  12. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/leases_locks.md +2 -0
  13. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/sync_client.md +9 -2
  14. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/index.md +1 -1
  15. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/operations/async_error_handling.md +12 -2
  16. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/operations/auto_reconnection.md +33 -0
  17. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/operations/error_handling.md +10 -7
  18. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/operations/failover.md +5 -1
  19. python_etcd3_async-0.2.1/docs/en/resources/history.md +110 -0
  20. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/resources/releasing.md +24 -3
  21. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/getting-started/installation.md +5 -0
  22. python_etcd3_async-0.2.1/docs/zh/guide/advanced_queries.md +100 -0
  23. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/configuration.md +5 -1
  24. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/leases_locks.md +2 -0
  25. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/sync_client.md +8 -2
  26. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/index.md +1 -1
  27. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/operations/async_error_handling.md +9 -2
  28. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/operations/auto_reconnection.md +26 -0
  29. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/operations/error_handling.md +9 -5
  30. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/operations/failover.md +5 -1
  31. python_etcd3_async-0.2.1/docs/zh/resources/history.md +94 -0
  32. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/resources/releasing.md +19 -2
  33. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/__init__.py +3 -1
  34. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_client.py +21 -0
  35. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_client_extensions.py +147 -59
  36. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_endpoint.py +17 -3
  37. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_managers.py +30 -5
  38. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_multi_client.py +236 -107
  39. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_single_client.py +18 -3
  40. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_watch.py +125 -113
  41. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/client.py +20 -0
  42. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/endpoint.py +16 -0
  43. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/errors.py +14 -0
  44. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/locks.py +49 -13
  45. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/multi_client.py +236 -47
  46. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/shared.py +1 -55
  47. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/sync_kv_operations.py +69 -59
  48. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/utils.py +0 -35
  49. python_etcd3_async-0.2.1/etcdrpc/auth_pb2.py +43 -0
  50. python_etcd3_async-0.2.1/etcdrpc/auth_pb2.pyi +44 -0
  51. python_etcd3_async-0.2.1/etcdrpc/auth_pb2_grpc.py +24 -0
  52. python_etcd3_async-0.2.1/etcdrpc/kv_pb2.py +40 -0
  53. python_etcd3_async-0.2.1/etcdrpc/kv_pb2.pyi +39 -0
  54. python_etcd3_async-0.2.1/etcdrpc/kv_pb2_grpc.py +24 -0
  55. python_etcd3_async-0.2.1/etcdrpc/rpc_pb2.py +237 -0
  56. python_etcd3_async-0.2.1/etcdrpc/rpc_pb2.pyi +811 -0
  57. python_etcd3_async-0.2.1/etcdrpc/rpc_pb2_grpc.py +1893 -0
  58. python_etcd3_async-0.2.1/pyproject.toml +113 -0
  59. python_etcd3_async-0.2.1/scripts/gen-protos.sh +90 -0
  60. python_etcd3_async-0.2.1/scripts/publish.sh +281 -0
  61. python_etcd3_async-0.2.1/tests/test_async_health_check.py +178 -0
  62. python_etcd3_async-0.2.1/tests/test_async_watch.py +87 -0
  63. python_etcd3_async-0.2.1/tests/test_async_watch_cancel.py +90 -0
  64. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_batch_operations.py +90 -8
  65. python_etcd3_async-0.2.1/tests/test_endpoint_selection.py +201 -0
  66. python_etcd3_async-0.2.1/tests/test_grpc_compat.py +160 -0
  67. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_locks.py +41 -0
  68. python_etcd3_async-0.2.1/tests/test_retry_behavior.py +218 -0
  69. python_etcd3_async-0.2.1/tests/test_return_semantics.py +170 -0
  70. python_etcd3_async-0.2.1/tests/test_sync_health_check.py +245 -0
  71. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/uv.lock +11 -5
  72. python_etcd3_async-0.1.6/.gitignore +0 -18
  73. python_etcd3_async-0.1.6/PHASE2_PLAN.md +0 -164
  74. python_etcd3_async-0.1.6/docs/en/guide/advanced_queries.md +0 -55
  75. python_etcd3_async-0.1.6/docs/en/resources/history.md +0 -39
  76. python_etcd3_async-0.1.6/docs/zh/guide/advanced_queries.md +0 -55
  77. python_etcd3_async-0.1.6/docs/zh/resources/history.md +0 -38
  78. python_etcd3_async-0.1.6/gen_protos.py +0 -69
  79. python_etcd3_async-0.1.6/pyproject.toml +0 -72
  80. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/.pylintrc +0 -0
  81. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/.python-version +0 -0
  82. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/SKILL.md +0 -0
  83. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/assets/stylesheets/extra.css +0 -0
  84. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/authentication.md +0 -0
  85. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/cluster_maintenance.md +0 -0
  86. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/leader_election.md +0 -0
  87. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/service_discovery.md +0 -0
  88. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/service_registration.md +0 -0
  89. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/transactions.md +0 -0
  90. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/usage.md +0 -0
  91. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/guide/watching.md +0 -0
  92. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/operations/monitoring.md +0 -0
  93. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/operations/observability.md +0 -0
  94. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/resources/authors.md +0 -0
  95. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/en/resources/contributing.md +0 -0
  96. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/superpowers/plans/2026-04-03-high-cohesion-low-coupling-plan.md +0 -0
  97. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/superpowers/specs/2026-04-03-high-cohesion-low-coupling-design.md +0 -0
  98. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/authentication.md +0 -0
  99. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/cluster_maintenance.md +0 -0
  100. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/leader_election.md +0 -0
  101. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/service_discovery.md +0 -0
  102. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/service_registration.md +0 -0
  103. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/transactions.md +0 -0
  104. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/usage.md +0 -0
  105. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/guide/watching.md +0 -0
  106. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/operations/monitoring.md +0 -0
  107. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/operations/observability.md +0 -0
  108. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/resources/authors.md +0 -0
  109. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/docs/zh/resources/contributing.md +0 -0
  110. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_config.py +0 -0
  111. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/async_monitor.py +0 -0
  112. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/auth.py +0 -0
  113. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/config.py +0 -0
  114. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/events.py +0 -0
  115. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/exceptions.py +0 -0
  116. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/exporters/__init__.py +0 -0
  117. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/exporters/prometheus.py +0 -0
  118. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/leases.py +0 -0
  119. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/log.py +0 -0
  120. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/log_formatters.py +0 -0
  121. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/log_sampling.py +0 -0
  122. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/members.py +0 -0
  123. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/observability.py +0 -0
  124. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/single_client.py +0 -0
  125. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/sync_cluster_operations.py +0 -0
  126. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/sync_lock_operations.py +0 -0
  127. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/sync_maintenance_operations.py +0 -0
  128. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/trace.py +0 -0
  129. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/trace_attributes.py +0 -0
  130. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/transactions.py +0 -0
  131. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcd3/watch.py +0 -0
  132. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcdrpc/__init__.py +0 -0
  133. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcdrpc/auth.proto +0 -0
  134. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcdrpc/kv.proto +0 -0
  135. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/etcdrpc/rpc.proto +0 -0
  136. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/leader_election.py +0 -0
  137. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/leader_election_sync.py +0 -0
  138. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/service_discovery.py +0 -0
  139. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/service_discovery_sync.py +0 -0
  140. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/service_discovery_v2.py +0 -0
  141. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/service_registration.py +0 -0
  142. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/service_registration_sync.py +0 -0
  143. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/examples/service_registry_v2.py +0 -0
  144. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/mkdocs.yml +0 -0
  145. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/scripts/etcd-test.sh +0 -0
  146. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/__init__.py +0 -0
  147. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/conftest.py +0 -0
  148. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_async_client.py +0 -0
  149. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_async_client_integration.py +0 -0
  150. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_async_error_handling.py +0 -0
  151. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_async_kv_operations_integration.py +0 -0
  152. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_async_managers.py +0 -0
  153. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_auth.py +0 -0
  154. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_auth_integration.py +0 -0
  155. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_client.py +0 -0
  156. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_cluster_integration.py +0 -0
  157. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_events.py +0 -0
  158. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_kv_operations.py +0 -0
  159. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_lease_keepalive_retry.py +0 -0
  160. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_leases.py +0 -0
  161. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_leases_integration.py +0 -0
  162. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_locks_integration.py +0 -0
  163. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_log_formatters.py +0 -0
  164. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_log_sampling.py +0 -0
  165. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_maintenance_integration.py +0 -0
  166. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_members.py +0 -0
  167. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_observability_integration.py +0 -0
  168. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_tls_integration.py +0 -0
  169. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_trace_attributes.py +0 -0
  170. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_transactions.py +0 -0
  171. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_transactions_integration.py +0 -0
  172. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_utils.py +0 -0
  173. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_watch.py +0 -0
  174. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/tests/test_watch_integration.py +0 -0
  175. {python_etcd3_async-0.1.6 → python_etcd3_async-0.2.1}/usage_guide.md +0 -0
@@ -35,6 +35,13 @@ $:
35
35
  - name: ruff format
36
36
  script:
37
37
  - uv run --no-sync ruff format --check etcd3/ etcdrpc/ tests/
38
+ - name: generated protobuf code is up to date
39
+ # The generated code in etcdrpc/ is checked in and carries the
40
+ # grpcio/protobuf version floors this package supports. Regenerate
41
+ # it with the pinned codegen toolchain and fail if it drifted.
42
+ script:
43
+ - uv sync --group codegen
44
+ - uv run --no-sync ./scripts/gen-protos.sh --check
38
45
 
39
46
  - name: unit
40
47
  docker:
@@ -0,0 +1,18 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ # The generated protobuf/gRPC code in etcdrpc/ is checked in on purpose:
9
+ # it embeds GRPC_GENERATED_VERSION, which is the floor of the grpcio range
10
+ # this package supports. Regenerate it with scripts/gen-protos.sh.
11
+ .trae/documents/*
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ htmlcov/
15
+ # Virtual environments
16
+ .venv
17
+ /site
18
+ .etcd-tls/
@@ -67,10 +67,24 @@ uv run ruff format etcd3/ etcdrpc/ # Format
67
67
 
68
68
  ### Other
69
69
  ```bash
70
- uv run python gen_protos.py # Regenerate protobuf
70
+ ./scripts/gen-protos.sh # Regenerate the protobuf/gRPC code in etcdrpc/
71
71
  uv run mkdocs serve # Serve docs
72
72
  ```
73
73
 
74
+ ### Protobuf / gRPC version floors
75
+ - `etcdrpc/*_pb2*.py` is **generated code that is checked in**. The
76
+ `grpcio` and `protobuf` floors in `pyproject.toml` must equal the
77
+ versions embedded in it (`GRPC_GENERATED_VERSION` and the protobuf
78
+ gencode version).
79
+ - The `codegen` dependency group pins the generator to that floor, so
80
+ regenerating never silently raises it. `scripts/gen-protos.sh` uses it.
81
+ - The runtime dependencies are **ranges** (`grpcio>=...`, `protobuf>=...`):
82
+ the generated code only rejects a runtime *older* than the floor, so
83
+ newer versions are fine and the package can be installed next to other
84
+ packages that need a different grpcio.
85
+ - `tests/test_grpc_compat.py` fails if the floors and the codegen pin
86
+ drift apart.
87
+
74
88
  ### Versioning
75
89
  - Single source of truth: `__version__` in `etcd3/__init__.py`
76
90
  - `pyproject.toml` uses `dynamic = ["version"]` via
@@ -179,7 +193,7 @@ etcd3/
179
193
  log.py / log_formatters.py / log_sampling.py # Logging
180
194
  observability.py # ObservabilityConfig / configure_from_env / auto_init
181
195
  exporters/ # Prometheus exporter
182
- etcdrpc/ # Generated protobuf/gRPC code
196
+ etcdrpc/ # .proto sources + the generated protobuf/gRPC code
183
197
  tests/ # Test files
184
198
  ```
185
199
 
@@ -82,9 +82,10 @@ uv run ruff format etcd3/ etcdrpc/
82
82
 
83
83
  ### Protobuf Generation
84
84
  ```bash
85
- # Protobuf files are auto-generated during build via hatchling hook
86
- # To manually regenerate:
87
- uv run python gen_protos.py
85
+ # The generated code is checked in; regenerate it with:
86
+ ./scripts/gen-protos.sh
87
+ # CI fails if the checked-in code drifts from the .proto files:
88
+ ./scripts/gen-protos.sh --check
88
89
  ```
89
90
 
90
91
  ### Environment Variables
@@ -212,7 +213,11 @@ Generated gRPC/Python bindings from etcd proto files:
212
213
  - `kv_pb2.py`, `kv_pb2_grpc.py` - Key-value service
213
214
  - `auth_pb2.py`, `auth_pb2_grpc.py` - Authentication service
214
215
 
215
- Compiled during build via `gen_protos.py` (hatchling build hook).
216
+ The generated code is checked in (it embeds `GRPC_GENERATED_VERSION`,
217
+ the oldest grpcio this package supports) and regenerated with
218
+ `scripts/gen-protos.sh` using the pinned `codegen` toolchain. The runtime
219
+ dependencies are ranges, so newer grpcio/protobuf versions work and the
220
+ package installs next to other packages with a different grpcio.
216
221
 
217
222
  ### Examples (`examples/`)
218
223
 
@@ -266,7 +271,11 @@ auto_init() # Auto-initialize at import time
266
271
  - Error handlers are created via `create_sync_error_handlers()` and `create_async_error_handlers()` factories from `errors.py`
267
272
  - `_sync_handle_keepalive_errors` / `_async_handle_keepalive_errors` - Lease keepalive with `propagate_errors=True`
268
273
 
269
- 3. **Endpoint failover:** `MultiEndpointEtcd3Client` tracks failed endpoints with retry on `failover=True`
274
+ 3. **Endpoint failover:** Both `MultiEndpointEtcd3Client` and
275
+ `AsyncMultiEndpointEtcd3Client` track failed endpoints. With
276
+ `failover=True` a failed endpoint is bypassed and another healthy
277
+ endpoint is picked; with `failover=False` the client stays on the
278
+ endpoint it holds and rebuilds that endpoint's channel in place.
270
279
 
271
280
  4. **Load balancing:** Multiple strategies supported via `LoadBalancerStrategy`:
272
281
  - `PRIORITY` (default): Keep current endpoint if healthy, failover on failure
@@ -274,11 +283,14 @@ auto_init() # Auto-initialize at import time
274
283
  - `RANDOM`: Random endpoint selection
275
284
  - `STICKY`: Maintain connection to the same endpoint
276
285
 
277
- 5. **Auto-reconnection:** Async has `_get_channel()`, sync has matching implementation with health check
286
+ 5. **Auto-reconnection:** Async has `_get_channel()` / `_acquire_channel()`,
287
+ sync has a matching `_get_channel()`, both with a background health probe
278
288
 
279
289
  6. **Lazy authentication:** Async uses `_init_auth()` with `asyncio.Lock()`, sync initializes in `__init__`
280
290
 
281
- 7. **Watch pattern:** Iterator + cancel function returns for both sync and async
291
+ 7. **Watch pattern:** Iterator + cancel function returns for both sync and
292
+ async. Cancelling sends a real cancel request to the server and unblocks
293
+ a pending iteration.
282
294
 
283
295
  8. **Request context tracking:** Use `client.context()` to track operations across async tasks:
284
296
  ```python
@@ -1,11 +1,28 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-etcd3-async
3
- Version: 0.1.6
3
+ Version: 0.2.1
4
4
  Summary: etcd3 async sdk
5
+ Project-URL: Repository, https://cnb.cool/owasd/python3-etcd-async
6
+ Project-URL: Source, https://cnb.cool/owasd/python3-etcd-async
7
+ Project-URL: Homepage, https://cnb.cool/owasd/python3-etcd-async
8
+ Project-URL: Issues, https://cnb.cool/owasd/python3-etcd-async/issues
9
+ Project-URL: Changelog, https://cnb.cool/owasd/python3-etcd-async/blob/main/docs/en/resources/history.md
10
+ Author: owasd
11
+ License-Expression: Apache-2.0
12
+ Keywords: async,etcd,etcd3,grpc,kubernetes
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
5
22
  Requires-Python: <4,>=3.12
6
- Requires-Dist: grpcio==1.76.0
23
+ Requires-Dist: grpcio>=1.76.0
7
24
  Requires-Dist: prometheus-client>=0.24.1
8
- Requires-Dist: protobuf==6.31.1
25
+ Requires-Dist: protobuf>=6.31.1
9
26
  Description-Content-Type: text/markdown
10
27
 
11
28
  # python-etcd3-async
@@ -23,7 +40,7 @@ Python async client for the etcd API v3, supporting both synchronous and asynchr
23
40
  - **Async/Sync dual support**: Same API for both async and sync usage with feature parity
24
41
  - **Automatic reconnection**: Auto-rebuild stale connections (e.g., after etcd server restart)
25
42
  - **Keepalive error propagation**: Lease keepalive errors are properly propagated for connection loss detection
26
- - **Endpoint failover**: Built-in support for multiple endpoints with automatic failover
43
+ - **Endpoint failover**: Built-in support for multiple endpoints with opt-in automatic failover (`failover=True`)
27
44
  - **Context manager**: Clean resource management for both sync and async
28
45
  - **Observability**: Structured logging, OpenTelemetry tracing, and Prometheus metrics
29
46
 
@@ -272,8 +289,14 @@ client = await async_client(
272
289
  )
273
290
 
274
291
  # Multiple endpoints with failover
275
- client = await async_client(
276
- endpoints=['localhost:2379', 'localhost:22379'],
292
+ # (the single-endpoint factory does not take endpoints=)
293
+ from etcd3 import AsyncMultiEndpointEtcd3Client, AsyncEndpoint
294
+
295
+ client = AsyncMultiEndpointEtcd3Client(
296
+ endpoints=[
297
+ AsyncEndpoint('localhost', 2379, secure=False),
298
+ AsyncEndpoint('localhost', 22379, secure=False),
299
+ ],
277
300
  failover=True, # Automatically switch to healthy endpoint
278
301
  )
279
302
 
@@ -13,7 +13,7 @@ Python async client for the etcd API v3, supporting both synchronous and asynchr
13
13
  - **Async/Sync dual support**: Same API for both async and sync usage with feature parity
14
14
  - **Automatic reconnection**: Auto-rebuild stale connections (e.g., after etcd server restart)
15
15
  - **Keepalive error propagation**: Lease keepalive errors are properly propagated for connection loss detection
16
- - **Endpoint failover**: Built-in support for multiple endpoints with automatic failover
16
+ - **Endpoint failover**: Built-in support for multiple endpoints with opt-in automatic failover (`failover=True`)
17
17
  - **Context manager**: Clean resource management for both sync and async
18
18
  - **Observability**: Structured logging, OpenTelemetry tracing, and Prometheus metrics
19
19
 
@@ -262,8 +262,14 @@ client = await async_client(
262
262
  )
263
263
 
264
264
  # Multiple endpoints with failover
265
- client = await async_client(
266
- endpoints=['localhost:2379', 'localhost:22379'],
265
+ # (the single-endpoint factory does not take endpoints=)
266
+ from etcd3 import AsyncMultiEndpointEtcd3Client, AsyncEndpoint
267
+
268
+ client = AsyncMultiEndpointEtcd3Client(
269
+ endpoints=[
270
+ AsyncEndpoint('localhost', 2379, secure=False),
271
+ AsyncEndpoint('localhost', 22379, secure=False),
272
+ ],
267
273
  failover=True, # Automatically switch to healthy endpoint
268
274
  )
269
275
 
@@ -13,7 +13,7 @@ Python etcd API v3 异步客户端,支持同步和异步操作。
13
13
  - **同步/异步双模式**: 同一套 API 同时支持 sync 和 async,功能完全对等
14
14
  - **自动重连**: 自动重建失效连接(如 etcd 服务器重启后)
15
15
  - **Keepalive 错误传播**: 租约保活错误正确传播,便于连接断开检测
16
- - **端点故障转移**: 支持多节点自动故障转移
16
+ - **端点故障转移**: 支持多节点,可选开启自动故障转移(`failover=True`)
17
17
  - **上下文管理**: 资源清理更加简洁
18
18
  - **可观测性**: 结构化日志、OpenTelemetry 追踪、Prometheus 指标
19
19
 
@@ -262,8 +262,14 @@ client = await async_client(
262
262
  )
263
263
 
264
264
  # 多节点故障转移
265
- client = await async_client(
266
- endpoints=['localhost:2379', 'localhost:22379'],
265
+ # (单端点工厂函数不接受 endpoints= 参数)
266
+ from etcd3 import AsyncMultiEndpointEtcd3Client, AsyncEndpoint
267
+
268
+ client = AsyncMultiEndpointEtcd3Client(
269
+ endpoints=[
270
+ AsyncEndpoint('localhost', 2379, secure=False),
271
+ AsyncEndpoint('localhost', 22379, secure=False),
272
+ ],
267
273
  failover=True, # 自动切换到健康节点
268
274
  )
269
275
 
@@ -28,6 +28,12 @@ uv pip install -e .
28
28
 
29
29
  - Python 3.12+
30
30
  - etcd v3 API server (v3.1+ recommended)
31
+ - `grpcio>=1.76.0` and `protobuf>=6.31.1`
32
+
33
+ The grpcio and protobuf requirements are **ranges**, not exact pins, so the
34
+ package installs next to other packages that need a different grpcio. The
35
+ generated protobuf code only rejects a runtime that is *older* than the
36
+ version it was generated with, so newer grpcio releases work as well.
31
37
 
32
38
  ## Virtual Environments with uv
33
39
 
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: Advanced Queries
3
+ ---
4
+
5
+ # Advanced Queries
6
+
7
+ ## Range Options
8
+
9
+ Range options are passed as keyword arguments to any range-based call
10
+ (`get`, `get_prefix`, `get_range`, `get_all` and their `*_response` variants);
11
+ they map to the fields of `RangeRequestConfig`.
12
+
13
+ ```python
14
+ # Range options as keyword arguments
15
+ resp = await client.get_prefix_response(
16
+ '/app/',
17
+ limit=100, # Limit number of results
18
+ revision=5, # Historical revision
19
+ count_only=True, # Only return count
20
+ sort_order='ascend', # 'ascend' or 'descend'
21
+ sort_target='key', # 'key' | 'version' | 'create' | 'mod'
22
+ )
23
+ print(f'{resp.count} keys under /app/')
24
+
25
+ # Serializable read (hit the follower, lower latency)
26
+ value, meta = await client.get('/mykey', serializable=True)
27
+
28
+ # Keys only (no values transferred)
29
+ async for value, meta in client.get_prefix('/app/', keys_only=True):
30
+ print(meta.key)
31
+ ```
32
+
33
+ Other available options: `min_mod_revision`, `max_mod_revision`,
34
+ `min_create_revision`, `max_create_revision`.
35
+
36
+ ## Prefix Query
37
+
38
+ ```python
39
+ # Get all keys with prefix
40
+ async for value, meta in client.get_prefix('/app/'):
41
+ print(f'{meta.key}: {value}')
42
+ ```
43
+
44
+ ## Range Query
45
+
46
+ ```python
47
+ # Get keys in range [start, end)
48
+ async for value, meta in client.get_range('/start', '/end'):
49
+ print(f'{meta.key}: {value}')
50
+ ```
51
+
52
+ ## Write Results
53
+
54
+ The sync and the async client return the same shapes for writes:
55
+
56
+ | Call | Result |
57
+ |------|--------|
58
+ | `put(key, value)` | the response header (revision, etc.) |
59
+ | `put(key, value, prev_kv=True)` | `KVMetadata` describing the **previous** value |
60
+ | `delete(key)` | the number of deleted keys |
61
+ | `delete_prefix(prefix)` | the number of deleted keys |
62
+ | `delete(key, return_response=True)` | the full `DeleteRangeResponse` |
63
+
64
+ ```python
65
+ # How many keys did that delete remove?
66
+ removed = await client.delete('/mykey')
67
+
68
+ # What was stored before this write?
69
+ previous = await client.put('/mykey', 'new', prev_kv=True)
70
+ print(previous.value) # b'old'
71
+ ```
72
+
73
+ `delete_prefix` accepts `return_response=True` on both clients as well.
74
+
75
+ ## Batch Operations
76
+
77
+ The three batch entry points share one concurrency and failure policy:
78
+ items run in parallel, the concurrency ceiling is controlled by
79
+ `concurrency`, and a failing item never aborts the whole batch.
80
+
81
+ ```python
82
+ # Atomic transaction: all succeed or none do
83
+ count = await client.kv.batch_put([('/a', '1'), ('/b', '2')])
84
+
85
+ # Parallel execution: at most 10 at a time, failures only affect the count
86
+ count = await client.kv.batch_put(items, atomic=False, concurrency=10)
87
+
88
+ # Returns (success_count, [(failed_key, error), ...])
89
+ success, failed = await client.kv.put_many(items, concurrency=10)
90
+ ```
91
+
92
+ `batch_put` and `put_many` accept either an iterable of `(key, value)`
93
+ tuples or a mapping; `batch_delete` accepts an iterable of keys.
94
+
95
+ The effective ceiling for `concurrency` is the client's `max_concurrent`
96
+ (default 100): callers may lower it, never raise it.
97
+
98
+ ## See Also
99
+
100
+ - [API Usage](usage.md)
101
+ - [Configuration](configuration.md)
@@ -75,12 +75,18 @@ client = AsyncMultiEndpointEtcd3Client(
75
75
  | `user` | str | None | Username for authentication |
76
76
  | `password` | str | None | Password for authentication |
77
77
  | `failover` | bool | False | Enable automatic failover between endpoints |
78
- | `max_retries` | int | 3 | Maximum retries for transient errors |
78
+ | `max_retries` | int | 3 | Maximum retries for transient errors (read paths only) |
79
79
  | `retry_backoff` | float | 0.1 | Initial retry backoff in seconds |
80
80
  | `retry_max_backoff` | float | 10.0 | Maximum retry backoff in seconds |
81
81
  | `load_balancer` | LoadBalancerStrategy | PRIORITY | Load balancing strategy |
82
82
  | `health_check_interval` | float | 30.0 | Health check interval in seconds |
83
83
 
84
+ `failover` and `load_balancer` decide which endpoint a request goes to:
85
+ with `failover=True` a failed endpoint is bypassed, with `failover=False`
86
+ the client stays on its endpoint and rebuilds the channel in place. The
87
+ retry settings apply to the idempotent read paths only — writes are
88
+ never retried automatically.
89
+
84
90
  ## Configuration Classes
85
91
 
86
92
  Every keyword above can also be passed as a typed dataclass via `config=`,
@@ -148,6 +148,8 @@ await lock.acquire()
148
148
  async with lock.auto_refresh(interval=10): # default: ttl / 3
149
149
  await do_long_running_work()
150
150
  # background refresh stops on exit (the lock itself is NOT released here)
151
+ # if the keep-alive loop fails, the error is raised on exit — otherwise the
152
+ # lease may already be gone while the caller still believes it holds the lock
151
153
  ```
152
154
 
153
155
  For sync clients, iterate the generator yourself:
@@ -38,8 +38,8 @@ from etcd3 import MultiEndpointEtcd3Client, Endpoint, LoadBalancerStrategy
38
38
 
39
39
  etcd = MultiEndpointEtcd3Client(
40
40
  endpoints=[
41
- Endpoint('10.0.0.1', 2379),
42
- Endpoint('10.0.0.2', 2379),
41
+ Endpoint('10.0.0.1', 2379, secure=False),
42
+ Endpoint('10.0.0.2', 2379, secure=False),
43
43
  ],
44
44
  failover=True,
45
45
  max_retries=3,
@@ -49,6 +49,10 @@ etcd = MultiEndpointEtcd3Client(
49
49
  )
50
50
  ```
51
51
 
52
+ `Endpoint` defaults to `secure=True`, which requires TLS credentials. Pass
53
+ `secure=False` for a plaintext connection (or supply `ca_cert` /
54
+ `cert_cert` / `cert_key` for TLS).
55
+
52
56
  ## KV Operations
53
57
 
54
58
  ```python
@@ -119,6 +123,9 @@ success, responses = etcd.transaction(
119
123
  | Lock keep-alive | iterate `lock.refresh()` | `async with lock.auto_refresh()` |
120
124
  | Batch ops | top-level `batch_put`/`batch_delete` | `client.kv.batch_put/concurrency=...` |
121
125
  | Maintenance | via `etcd.maintenance.*` | top-level `status/compact/...` |
126
+ | Members | `etcd.members` (property) | `async for m in etcd.members()` |
127
+ | Delete result | number of deleted keys | number of deleted keys |
128
+ | Put result | response header (or `KVMetadata` with `prev_kv=True`) | response header (or `KVMetadata` with `prev_kv=True`) |
122
129
  | Close | `etcd.close()` | `await client.close()` |
123
130
 
124
131
  ## Blocking in Async Code
@@ -10,7 +10,7 @@ python-etcd3-async is an async Python client library for the etcd v3 API based o
10
10
 
11
11
  - **Async First**: Pure async operations using `grpc.aio`
12
12
  - **Sync Support**: Synchronous client API also available
13
- - **Failover**: Multi-endpoint support with automatic failover
13
+ - **Failover**: Multi-endpoint support with opt-in automatic failover
14
14
  - **Auto Reconnection**: Automatic channel rebuild on connection failure
15
15
  - **Complete API**: Full support for all major etcd v3 features
16
16
 
@@ -22,7 +22,7 @@ This guide provides comprehensive information on the error handling mechanisms o
22
22
  The etcd3 async client provides robust error handling capabilities to ensure reliable operation in production environments. Key features include:
23
23
 
24
24
  - **Comprehensive error mapping**: All gRPC status codes are mapped to appropriate Python exceptions
25
- - **Exponential backoff retry**: Automatically retry transient errors with configurable backoff
25
+ - **Exponential backoff retry**: Transient errors are retried on the idempotent read paths
26
26
  - **Resource management**: Proper cleanup of gRPC channels and other resources
27
27
  - **Monitoring**: Detailed metrics collection for error rates, latencies, and endpoint health
28
28
  - **Failover support**: Automatic switching between endpoints when failures occur
@@ -52,13 +52,23 @@ The etcd3 async client maps gRPC status codes to the following exception types:
52
52
 
53
53
  ## Retry Mechanism
54
54
 
55
- The etcd3 async client implements an exponential backoff retry strategy for transient errors. By default, it will retry the following error types:
55
+ The client retries transient errors on the **idempotent read paths** (`get`,
56
+ `get_prefix`, `get_range`, `get_all` and their `*_response` variants).
57
+ By default, it will retry the following error types:
56
58
 
57
59
  - `UNAVAILABLE` (connection errors)
58
60
  - `DEADLINE_EXCEEDED` (timeouts)
59
61
  - `INTERNAL` (server errors)
60
62
  - `RESOURCE_EXHAUSTED` (resource constraints)
61
63
 
64
+ **Writes are not retried automatically.** A failed request may already have
65
+ been applied server-side, so repeating it could duplicate the effect.
66
+ Retry writes in the application, and only where they are safe to repeat
67
+ (idempotent puts). Treat `RevisionCompactedError` as a signal to resync
68
+ state rather than to retry blindly.
69
+
70
+ The same rules apply to the sync client.
71
+
62
72
  ### Retry Configuration
63
73
 
64
74
  You can configure the retry behavior when creating the client:
@@ -25,6 +25,30 @@ required.
25
25
  - Phase 3: Update endpoint state (under lock)
26
26
  5. Once rebuilt successfully, the endpoint is marked as healthy again
27
27
 
28
+ ## Health Probe Task
29
+
30
+ `health_check_interval` drives a background probe task:
31
+
32
+ - It starts lazily on the client's first request — a client that is never
33
+ used owns no task at all;
34
+ - Each cycle probes only the endpoints that are currently marked as failed,
35
+ so healthy endpoints cause no extra round trips;
36
+ - An endpoint that answers again is marked as healthy immediately, without
37
+ waiting for the `time_retry` window to expire;
38
+ - `close()` cancels the task, so its lifetime matches the client's exactly.
39
+
40
+ The sync client behaves the same way, with a daemon thread
41
+ (`etcd3-health-check`) as the carrier instead: it starts lazily when a
42
+ channel is first acquired, and `close()` signals it to exit and waits for
43
+ it to finish. The thread is a daemon, so it never keeps the process alive
44
+ even if `close()` is never called.
45
+
46
+ ```python
47
+ # Inspect endpoint state: marked-failed / ok
48
+ for label, endpoint in client.endpoints.items():
49
+ print(label, 'marked-failed' if endpoint.is_failed() else 'ok')
50
+ ```
51
+
28
52
  ## Tuning Reconnection
29
53
 
30
54
  Recovery attempts are configured on the multi-endpoint clients:
@@ -43,6 +67,15 @@ client = AsyncMultiEndpointEtcd3Client(
43
67
  )
44
68
  ```
45
69
 
70
+ `max_retries` and the backoff settings apply to the idempotent read paths.
71
+ Writes are never retried automatically — see
72
+ [Error Handling](error_handling.md#retry-guidance).
73
+
74
+ With `failover=True` a failed endpoint is bypassed and the client picks
75
+ another healthy one. With `failover=False` the client stays on the
76
+ endpoint it holds and rebuilds that endpoint's channel in place. The sync
77
+ and the async client behave the same way.
78
+
46
79
  Each `Endpoint` also carries `time_retry` (default 300 s) — the window in
47
80
  which a failed endpoint stays marked failed before being considered again.
48
81
 
@@ -60,8 +60,7 @@ current endpoint as failed, which drives [Failover](failover.md) and
60
60
  ## Handler Layers
61
61
 
62
62
  - **Unary calls**: wrapped by generated error handlers that map
63
- `grpc.RpcError` to the exceptions above and (for the multi-endpoint
64
- clients) trigger retry with backoff.
63
+ `grpc.RpcError` to the exceptions above.
65
64
  - **Generators** (watch iterators, range streams): errors are mapped as
66
65
  they surface during iteration; generator handlers may end the stream
67
66
  quietly when the watch is cancelled normally.
@@ -71,11 +70,15 @@ current endpoint as failed, which drives [Failover](failover.md) and
71
70
 
72
71
  ## Retry Guidance
73
72
 
74
- The client already retries transient failures when `failover=True`
75
- (`max_retries`, `retry_backoff`, `retry_max_backoff`). Retry at the
76
- application level only for operations that are safe to repeat (idempotent
77
- puts, reads) and treat `RevisionCompactedError` as a signal to resync
78
- state rather than to retry blindly.
73
+ Read operations are retried automatically on transient failures
74
+ (`UNAVAILABLE`, `DEADLINE_EXCEEDED`, `INTERNAL`), bounded by
75
+ `max_retries`, `retry_backoff` and `retry_max_backoff`.
76
+
77
+ Writes are **not** retried automatically: the request may already have been
78
+ applied server-side when the error surfaced, so repeating it could duplicate
79
+ the effect. Retry writes at the application level only where they are safe
80
+ to repeat (idempotent puts), and treat `RevisionCompactedError` as a signal
81
+ to resync state rather than to retry blindly.
79
82
 
80
83
  ## See Also
81
84
 
@@ -44,10 +44,14 @@ see [Configuration](../guide/configuration.md).
44
44
  the endpoint is marked failed
45
45
  3. On the next operation the client picks a healthy endpoint (per the load
46
46
  balancer strategy) and rebuilds the failed endpoint's channel in the
47
- background
47
+ background. With `failover=False` the client instead stays on the
48
+ endpoint it holds and rebuilds that endpoint's channel in place.
48
49
  4. Periodic health checks (`health_check_interval`) recover endpoints once
49
50
  the server answers again
50
51
 
52
+ Both clients behave identically here: the sync and the async multi-endpoint
53
+ client apply the same strategy and the same failover rules.
54
+
51
55
  ## Load Balancer Strategies
52
56
 
53
57
  | Strategy | Description |