Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6fe376d
feat(node-removal): online node removal as inverse of cluster expansion
schmidt-scaled Jun 15, 2026
ee05e96
Merge branch 'main' into feature/node-removal
Hamdy-khader Jun 22, 2026
084fb3c
update: point SIMPLY_BLOCK_DOCKER_IMAGE to feature-node-removal
Hamdy-khader Jun 22, 2026
2181fad
fix: update get_secondary_nodes to handle removed_node parameter
Hamdy-khader Jun 23, 2026
4e6ff2b
remove: comment out phase 2 "in_removal" logic in storage node operat…
Hamdy-khader Jun 24, 2026
d75b049
update: exclude node removal tasks in job task listing
Hamdy-khader Jun 24, 2026
ef5170e
update: replace device removal logic with state update in storage nod…
Hamdy-khader Jun 24, 2026
f264c3f
update: reorder and adjust device removal and finalization phases in …
Hamdy-khader Jun 24, 2026
ea86f13
update: add support for overridden device names during replacement an…
Hamdy-khader Jun 25, 2026
515b7fa
remove: drop deprecated "in_removal" status from StorageNode and rela…
Hamdy-khader Jun 30, 2026
e5ab5db
Merge branch 'main' into feature/node-removal
Hamdy-khader Jun 30, 2026
94289eb
update: reimplement `sn suspend` and `sn resume` commands to modify l…
Hamdy-khader Jun 30, 2026
42d51bc
Merge branch 'main' into feature/node-removal
Hamdy-khader Jun 30, 2026
9d102ef
fix: correct indentation for `removed_node` condition in storage node…
Hamdy-khader Jun 30, 2026
2902dc6
update: ensure `suspend` and `resume` operations toggle auto-restart …
Hamdy-khader Jun 30, 2026
2b1f5e7
update: streamline hublvol creation by removing redundant secondary c…
Hamdy-khader Jun 30, 2026
8f5cc1e
fix: handle overridden device names during JM reassignment and remote…
Hamdy-khader Jun 30, 2026
2da6fbd
Merge branch 'main' into feature/node-removal
Hamdy-khader Jun 30, 2026
60f105e
fix linter
Hamdy-khader Jun 30, 2026
a652d44
fix unit tests
Hamdy-khader Jun 30, 2026
fa97530
fix unit tests 2
Hamdy-khader Jun 30, 2026
92dfed1
Merge branch 'main' into feature/node-removal
geoffrey1330 Jul 2, 2026
f845f43
Merge branch 'main' into feature/node-removal
geoffrey1330 Jul 2, 2026
fae68f0
fix: prevent cluster activation from wedging in in_activation
wmousa Jul 4, 2026
dbec7f5
fix: make storage node restarts and cluster activation crash-safe
wmousa Jul 6, 2026
9a6c108
Add migration tasks before node status is set to Online
Hamdy-khader Jul 6, 2026
09ab387
Merge branch 'main' into feature/node-removal
Hamdy-khader Jul 9, 2026
9344d39
Fix type checker error in storage_node_ops.py
Hamdy-khader Jul 9, 2026
4fbc8b4
Allow migration tasks to run while a node is suspsnded
Hamdy-khader Jul 9, 2026
14036d9
fix(node-status): allow tasks to run on suspended nodes
Hamdy-khader Jul 10, 2026
4aaa7a4
Merge branch 'main-fix-stuck-activation' into feature/node-removal
Hamdy-khader Jul 10, 2026
87dd225
fix(storage-node): include suspended status in health check logic
Hamdy-khader Jul 10, 2026
ce282b8
fix(health-check): include suspended status and correct function name…
Hamdy-khader Jul 10, 2026
8521684
fix(health-check): include suspended status and correct function name…
Hamdy-khader Jul 10, 2026
61afcdb
fix(storage-node): extend allowed statuses to include suspended
Hamdy-khader Jul 10, 2026
fedadb5
Merge branch 'main' into feature/node-removal
Hamdy-khader Jul 10, 2026
f13dbfc
Merge branch 'main' into feature/node-removal
geoffrey1330 Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions simplyblock_cli/cli-reference.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -297,22 +297,19 @@ commands:
type: bool
action: store_true
- name: remove
help: "Removes a storage node from the cluster."
description: >
The storage node cannot be used or added any more. Any data residing on this storage node will be migrated to
the remaining storage nodes. The user must ensure that there is sufficient free space in remaining cluster to
allow for successful node removal.

