draining_hosts: List[orchestrator.HostSpec],
daemons: List[orchestrator.DaemonDescription],
related_service_daemons: Optional[List[DaemonDescription]] = None,
+ related_service_required_count: Optional[int] = None,
networks: Dict[str, Dict[str, Dict[str, List[str]]]] = {},
filter_new_host: Optional[Callable[[str, ServiceSpec], bool]] = None,
allow_colo: bool = False,
self.service_name = spec.service_name()
self.daemons = daemons
self.related_service_daemons = related_service_daemons
+ self.related_service_required_count = related_service_required_count
self.networks = networks
self.allow_colo = allow_colo
self.per_host_daemon_type = per_host_daemon_type
# If we still need to remove more, remove from hosts with related services
remaining_needed = total_excess - len(to_delete)
if remaining_needed > 0:
- remaining = [dd for dd in existing if dd not in to_delete]
- to_delete.extend(remaining[count:])
+ # Restrict defer-removal behavior to ingress only.
+ should_defer_related_removal = False
+ if self.spec.service_type == 'ingress':
+ related_service_count = len(self.related_service_daemons)
+ related_service_required_count = (
+ self.related_service_required_count or count
+ )
+ related_service_is_ranked = any(
+ dd.rank is not None
+ for dd in self.related_service_daemons
+ )
+ # For ingress with ranked backends (e.g. NFS), if backend
+ # scale-down is still pending, defer deleting co-located
+ # ingress daemons until a later iteration.
+ should_defer_related_removal = (
+ related_service_is_ranked
+ and related_service_count > related_service_required_count
+ )
+ if not should_defer_related_removal:
+ remaining = [dd for dd in existing if dd not in to_delete]
+ to_delete.extend(remaining[count:])
non_matching_daemons = to_delete
else:
rank_map = None
if svc.ranked(spec):
rank_map = self.mgr.spec_store[spec.service_name()].rank_map or {}
+ related_service_required_count = None
+ if service_type == 'ingress':
+ ingress_spec = cast(IngressSpec, spec)
+ assert ingress_spec.backend_service
+ backend_spec = self.mgr.spec_store.active_specs.get(
+ ingress_spec.backend_service
+ )
+ if backend_spec and backend_spec.placement:
+ related_service_required_count = backend_spec.placement.get_target_count(
+ self.mgr.cache.get_schedulable_hosts()
+ )
host_selector = _host_selector(svc)
ha = HostAssignment(
spec=spec,
blocking_daemon_hosts=blocking_daemon_hosts,
daemons=daemons,
related_service_daemons=related_service_daemons,
+ related_service_required_count=related_service_required_count,
networks=self.mgr.cache.networks,
filter_new_host=host_filters.get(service_type, None),
allow_colo=svc.allow_colo(),
assert sorted(got_add) == sorted(expected_add)
assert sorted([d.name() for d in to_remove]) == sorted(expected_remove)
+
+
+def test_ingress_scale_down_defers_related_when_ranked_backend_exceeds_required():
+ """
+ With ingress count below current daemons, ranked NFS backends still above
+ placement target (related_service_required_count), remove only ingress on
+ hosts without NFS first; defer removing co-located ingress until NFS scales
+ down (next iteration).
+ """
+ spec = IngressSpec(
+ service_type='ingress',
+ service_id='nfs.test',
+ frontend_port=443,
+ monitor_port=8888,
+ virtual_ip='10.0.0.20/8',
+ backend_service='nfs.test',
+ placement=PlacementSpec(count=1),
+ )
+ hosts = [HostSpec(h) for h in 'host1 host2 host3'.split()]
+ daemons = [
+ DaemonDescription('haproxy', 'ingress1', 'host1'),
+ DaemonDescription('keepalived', 'ingress1', 'host1'),
+ DaemonDescription('haproxy', 'ingress2', 'host2'),
+ DaemonDescription('keepalived', 'ingress2', 'host2'),
+ DaemonDescription('haproxy', 'ingress3', 'host3'),
+ DaemonDescription('keepalived', 'ingress3', 'host3'),
+ ]
+ related_service_daemons = [
+ DaemonDescription('nfs', 'nfs1', 'host1', rank=0, rank_generation=0),
+ DaemonDescription('nfs', 'nfs2', 'host2', rank=1, rank_generation=0),
+ ]
+ all_slots, to_add, to_remove = HostAssignment(
+ spec=spec,
+ hosts=hosts,
+ unreachable_hosts=[],
+ draining_hosts=[],
+ daemons=daemons,
+ related_service_daemons=related_service_daemons,
+ related_service_required_count=1,
+ primary_daemon_type='haproxy',
+ per_host_daemon_type='keepalived',
+ rank_map=None,
+ ).place()
+
+ assert sorted(str(p) for p in all_slots) == sorted([
+ 'haproxy:host1(*:443,8888,1024)',
+ 'haproxy:host2(*:443,8888,1024)',
+ 'keepalived:host1',
+ 'keepalived:host2',
+ ])
+ assert to_add == []
+ assert sorted(d.name() for d in to_remove) == sorted([
+ 'haproxy.ingress3',
+ 'keepalived.ingress3',
+ ])