Fixes + docker

This commit is contained in:
ciomek committed 2026-09-19 20:57:32 +02:00
1 parent 34aa68e987
commit c0992fe461
22 files changed
+179 -300

No files matched your search

+3 -2
View File
@@ -65,10 +65,11 @@ class Network:
@staticmethod
def execute_to_unregistered_node(ip, port, command: Enum, *args, **kwargs):
def execute_to_unregistered_node(ip, port, command: Enum, *args, timeout=None, **kwargs):
request = CommandRequest.form(command, *args, **kwargs)
return Whisperer(ip, port).send(request.collapse())
options = {} if timeout is None else {"timeout": timeout}
return Whisperer(ip, port).send(request.collapse(), **options)
def load_network_from_snapshot(self, file_path: str = "storage/colony_snapshot.json"):
+24 -18
View File
@@ -1,5 +1,6 @@
import copy
import ipaddress
import math
import threading
import time
from dataclasses import dataclass, field, asdict
@@ -19,7 +20,12 @@ def _is_int(value):
def _is_number(value):
return isinstance(value, (int, float)) and not isinstance(value, bool)
if isinstance(value, bool) or not isinstance(value, (int, float)):
return False
try:
return math.isfinite(value)
except OverflowError:
return False
def _text(value, name):
@@ -108,16 +114,15 @@ class Membership:
admission=None, clock=time.monotonic, wall=time.time):
self.clock = clock
self.wall = wall
self.member_timeout = setting("membership.member_timeout") if member_timeout is None else member_timeout
self.forget_after = setting("membership.forget_after") if forget_after is None else forget_after
self.max_members = setting("membership.max_members") if max_members is None else max_members
self._admission = resolve_admission(setting("membership.admission") if admission is None else admission)
self.member_timeout = setting("membership.member_timeout", member_timeout)
self.forget_after = setting("membership.forget_after", forget_after)
self.max_members = setting("membership.max_members", max_members)
self._admission = resolve_admission(setting("membership.admission", admission))
self.denied = False
self._lock = threading.RLock()
self._self = MemberRecord(ip, port, hostname, incarnation=int(wall() * 1000), seq=1)
self._records = {}
self._advanced = {}
self._entries = {}
self._declines = {}
@property
@@ -172,35 +177,36 @@ class Membership:
now = self.clock()
for record in records:
if record.key == self.key:
if record.version > self._self.version:
self._self.incarnation = record.incarnation
self._self.seq = record.seq + 1
continue
current = self._records.get(record.key)
if current is not None and record.version <= current.version:
current = self._entries.get(record.key)
if current is not None and record.version <= current[0].version:
continue
if current is None and len(self._records) >= self.max_members:
if current is None and len(self._entries) >= self.max_members:
continue
self._records[record.key] = record
self._advanced[record.key] = now
self._entries[record.key] = (record, now)
changed += 1
self._purge(now)
return changed
def _purge(self, now):
for key in [key for key, advanced in self._advanced.items() if now - advanced > self.forget_after]:
del self._records[key]
del self._advanced[key]
for key in [key for key, (_, advanced) in self._entries.items() if now - advanced > self.forget_after]:
del self._entries[key]
def alive_records(self):
with self._lock:
now = self.clock()
alive = [self._current_self()]
alive += [record for key, record in self._records.items()
if now - self._advanced[key] <= self.member_timeout]
alive += [record for record, advanced in self._entries.values()
if now - advanced <= self.member_timeout]
return sorted(alive, key=lambda record: record.key)
def known_addresses(self):
with self._lock:
self._purge(self.clock())
return sorted(self._records)
return sorted(self._entries)
def export(self):
return [record.to_dict() for record in self.alive_records()]
+32 -25
View File
@@ -27,6 +27,7 @@ class Gossip(Role):
BOOTSTRAP_REPROBE = 5
DENIED_BACKOFF_CAP = 60
SCORES_TTL = 30
MAX_SEEDS = 64
class COMMANDS(Enum):
HELLO = 'Gossip.Hello'
@@ -41,6 +42,7 @@ class Gossip(Role):
self.reprobe_interval = setting("membership.reprobe_interval")
self.fanout = setting("membership.fanout")
self.hello_timeout = setting("membership.hello_timeout")
self.max_members = setting("membership.max_members")
self.discover_port = setting("discovery.discover_port")
self.probe_timeout = setting("discovery.probe_timeout")
@@ -50,7 +52,6 @@ class Gossip(Role):
self.random = random.Random()
self.next_reprobe = 0.0
self.denied_attempts = 0
self.outcome = None
self._scores = {}
self._scores_at = None
self._persisted_peers = None
@@ -91,17 +92,13 @@ class Gossip(Role):
seeds.append((entry[0], entry[1]))
return seeds
def probe_targets(self):
membership = self.membership
alive = {record.key for record in membership.alive_records()}
remembered = (set(membership.known_addresses()) | set(self.seed_addresses())) - alive
def probe_targets(self, alive):
remembered = (set(self.membership.known_addresses()) | set(self.seed_addresses())) - alive
targets = list(self.scan_targets())
targets = dict.fromkeys(self.scan_targets())
for ip, port in sorted(remembered):
target = self.discover_target(ip, port)
if target not in targets:
targets.append(target)
return targets
targets[self.discover_target(ip, port)] = None
return list(targets)
def _probe(self, target):
host, discover_port = target
@@ -120,7 +117,7 @@ class Gossip(Role):
def find_peers(self):
membership = self.membership
alive = {record.key for record in membership.alive_records()}
found = self.scan(self.probe_targets())
found = self.scan(self.probe_targets(alive))
return [address for address in dict.fromkeys(found) if address not in alive]
def exchange(self, ip, port):
@@ -129,7 +126,9 @@ class Gossip(Role):
known = [record.to_dict() for record in membership.alive_records() if record.key != membership.key]
try:
resp = self.context.execute_to_unregistered_node(ip, port, self.COMMANDS.HELLO, sender, known)
resp = self.context.execute_to_unregistered_node(
ip, port, self.COMMANDS.HELLO, sender, known, timeout=self.hello_timeout
)
except Exception as e:
self.logger.debug(f"Hello to {ip}:{port} failed: {e}")
return FAILED
@@ -153,16 +152,23 @@ class Gossip(Role):
now = self.membership.now()
if self._scores_at is None or now - self._scores_at >= self.SCORES_TTL:
self._scores = self.collect_scores()
self._scores_at = now
if self._scores:
self._scores_at = now
return self._scores
def collect_scores(self):
return collect_scores()
def refresh_self(self):
self.membership.update_self(roles=list(self.context.self().roles), scores=self.scores())
self.membership.update_self(scores=self.scores())
def _after_probe(self, results, now):
def exchange_all(self, addresses):
if not addresses:
return []
with ThreadPoolExecutor(max_workers=min(self.probe_workers, len(addresses))) as pool:
return list(pool.map(lambda address: self.exchange(*address), addresses))
def _after_probe(self, results):
membership = self.membership
has_peers = any(record.key != membership.key for record in membership.alive_records())
@@ -179,28 +185,27 @@ class Gossip(Role):
outcome, delay = BOOTSTRAPPED, self.BOOTSTRAP_REPROBE
membership.denied = False
self.next_reprobe = now + delay
self.outcome = outcome
self.next_reprobe = membership.now() + delay
return outcome
def join_or_bootstrap(self):
self.refresh_self()
results = [self.exchange(ip, port) for ip, port in self.find_peers()]
outcome = self._after_probe(results, self.membership.now())
results = self.exchange_all(self.find_peers())
outcome = self._after_probe(results)
self.logger.info(f"Startup probe finished: {outcome}")
return outcome
def reprobe(self):
results = [self.exchange(ip, port) for ip, port in self.find_peers()]
self._after_probe(results, self.membership.now())
results = self.exchange_all(self.find_peers())
self._after_probe(results)
def tick(self):
membership = self.membership
self.refresh_self()
peers = [record for record in membership.alive_records() if record.key != membership.key]
for record in self.random.sample(peers, min(self.fanout, len(peers))):
self.exchange(*record.key)
chosen = self.random.sample(peers, min(self.fanout, len(peers)))
self.exchange_all([record.key for record in chosen])
if membership.now() >= self.next_reprobe:
self.reprobe()
@@ -209,7 +214,9 @@ class Gossip(Role):
self.release_roles()
def persist_peers(self):
addresses = self.membership.known_addresses()
known = self.membership.known_addresses()
seeds = [address for address in self.seed_addresses() if address not in known]
addresses = (known + seeds)[:self.MAX_SEEDS]
if addresses == self._persisted_peers:
return
state = self.context.get_role_manager().state_manager
@@ -234,4 +241,4 @@ class Gossip(Role):
drop = set(forbidden) | set(over_held)
if drop:
self.logger.info(f"Releasing roles {sorted(drop)}")
manager.set_roles([name for name in held if name not in drop])
manager.drop_roles(drop)
+29 -13
View File
@@ -1,9 +1,9 @@
import threading
from colony.core.context import RoleContext
from colony.core.network.advice import assignable_roles
from colony.core.roles.policy import DefaultRolePolicy
from colony.core.state_manager import StateManager
from colony.role_registry import RolePriority
from colony.core.logger.logging import get_logger
@@ -18,10 +18,8 @@ class RoleManager:
self._lock = threading.RLock()
def ensure_general_roles(self):
general = sorted(
name for name, stats in self.role_registry.roles.items()
if stats.priority == RolePriority.GENERAL
)
roles = self.role_registry.roles
general = sorted(set(roles) - set(assignable_roles(roles)))
current = self.state_manager.get(StateManager.Data.Roles)
wanted = list(dict.fromkeys(general + current))
if wanted != current:
@@ -42,11 +40,14 @@ class RoleManager:
self.logger.debug(f"Creating instance of role {role_name}. ")
roles[role_name] = role_cls(context=context)
self.network.self_node.roles = list(roles)
self.network.self_node.set_role_instances(roles)
self.network.membership.update_self(roles=list(roles))
self._publish(roles)
self.logger.info(f"Roles initialized: {list(roles.keys())}. ")
def _publish(self, roles):
self.network.self_node.set_role_instances(roles)
self.network.self_node.roles = list(roles)
self.network.membership.update_self(roles=list(roles))
def get_role_instances(self):
return self.network.self_node.get_role_instances()
@@ -92,9 +93,7 @@ class RoleManager:
}
self.state_manager.set(StateManager.Data.Roles, list(final_roles.keys()))
self.network.self_node.set_role_instances(final_roles)
self.network.self_node.roles = list(final_roles.keys())
self.network.membership.update_self(roles=list(final_roles))
self._publish(final_roles)
for role_name in roles_to_remove:
self.logger.debug(f"Stopping removed role {role_name}")
@@ -106,6 +105,17 @@ class RoleManager:
self.logger.info(f"Roles updated to: {list(final_roles.keys())}")
def drop_roles(self, role_names):
with self._lock:
assignable = assignable_roles(self.role_registry.roles)
held = list(self.get_role_instances())
remaining = [name for name in held if name not in role_names or name not in assignable]
if remaining != held:
self.set_roles(remaining)
def _decline(self, role_name):
self.network.membership.decline(role_name, self.role_policy.decline_cooldown)
def request_role(self, role_name):
membership = self.network.membership
@@ -124,9 +134,15 @@ class RoleManager:
accepted, reason = self.role_policy.decide(role_name, held, scores, self.role_registry.roles)
if not accepted:
self.logger.info(f"Declining role {role_name}: {reason}")
membership.decline(role_name, self.role_policy.decline_cooldown)
self._decline(role_name)
return {"accepted": False, "reason": reason}
self.set_roles(held + [role_name])
try:
self.set_roles(held + [role_name])
except Exception:
self.logger.exception(f"Could not take role {role_name}")
self._decline(role_name)
return {"accepted": False, "reason": "role could not be started"}
membership.clear_decline(role_name)
return {"accepted": True, "reason": reason}
+8 -6
View File
@@ -1,4 +1,4 @@
from colony.role_registry import RolePriority
from colony.core.network.advice import assignable_roles
from colony.settings import setting
@@ -14,11 +14,11 @@ class RolePolicy:
class DefaultRolePolicy(RolePolicy):
def __init__(self, max_roles=None, deny=None, min_scores=None, decline_cooldown=None):
self.max_roles = setting("role_policy.max_roles") if max_roles is None else max_roles
self.deny = set(setting("role_policy.deny") if deny is None else deny)
raw_minimums = setting("role_policy.min_scores") if min_scores is None else min_scores
self.max_roles = setting("role_policy.max_roles", max_roles)
self.deny = set(setting("role_policy.deny", deny))
raw_minimums = setting("role_policy.min_scores", min_scores)
self.min_scores = {role: dict(minimums) for role, minimums in dict(raw_minimums).items()}
self.decline_cooldown = setting("role_policy.decline_cooldown") if decline_cooldown is None else decline_cooldown
self.decline_cooldown = setting("role_policy.decline_cooldown", decline_cooldown)
def _violation(self, role_name, scores):
if role_name in self.deny:
@@ -34,11 +34,13 @@ class DefaultRolePolicy(RolePolicy):
return False, reason
if self.max_roles is not None:
assigned = [name for name in held if name in roles and roles[name].priority != RolePriority.GENERAL]
assigned = [name for name in held if name in assignable_roles(roles)]
if len(assigned) >= self.max_roles:
return False, "node already holds the maximum number of roles"
return True, "accepted"
def should_keep(self, role_name, scores):
if not scores:
return role_name not in self.deny
return self._violation(role_name, scores) is None
+7 -3
View File
@@ -19,6 +19,7 @@ class Role:
self._stop_event = threading.Event()
self._thread = None
self._start_lock = threading.Lock()
if self.CONTINUOUS and self.LOOP_SECONDS_WAIT is None:
raise NotImplementedError("You made this role to run continuously but have not set seconds to wait. Please set constant variable LOOP_SECONDS_WAIT. ")
@@ -31,11 +32,14 @@ class Role:
return self.__class__.__name__
def start(self):
if not self.CONTINUOUS or self._thread is not None:
if not self.CONTINUOUS:
return
self._thread = threading.Thread(target=self._run_loop, daemon=True, name=str(self))
self._thread.start()
with self._start_lock:
if self._thread is not None:
return
self._thread = threading.Thread(target=self._run_loop, daemon=True, name=str(self))
self._thread.start()
def _run_loop(self):
while not self._stop_event.is_set():
+4 -1
View File
@@ -17,6 +17,7 @@ DEFAULTS = {
"membership.reprobe_interval": 15,
"membership.fanout": 3,
"membership.forget_after": 600,
"membership.hello_timeout": 3,
"membership.max_members": 1024,
"membership.admission": "open",
"foreman.interval": 5,
@@ -30,5 +31,7 @@ DEFAULTS = {
}
def setting(key):
def setting(key, override=None):
if override is not None:
return override
return configuration.get(key, DEFAULTS[key])