!!! danger
If there isn't enough storage available, the cluster may run full and switch to read-only mode.
help: 'Removes a storage node from the cluster (inverse of cluster expansion).
The node must be ONLINE and all other nodes ONLINE; it must host no logical
volumes or snapshots (migrate those first). Runs as a background task: shuts
the node down, rewires LVS replicas onto other nodes, then removes, fails and
migrates its devices before marking the node removed.'
arguments:
- name: node_id
help: Storage node id
dest: node_id
completer: _completer_get_sn_list
type: str
- name: "--force-remove"
help: "Force remove all logical volumes and snapshots."
help: "Cancel any active tasks on the node before starting removal."
dest: force_remove
type: bool
action: store_true
Expand Down Expand Up @@ -449,21 +446,16 @@ commands:
type: bool
action: store_true
- name: suspend
help: "DEPRECATED: the suspension phase was removed from graceful shutdown (it caused writer conflicts on sec/tert lvstores). This command is now a no-op returning success. Use `sn shutdown`."
usage: The node will stop accepting new IO, but will finish processing any IO, which has been received already.
help: "Exclude node from lvol allocation."
usage: The node will be excluded from lvol allocation. It will not be able to create new logical volumes on this node.
arguments:
- name: node_id
help: Storage node id
dest: node_id
completer: _completer_get_sn_list
type: str
- name: "--force"
help: "Ignored (kept for backwards compatibility)."
dest: force
type: bool
action: store_true
- name: resume
help: "DEPRECATED: counterpart to `sn suspend`, also a no-op."
help: "Include node in lvol allocation."
arguments:
- name: node_id
help: Storage node id
Expand Down
9 changes: 4 additions & 5 deletions simplyblock_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,9 @@ def init_storage_node__delete(self, subparser):
subcommand.add_argument('--force', help='Force delete storage node from DB. Ensure you know what you\'re doing.', dest='force_remove', action='store_true')

def init_storage_node__remove(self, subparser):
subcommand = self.add_sub_command(subparser, 'remove', 'Removes a storage node from the cluster.')
subcommand = self.add_sub_command(subparser, 'remove', 'Removes a storage node from the cluster (inverse of cluster expansion). The node must be ONLINE and all other nodes ONLINE; it must host no logical volumes or snapshots (migrate those first). Runs as a background task: shuts the node down, rewires LVS replicas onto other nodes, then removes, fails and migrates its devices before marking the node removed.')
subcommand.add_argument('node_id', help='Storage node id', type=str).completer = self._completer_get_sn_list
subcommand.add_argument('--force-remove', help='Force remove all logical volumes and snapshots.', dest='force_remove', action='store_true')
subcommand.add_argument('--force-remove', help='Cancel any active tasks on the node before starting removal.', dest='force_remove', action='store_true')

def init_storage_node__list(self, subparser):
subcommand = self.add_sub_command(subparser, 'list', 'Lists all storage nodes.')
Expand Down Expand Up @@ -209,12 +209,11 @@ def init_storage_node__shutdown(self, subparser):
subcommand.add_argument('--force', help='Force node shutdown.', dest='force', action='store_true')

def init_storage_node__suspend(self, subparser):
subcommand = self.add_sub_command(subparser, 'suspend', 'DEPRECATED: the suspension phase was removed from graceful shutdown (it caused writer conflicts on sec/tert lvstores). This command is now a no-op returning success. Use `sn shutdown`.')
subcommand = self.add_sub_command(subparser, 'suspend', 'Exclude node from lvol allocation.')
subcommand.add_argument('node_id', help='Storage node id', type=str).completer = self._completer_get_sn_list
subcommand.add_argument('--force', help='Ignored (kept for backwards compatibility).', dest='force', action='store_true')

