feat: add Consul-based global ID generator with fallback mechanism
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import consul_kv
|
||||
import socket
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
|
||||
# --- Configuration ---
|
||||
CONSUL_HOST = os.environ.get('CONSUL_HOST', 'localhost')
|
||||
CONSUL_PORT = int(os.environ.get('CONSUL_PORT', 8500))
|
||||
CONSUL_COUNTER_KEY = 'service/ids/counter'
|
||||
MAX_RETRIES = 5 # Max attempts for atomic update
|
||||
RETRY_DELAY_SECONDS = 0.1 # Delay between retries for atomic update
|
||||
|
||||
# --- Logging Setup ---
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- Fallback ID Generation ---
|
||||
def generate_fallback_id() -> int:
|
||||
"""
|
||||
Generates a fallback ID based on hostname and PID if Consul is unavailable.
|
||||
This provides a unique-ish ID for local testing/development but is not guaranteed
|
||||
to be globally unique across different machines or after restarts.
|
||||
"""
|
||||
hostname = socket.gethostname()
|
||||
pid = os.getpid()
|
||||
# A simple way to combine them into a number.
|
||||
# Hash hostname to an integer and combine with PID.
|
||||
# Note: This is a simple fallback. For production, consider a more robust
|
||||
# local ID generation or a different strategy if Consul is critical.
|
||||
fallback_id = (hash(hostname) % 1000000) * 100000 + pid
|
||||
logger.warning(
|
||||
f"Consul is unavailable. Generating fallback ID: {fallback_id} (based on hostname: {hostname}, PID: {pid})")
|
||||
return fallback_id
|
||||
|
||||
|
||||
# --- Unique Instance ID Generation Function ---
|
||||
def get_unique_instance_id() -> int:
|
||||
"""
|
||||
Reads and atomically increments a counter in Consul KV to get a unique instance ID.
|
||||
If Consul is unavailable or the atomic update fails after retries,
|
||||
it falls back to generating an ID based on hostname and PID.
|
||||
"""
|
||||
try:
|
||||
# Initialize Consul client
|
||||
client = consul_kv.Connection(endpoint=f"{CONSUL_HOST}:{CONSUL_PORT}")
|
||||
logger.info(f"Attempting to connect to Consul at {CONSUL_HOST}:{CONSUL_PORT}")
|
||||
|
||||
for attempt in range(MAX_RETRIES):
|
||||
try:
|
||||
# 1. Read the current value and its modifyIndex (for CAS)
|
||||
# client.get returns (value, modifyIndex)
|
||||
current_value_str, modify_index = client.get(CONSUL_COUNTER_KEY)
|
||||
|
||||
# If key doesn't exist, initialize it to 0
|
||||
if current_value_str is None:
|
||||
current_value = 0
|
||||
modify_index = 0 # For a non-existent key, modify_index is 0 for initial CAS
|
||||
logger.info(f"Consul key '{CONSUL_COUNTER_KEY}' not found. Initializing to 0.")
|
||||
else:
|
||||
current_value = int(current_value_str)
|
||||
|
||||
new_value = current_value + 1
|
||||
|
||||
# 2. Attempt atomic update using CAS (Check-And-Set)
|
||||
# client.set returns True on success, False on failure (if modify_index changed)
|
||||
success = client.put(CONSUL_COUNTER_KEY, str(new_value), cas=modify_index)
|
||||
|
||||
if success:
|
||||
logger.info(f"Successfully obtained unique ID: {new_value} (Attempt {attempt + 1}/{MAX_RETRIES})")
|
||||
return new_value
|
||||
else:
|
||||
logger.warning(
|
||||
f"CAS failed for '{CONSUL_COUNTER_KEY}' (modify_index changed). Retrying... (Attempt {attempt + 1}/{MAX_RETRIES})")
|
||||
time.sleep(RETRY_DELAY_SECONDS * (attempt + 1)) # Exponential backoff for retries
|
||||
except ValueError:
|
||||
# Handle case where the stored value is not an integer
|
||||
logger.error(
|
||||
f"Value stored at '{CONSUL_COUNTER_KEY}' is not a valid integer. Please check Consul KV. Attempting to overwrite.")
|
||||
# To recover, you might force a set without CAS, but that risks data loss.
|
||||
# For this demo, we'll just let it retry or fall back.
|
||||
time.sleep(RETRY_DELAY_SECONDS * (attempt + 1))
|
||||
except Exception as e:
|
||||
# Catch specific ConsulKV errors (e.g., connection issues, invalid response)
|
||||
logger.error(f"ConsulKV error during atomic update (Attempt {attempt + 1}/{MAX_RETRIES}): {e}")
|
||||
time.sleep(RETRY_DELAY_SECONDS * (attempt + 1)) # Exponential backoff
|
||||
|
||||
logger.error(f"Failed to obtain unique ID from Consul after {MAX_RETRIES} attempts.")
|
||||
return generate_fallback_id() # Fallback if all retries fail
|
||||
|
||||
except Exception as e:
|
||||
# Catch broader connection errors or unexpected issues with Consul client initialization
|
||||
logger.error(f"Could not connect to Consul or an unexpected error occurred: {e}")
|
||||
return generate_fallback_id() # Fallback if Consul is completely unreachable
|
||||
|
||||
Reference in New Issue
Block a user