|
| 1 | +""" |
| 2 | +This module provides functionality for managing nodes in a Typesense cluster configuration. |
| 3 | +
|
| 4 | +It contains the NodeManager class, which is responsible for node selection, health checks, |
| 5 | +and rotation strategies for load balancing and fault tolerance in a Typesense cluster. |
| 6 | +
|
| 7 | +Key features: |
| 8 | +- Round-robin node selection |
| 9 | +- Nearest node prioritization (if configured) |
| 10 | +- Node health tracking and updates |
| 11 | +- Periodic health checks based on a configurable interval |
| 12 | +
|
| 13 | +Classes: |
| 14 | + NodeManager: Manages the nodes in a Typesense cluster configuration. |
| 15 | +
|
| 16 | +Dependencies: |
| 17 | + - typesense.configuration: Provides Configuration and Node classes |
| 18 | + - typesense.logger: Provides logging functionality |
| 19 | +
|
| 20 | +Usage: |
| 21 | + from typesense.configuration import Configuration |
| 22 | + from node_manager import NodeManager |
| 23 | +
|
| 24 | + config = Configuration(...) |
| 25 | + node_manager = NodeManager(config) |
| 26 | + node = node_manager.get_node() |
| 27 | +
|
| 28 | +Note: This module is part of the Typesense Python client library and is |
| 29 | +used internally by other components of the library. |
| 30 | +""" |
| 31 | + |
| 32 | +import copy |
| 33 | +import time |
| 34 | + |
| 35 | +from typesense.configuration import Configuration, Node |
| 36 | +from typesense.logger import logger |
| 37 | + |
| 38 | + |
| 39 | +class NodeManager: |
| 40 | + """ |
| 41 | + Manages the nodes in a Typesense cluster configuration. |
| 42 | +
|
| 43 | + This class handles node selection, health checks, and rotation for load balancing |
| 44 | + and fault tolerance in a Typesense cluster. |
| 45 | +
|
| 46 | + Attributes: |
| 47 | + config (Configuration): The configuration object for the Typesense client. |
| 48 | + nodes (List[Node]): A copy of the nodes from the configuration. |
| 49 | + node_index (int): The index of the current node in the rotation. |
| 50 | + """ |
| 51 | + |
| 52 | + def __init__(self, config: Configuration): |
| 53 | + """ |
| 54 | + Initialize the NodeManager with a given configuration. |
| 55 | +
|
| 56 | + Args: |
| 57 | + config (Configuration): The configuration object for the Typesense client. |
| 58 | + """ |
| 59 | + self.config = config |
| 60 | + self.nodes = copy.deepcopy(config.nodes) |
| 61 | + self.node_index = 0 |
| 62 | + self._initialize_nodes() |
| 63 | + |
| 64 | + def get_node(self) -> Node: |
| 65 | + """ |
| 66 | + Get the next available healthy node. |
| 67 | +
|
| 68 | + This method implements a round-robin selection strategy, prioritizing the nearest node |
| 69 | + if configured, and considering the health status of each node. |
| 70 | +
|
| 71 | + Returns: |
| 72 | + Node: The selected node for the next operation. |
| 73 | + """ |
| 74 | + if self.config.nearest_node: |
| 75 | + if self.config.nearest_node.healthy or self._is_due_for_health_check( |
| 76 | + self.config.nearest_node, |
| 77 | + ): |
| 78 | + return self.config.nearest_node |
| 79 | + |
| 80 | + node_index = 0 |
| 81 | + while node_index < len(self.nodes): |
| 82 | + node_index += 1 |
| 83 | + node = self.nodes[self.node_index] |
| 84 | + self.node_index = (self.node_index + 1) % len(self.nodes) |
| 85 | + if node.healthy or self._is_due_for_health_check(node): |
| 86 | + return node |
| 87 | + |
| 88 | + logger.debug("No healthy nodes were found. Returning the next node.") |
| 89 | + return self.nodes[self.node_index] |
| 90 | + |
| 91 | + def set_node_health(self, node: Node, is_healthy: bool) -> None: |
| 92 | + """ |
| 93 | + Set the health status of a node and update its last access timestamp. |
| 94 | +
|
| 95 | + Args: |
| 96 | + node (Node): The node to update. |
| 97 | + is_healthy (bool): The health status to set for the node. |
| 98 | + """ |
| 99 | + node.healthy = is_healthy |
| 100 | + node.last_access_ts = int(time.time()) |
| 101 | + |
| 102 | + def _is_due_for_health_check(self, node: Node) -> bool: |
| 103 | + """ |
| 104 | + Check if a node is due for a health check based on the configured interval. |
| 105 | +
|
| 106 | + Args: |
| 107 | + node (Node): The node to check. |
| 108 | +
|
| 109 | + Returns: |
| 110 | + bool: True if the node is due for a health check, False otherwise. |
| 111 | + """ |
| 112 | + current_epoch_ts = int(time.time()) |
| 113 | + return bool( |
| 114 | + (current_epoch_ts - node.last_access_ts) |
| 115 | + > self.config.healthcheck_interval_seconds, |
| 116 | + ) |
| 117 | + |
| 118 | + def _initialize_nodes(self) -> None: |
| 119 | + """ |
| 120 | + Initialize all nodes as healthy. |
| 121 | +
|
| 122 | + This method sets the initial health status of all nodes, including the nearest node |
| 123 | + if configured, to healthy. |
| 124 | + """ |
| 125 | + if self.config.nearest_node: |
| 126 | + self.set_node_health(self.config.nearest_node, is_healthy=True) |
| 127 | + for node in self.nodes: |
| 128 | + self.set_node_health(node, is_healthy=True) |
0 commit comments