def init_storage_node__resume(self, subparser):
subcommand = self.add_sub_command(subparser, 'resume', 'DEPRECATED: counterpart to `sn suspend`, also a no-op.')
subcommand = self.add_sub_command(subparser, 'resume', 'Include node in lvol allocation.')
subcommand.add_argument('node_id', help='Storage node id', type=str).completer = self._completer_get_sn_list

def init_storage_node__get_io_stats(self, subparser):
Expand Down
2 changes: 1 addition & 1 deletion simplyblock_cli/clibase.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def storage_node__shutdown(self, sub_command, args):
return ret

def storage_node__suspend(self, sub_command, args):
ret = storage_ops.suspend_storage_node(args.node_id, args.force)
ret = storage_ops.suspend_storage_node(args.node_id)
if isinstance(ret, tuple):
ok, reason = ret
if not ok:
Expand Down
21 changes: 21 additions & 0 deletions simplyblock_core/cluster_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,27 @@ def _wait_for_full_device_connectivity(cl_id, timeout_sec=300, poll_sec=10):

def cluster_activate(cl_id, force=False, force_lvstore_create=False) -> None:
cluster = db_controller.get_cluster_by_id(cl_id)
prev_status = cluster.status
if prev_status == Cluster.STATUS_IN_ACTIVATION:
prev_status = Cluster.STATUS_UNREADY
try:
_cluster_activate(cl_id, force=force, force_lvstore_create=force_lvstore_create)
except Exception:
# Never leave the cluster wedged in in_activation: this often runs in
# a fire-and-forget thread, and an unhandled failure would otherwise
# block any retry (the activate API rejects in_activation clusters).
# The expected-failure paths inside _cluster_activate restore the
# status themselves; this only catches what they missed.
cluster = db_controller.get_cluster_by_id(cl_id)
if cluster.status == Cluster.STATUS_IN_ACTIVATION:
logger.error("Cluster activation failed unexpectedly; reverting status "
f"from {Cluster.STATUS_IN_ACTIVATION} to {prev_status}")
set_cluster_status(cl_id, prev_status)
raise


def _cluster_activate(cl_id, force=False, force_lvstore_create=False) -> None:
cluster = db_controller.get_cluster_by_id(cl_id)

if cluster.status == Cluster.STATUS_ACTIVE:
logger.warning("Cluster is ACTIVE")
Expand Down
19 changes: 13 additions & 6 deletions simplyblock_core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,19 @@ def get_config_var(name, default=None):

# A JobSchedule's lease is held by the runner host (by hostname) that last
# touched it. Another host may only take over a task whose lease is older than
# this — i.e. the owning runner is presumed dead. Must exceed the longest
# single task_runner() invocation that does not write the task back (the
# restart runner can block on RPCs for several minutes), so a live owner is
# never falsely preempted. A runner restarting on the SAME host re-claims its
# own tasks immediately regardless of this value (owner id is the hostname).
TASK_LEASE_TTL_SEC = 1200
# this — i.e. the owning runner is presumed dead. A live owner refreshes the
# lease every TASK_LEASE_HEARTBEAT_SEC from a background thread while driving
# a restart (restart_storage_node wrapper), so the TTL no longer needs to
# exceed the longest blocking RPC — it only needs to be several heartbeats
# wide so a momentarily slow (but alive) owner is never falsely preempted.
# Keeping it short is what makes ownership transfer fast: when the driving
# process dies (pod evicted while its host drains, CLI killed), a live
# tasks-runner claims the stale lease and resumes the restart instead of the
# node staying orphaned in RESTARTING (2026-07-04 MCO rollout deadlock).
# A runner restarting on the SAME host re-claims its own tasks immediately
# regardless of this value (owner id is the hostname).
TASK_LEASE_HEARTBEAT_SEC = 30
TASK_LEASE_TTL_SEC = 180

