-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DPE-4575][DPE-4886][DPE-4983] Add voting exclusions management #367
Changes from all commits
52fcb0d
8802410
d360fe9
05c828b
186dadd
784dd0f
b4b9fde
b9becc8
f22f75b
ce0c3e6
3e59a79
b3a9c11
1bc6555
ee49e33
5a9e006
e633312
fc148a5
68ca4f0
8fa6f29
48c511e
3b16370
4004ba5
6c2137c
c45e7e9
4591f2b
6ff1ad9
9e0e20d
c3b71e7
3ebadee
f0e0fec
bbca393
adeef52
3d74325
e14ae76
b909c16
af9fa25
7c1595a
3851b2d
631b6a8
8972bf3
ad524e6
77b1c67
d84e1d5
bd97709
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,7 +64,11 @@ | |
from charms.opensearch.v0.opensearch_health import HealthColors, OpenSearchHealth | ||
from charms.opensearch.v0.opensearch_internal_data import RelationDataStore, Scope | ||
from charms.opensearch.v0.opensearch_locking import OpenSearchNodeLock | ||
from charms.opensearch.v0.opensearch_nodes_exclusions import OpenSearchExclusions | ||
from charms.opensearch.v0.opensearch_nodes_exclusions import ( | ||
OpenSearchExclusionError, | ||
OpenSearchExclusionNodeNotRegisteredError, | ||
OpenSearchExclusions, | ||
) | ||
from charms.opensearch.v0.opensearch_peer_clusters import ( | ||
OpenSearchPeerClustersManager, | ||
OpenSearchProvidedRolesException, | ||
|
@@ -436,7 +440,7 @@ def _on_peer_relation_changed(self, event: RelationChangedEvent): | |
if not (unit_data := event.relation.data.get(event.unit)): | ||
return | ||
|
||
self.opensearch_exclusions.cleanup() | ||
self.opensearch_exclusions.allocation_cleanup() | ||
|
||
if self.unit.is_leader() and unit_data.get("bootstrap_contributor"): | ||
contributor_count = self.peers_data.get(Scope.APP, "bootstrap_contributors_count", 0) | ||
|
@@ -517,7 +521,7 @@ def _on_opensearch_data_storage_detaching(self, _: StorageDetachingEvent): # no | |
# release lock | ||
self.node_lock.release() | ||
|
||
def _on_update_status(self, event: UpdateStatusEvent): | ||
def _on_update_status(self, event: UpdateStatusEvent): # noqa: C901 | ||
"""On update status event. | ||
|
||
We want to periodically check for the following: | ||
|
@@ -533,23 +537,39 @@ def _on_update_status(self, event: UpdateStatusEvent): | |
self.status.set(BlockedStatus(" - ".join(missing_sys_reqs))) | ||
return | ||
|
||
# if node already shutdown - leave | ||
if not self.opensearch.is_node_up(): | ||
return | ||
|
||
# if there are exclusions to be removed | ||
if self.unit.is_leader(): | ||
self.opensearch_exclusions.cleanup() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason why the shards allocation exclusion cleanup is postponed until later in the hook? As long as there is connectivity to a host, we should be able to cleanup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the health checks below allow anything pass, unless the cluster is really on a bad state (i.e. UNKNOWN). So, moved the check below these first health checks because it makes more sense. |
||
|
||
if (health := self.health.apply(wait_for_green_first=True)) not in [ | ||
HealthColors.GREEN, | ||
HealthColors.IGNORE, | ||
]: | ||
] or not self.opensearch.is_node_up(): | ||
# Do not return right now! | ||
# We must first check if we need to remove exclusions | ||
event.defer() | ||
|
||
# Unless it is unknown, in this case we can return and wait for the next run | ||
if health == HealthColors.UNKNOWN: | ||
return | ||
|
||
# Execute allocations now, as we know the cluster is minimally healthy. | ||
self.opensearch_exclusions.allocation_cleanup() | ||
# Now, review voting exclusions, as we may have lost a unit due to an outage | ||
if not self.is_any_voting_unit_stopping(): | ||
try: | ||
self.opensearch_exclusions.settle_voting( | ||
unit_is_stopping=False, | ||
retry=False, | ||
) | ||
except (OpenSearchExclusionError, OpenSearchHttpError): | ||
# Register the issue but do not act on it: let the next update status event | ||
# retry the operation. | ||
# We cannot assume or try to enforce the cluster to be in a healthy state | ||
logger.warning("Failed to settle voting exclusions") | ||
|
||
# if node already shutdown - leave | ||
if not self.opensearch.is_node_up(): | ||
return | ||
|
||
for relation in self.model.relations.get(ClientRelationName, []): | ||
self.opensearch_provider.update_endpoints(relation) | ||
|
||
|
@@ -584,6 +604,16 @@ def _on_config_changed(self, event: ConfigChangedEvent): # noqa C901 | |
RelationJoinedEvent(event.handle, PeerRelationName, self.app, self.unit) | ||
) | ||
|
||
# Review voting exclusions as our IP has changed: we may be coming back from a network | ||
# outage case. | ||
# In this case, we should retry if the node is not found in the cluster | ||
if not self.is_any_voting_unit_stopping(): | ||
try: | ||
self.opensearch_exclusions.settle_voting(unit_is_stopping=False) | ||
except (OpenSearchExclusionNodeNotRegisteredError, OpenSearchHttpError): | ||
event.defer() | ||
return | ||
|
||
previous_deployment_desc = self.opensearch_peer_cm.deployment_desc() | ||
if self.unit.is_leader(): | ||
# run peer cluster manager processing | ||
|
@@ -741,6 +771,14 @@ def on_tls_relation_broken(self, _: RelationBrokenEvent): | |
# Otherwise, we block. | ||
self.status.set(BlockedStatus(TLSRelationBrokenError)) | ||
|
||
def is_any_voting_unit_stopping(self) -> bool: | ||
"""Check if any voting unit is stopping.""" | ||
rel = self.model.get_relation(PeerRelationName) | ||
for unit in all_units(self): | ||
if rel.data[unit].get("voting_unit_stopping") == "True": | ||
return True | ||
return False | ||
|
||
def is_every_unit_marked_as_started(self) -> bool: | ||
"""Check if every unit in the cluster is marked as started.""" | ||
rel = self.model.get_relation(PeerRelationName) | ||
|
@@ -914,7 +952,9 @@ def _post_start_init(self, event: _StartOpenSearch): # noqa: C901 | |
self._cleanup_bootstrap_conf_if_applies() | ||
|
||
# Remove the exclusions that could not be removed when no units were online | ||
self.opensearch_exclusions.delete_current() | ||
if not self.is_any_voting_unit_stopping(): | ||
self.opensearch_exclusions.settle_voting(unit_is_stopping=False) | ||
self.opensearch_exclusions.delete_allocations() | ||
|
||
self.node_lock.release() | ||
|
||
|
@@ -1006,34 +1046,47 @@ def _post_start_init(self, event: _StartOpenSearch): # noqa: C901 | |
if self.opensearch_peer_cm.is_provider(): | ||
self.peer_cluster_provider.refresh_relation_data(event, can_defer=False) | ||
|
||
def _stop_opensearch(self, *, restart=False) -> None: | ||
def _stop_opensearch(self, *, restart=False) -> None: # noqa: C901 | ||
"""Stop OpenSearch if possible.""" | ||
self.status.set(WaitingStatus(ServiceIsStopping)) | ||
|
||
if "cluster_manager" in self.opensearch.roles or "voting_only" in self.opensearch.roles: | ||
# Inform peers that this unit is stopping and it has a voting seat. | ||
# This unit must be the only one managing the voting exclusions, as it may have to | ||
# exclude itself from the voting while stopping. | ||
self.peers_data.put(Scope.UNIT, "voting_unit_stopping", True) | ||
|
||
nodes = [] | ||
if self.opensearch.is_node_up(): | ||
try: | ||
nodes = self._get_nodes(True) | ||
nodes = self._get_nodes(self.opensearch.is_node_up()) | ||
# do not add exclusions if it's the last unit to stop | ||
# otherwise cluster manager election will be blocked when starting up again | ||
# and re-using storage | ||
if len(nodes) > 1: | ||
# 1. Add current node to the voting + alloc exclusions | ||
self.opensearch_exclusions.add_current(restart=restart) | ||
if len(nodes) > 1 and not restart: | ||
# 1. Add current node to the voting + alloc exclusions if not restarting | ||
self.opensearch_exclusions.add_allocations() | ||
except OpenSearchHttpError: | ||
logger.debug("Failed to get online nodes, voting and alloc exclusions not added") | ||
logger.debug("Failed to get online nodes, alloc exclusion not added") | ||
|
||
# block until all primary shards are moved away from the unit that is stopping | ||
self.health.wait_for_shards_relocation() | ||
|
||
# 2. stop the service | ||
# We should only run voting settle right before stop. We need to manage voting before | ||
# stopping e.g. in case we are going 1->0 units. | ||
# We MUST run settle_voting, even if other units are stopping as well. | ||
self.opensearch_exclusions.settle_voting(unit_is_stopping=True) | ||
self.opensearch.stop() | ||
|
||
self.peers_data.delete(Scope.UNIT, "voting_unit_stopping") | ||
self.peers_data.delete(Scope.UNIT, "started") | ||
self.status.set(WaitingStatus(ServiceStopped)) | ||
|
||
# 3. Remove the exclusions | ||
if not restart: | ||
try: | ||
self.opensearch_exclusions.delete_current() | ||
self.opensearch_exclusions.delete_allocations() | ||
except Exception: | ||
# It is purposefully broad - as this can fail for HTTP reasons, | ||
# or if the config wasn't set on disk etc. In any way, this operation is on | ||
|
@@ -1175,7 +1228,7 @@ def _remove_data_role_from_dedicated_cm_if_needed( # noqa: C901 | |
self.opensearch_config.remove_temporary_data_role() | ||
|
||
# wait until data moves out completely | ||
self.opensearch_exclusions.add_current() | ||
self.opensearch_exclusions.add_allocations() | ||
|
||
try: | ||
for attempt in Retrying(stop=stop_after_attempt(3), wait=wait_fixed(0.5)): | ||
|
@@ -1188,7 +1241,7 @@ def _remove_data_role_from_dedicated_cm_if_needed( # noqa: C901 | |
raise Exception | ||
return True | ||
except RetryError: | ||
self.opensearch_exclusions.delete_current() | ||
self.opensearch_exclusions.delete_allocations() | ||
event.defer() | ||
return False | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While before this cleanup was done on every
update_status
, now it's only done when the Health isgreen
. Is this on purpose?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is not the case... It is done on every case, except
HealthColors.UNKNOWN
. Indeed, we defer the event if it is not green... I put it down there because I need the API to be responsive before configuring voting exclusions. If it is not responsive, we will get UNKNOWN anyways and retry later anyways.I will add some comments to clarify that.