# Node-add concurrency: the cross-node mesh section of add_node is serialized
# per cluster behind a ClusterAddNodeLock. The holder refreshes the lock every
Expand Down
4 changes: 2 additions & 2 deletions simplyblock_core/controllers/health_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ def _check_node_lvstore(
dev = db_controller.get_storage_device_by_id(result['UUID'])
dev_node = db_controller.get_storage_node_by_id(dev.node_id)
if dev.status == NVMeDevice.STATUS_ONLINE and dev_node.status in [
StorageNode.STATUS_ONLINE, StorageNode.STATUS_DOWN, StorageNode.STATUS_UNREACHABLE]:
StorageNode.STATUS_ONLINE, StorageNode.STATUS_DOWN, StorageNode.STATUS_UNREACHABLE, StorageNode.STATUS_SUSPENDED]:
try:
remote_bdev = storage_node_ops.connect_device(
f"remote_{dev.alceml_bdev}", dev, node)
Expand Down Expand Up @@ -713,7 +713,7 @@ def check_node(node_id, with_devices=True):
logger.info(f"Node remote JMs: {len(snode.remote_jm_devices)}")
for remote_device in snode.remote_jm_devices:

name = f'remote_{remote_device.jm_bdev}n1'
name = remote_device.remote_bdev
bdev_info = rpc_client.get_bdevs(name)
logger.log(INFO if bdev_info else ERROR,
f"Checking bdev: {name} ... " + ('ok' if bdev_info else 'failed'))
Expand Down
71 changes: 70 additions & 1 deletion simplyblock_core/controllers/tasks_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,48 @@ def _mutate(t):
return decision["won"]


def refresh_task_lease(task, owner=None):
"""Heartbeat: refresh this host's lease on a task it already owns, so a
live owner is never preempted while blocking on long RPCs. Returns False
(without touching the task) if the task is done or owned by another host —
the caller lost the lease and should treat the takeover as authoritative."""
owner = owner or _RUNNER_HOST
now = str(datetime.datetime.now(datetime.timezone.utc))
refreshed = {"ok": False}

def _mutate(t):
if t.status == JobSchedule.STATUS_DONE:
return False
if t.owner != owner:
return False
t.updated_at = now
refreshed["ok"] = True
return True

if db.atomic_update(task, _mutate) is None:
return False
return refreshed["ok"]


def ensure_node_restart_task(node):
"""Return the id of an unfinished FN_NODE_RESTART task for this node,
creating one if none exists. Used by explicit restarts
(restart_storage_node wrapper) to make their ownership transferable: the
driver claims and heartbeats the task's lease, and if the driving process
dies mid-restart, a live tasks-runner claims the stale lease and resumes.

Deliberately bypasses the auto_restart_disabled guard of
add_node_to_auto_restart: that flag means "no UNATTENDED restart after a
deliberate shutdown" — an explicit restart is precisely the operator
intervention the flag waits for, and this task only continues that
expressed intent. On success the ONLINE transition cancels the task
(cancel_pending_node_restart_tasks via set_node_status)."""
existing = _validate_new_task_node_restart(node.cluster_id, node.get_id())
if existing:
return existing
return _add_task(JobSchedule.FN_NODE_RESTART, node.cluster_id, node.get_id(), "", max_retry=11)


def _validate_new_task_dev_restart(cluster_id, node_id, device_id):
tasks = db.get_job_tasks(cluster_id)
for task in tasks:
Expand Down Expand Up @@ -174,6 +216,12 @@ def _add_task(function_name, cluster_id, node_id, device_id,
logger.info(f"Task found, skip adding new task: {task_id}")
return False

elif function_name == JobSchedule.FN_NODE_REMOVAL:
task_id = get_active_node_removal_task(cluster_id, node_id)
if task_id:
logger.info(f"Task found, skip adding new task: {task_id}")
return False

task_obj = JobSchedule()
task_obj.uuid = str(uuid.uuid4())
task_obj.cluster_id = cluster_id
Expand Down Expand Up @@ -551,6 +599,10 @@ def get_active_node_mig_task(cluster_id, node_id, distr_name=None):
def add_device_failed_mig_task(device_id):
device = db.get_storage_device_by_id(device_id)
for node in db.get_storage_nodes_by_cluster_id(device.cluster_id):
# IN_REMOVAL nodes have a dead SPDK (shut down by the removal flow);
# a migration task targeting their distribs can never run and would
# stall the node-removal completion check forever. Skip them like
# already-REMOVED nodes.
if node.status == StorageNode.STATUS_REMOVED:
continue
for bdev in node.lvstore_stack:
Expand All @@ -577,6 +629,23 @@ def add_node_add_task(cluster_id, function_params):
function_params=function_params, max_retry=11)


def add_node_removal_task(cluster_id, node_id, function_params=None):
# max_retry=-1: the removal runner drives a multi-step, possibly multi-hour
# orchestration (shutdown -> LVS rewire -> device fail+migrate). Migration
# waits legitimately suspend-and-retry many times; do not cap retries.
return _add_task(JobSchedule.FN_NODE_REMOVAL, cluster_id, node_id, "",
function_params=function_params or {}, max_retry=-1)


def get_active_node_removal_task(cluster_id, node_id):
tasks = db.get_job_tasks(cluster_id)
for task in tasks:
if task.function_name == JobSchedule.FN_NODE_REMOVAL and task.node_id == node_id:
if task.status != JobSchedule.STATUS_DONE and task.canceled is False:
return task.uuid
return False


def add_cluster_expand_task(cluster_id, new_node_id):
"""Queue a single-node cluster-expansion task. The runner drives the
planner/orchestrator/executor to integrate ``new_node_id`` into the
Expand Down Expand Up @@ -627,7 +696,7 @@ def get_active_node_tasks(cluster_id, node_id):
tasks = db.get_job_tasks(cluster_id)
out = []
for task in tasks:
if task.function_name in [JobSchedule.FN_PORT_ALLOW, JobSchedule.FN_JC_COMP_RESUME]:
if task.function_name in [JobSchedule.FN_PORT_ALLOW, JobSchedule.FN_JC_COMP_RESUME, JobSchedule.FN_NODE_REMOVAL]:
continue
if task.node_id == node_id:
if task.status != JobSchedule.STATUS_DONE and task.canceled is False:
Expand Down
2 changes: 1 addition & 1 deletion simplyblock_core/env_var
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SIMPLY_BLOCK_COMMAND_NAME=sbcli-dev
SIMPLY_BLOCK_VERSION=19.2.34

SIMPLY_BLOCK_DOCKER_IMAGE=public.ecr.aws/simply-block/simplyblock:main
SIMPLY_BLOCK_DOCKER_IMAGE=public.ecr.aws/simply-block/simplyblock:feature-node-removal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must not be merged to main.

SIMPLY_BLOCK_SPDK_ULTRA_IMAGE=public.ecr.aws/simply-block/ultra:main-latest
1 change: 1 addition & 0 deletions simplyblock_core/models/job_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class JobSchedule(BaseModel):
FN_FAILED_DEV_MIG = "failed_device_migration"
FN_NEW_DEV_MIG = "new_device_migration"
FN_NODE_ADD = "node_add"
FN_NODE_REMOVAL = "node_removal"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rename this to FN_NODE_REMOVE = "node_remove" for consistency.

FN_PORT_ALLOW = "port_allow"
FN_BALANCING_AFTER_NODE_RESTART = "balancing_on_restart"
FN_BALANCING_AFTER_DEV_REMOVE = "balancing_on_dev_rem"
Expand Down
3 changes: 3 additions & 0 deletions simplyblock_core/models/nvme_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ class JMDevice(NVMeDevice):
# the per-leg member partitions. Empty for single-device (no-raid) JMs.
jm_leg_bdevs: List[str] = []
jm_leg_members: List = []
# When attaching this JM to a node, override the device name on that node.
# This is needed when a JM device is removed and needed to be replaced, but the name must be the same.
override_name_on_node: dict[str, str] = {} # node_id: new_name
Comment on lines +111 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd provide a single helper JMDevice.controller_name(node_id: UUID) -> str that is used to centrally determine the controller name and is called from storage_node_ops. The derivation logic is duplicated and prone to be missed in future changes.



class RemoteDevice(BaseModel):
Expand Down
14 changes: 14 additions & 0 deletions simplyblock_core/scripts/docker-compose-swarm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,20 @@ services:
environment:
SIMPLYBLOCK_LOG_LEVEL: "$LOG_LEVEL"

TasksNodeRemovalRunner:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rename this to TasksNodeRemoveRunner for consistency.

<<: *service-base
image: $SIMPLYBLOCK_DOCKER_IMAGE
command: "python3 simplyblock_core/services/tasks_runner_node_removal.py"
deploy:
placement:
constraints: [node.role == manager]
volumes:
- "/etc/foundationdb:/etc/foundationdb"
networks:
- hostnet
environment:
SIMPLYBLOCK_LOG_LEVEL: "$LOG_LEVEL"

TasksRunnerClusterExpand:
<<: *service-base
image: $SIMPLYBLOCK_DOCKER_IMAGE
Expand Down
2 changes: 1 addition & 1 deletion simplyblock_core/services/health_check_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def check_node(snode):
# (reconnecting remote devices, repairing multipath, recreating hublvols) —
# but their node/device health is reported as "not applicable" (None),
# never true/false.
report_health = snode.status in [StorageNode.STATUS_ONLINE, StorageNode.STATUS_DOWN]
report_health = snode.status in [StorageNode.STATUS_ONLINE, StorageNode.STATUS_DOWN, StorageNode.STATUS_SUSPENDED]

# 1- check node ping
ping_check = health_controller._check_node_ping(snode.mgmt_ip)
Expand Down
1 change: 1 addition & 0 deletions simplyblock_core/services/storage_node_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ def _spawn_recovery_shutdown(node):

def _run():
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is superfluous

storage_node_ops.shutdown_storage_node(
node_id, force=True, keep_auto_restart=True)
except Exception as e:
Expand Down
6 changes: 3 additions & 3 deletions simplyblock_core/services/tasks_runner_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def _cluster_unavailable_state(cluster_id):
for node in db.get_storage_nodes_by_cluster_id(cluster_id):
if node.status in [StorageNode.STATUS_IN_CREATION, StorageNode.STATUS_REMOVED]:
continue
if node.status != StorageNode.STATUS_ONLINE:
if node.status not in [StorageNode.STATUS_ONLINE, StorageNode.STATUS_SUSPENDED]:
unavailable.append(f"node:{node.get_id()}")
for dev in node.nvme_devices:
if dev.status in [NVMeDevice.STATUS_REMOVED, NVMeDevice.STATUS_FAILED_AND_MIGRATED]:
Expand Down Expand Up @@ -75,7 +75,7 @@ def task_runner(task):
task.write_to_db(db.kv_store)
return True

if snode.status != StorageNode.STATUS_ONLINE:
if snode.status not in [StorageNode.STATUS_ONLINE, StorageNode.STATUS_SUSPENDED]:
task.function_result = "node is not online, retrying"
task.status = JobSchedule.STATUS_SUSPENDED
unavailable = _cluster_unavailable_state(task.cluster_id)
Expand Down Expand Up @@ -307,7 +307,7 @@ def _set_master_task_status(master_task, status):
logger.info("no task found on same node, resuming compression")
node = db.get_storage_node_by_id(task.node_id)
for n in db.get_storage_nodes_by_cluster_id(node.cluster_id):
if n.status != StorageNode.STATUS_ONLINE:
if n.status not in [StorageNode.STATUS_ONLINE, StorageNode.STATUS_SUSPENDED]:
logger.warning("Not all nodes are online, can not resume JC compression")
continue
rpc_client = node.rpc_client(timeout=5, retry=2)
Expand Down
Loading
Loading