Colony

my most ambitious (and needlessly convoluted) project yet

Colony is a project that I started to challenge myself. Its goal is to work as a self-sustainable network of nodes with some tasks to do. It's supposed to be managing itself in the most effective and resourceful way possible.

Basic Flow

1. boot

starts its base roles

2. look around

pings the IP range and old peers

3. say hello

swaps member lists with whoever answered

refused

backs off for a bit

first node

starts a new colony and takes every role

4. gossip

keeps chatting with peers, forgets silent ones

5. fill gaps

finds roles nobody holds, asks the best node

take the role?

that node accepts or declines

  • 1. boot to 2. look around
  • 2. look around to 3. say hello
  • 3. say hello to refused
  • refused to 2. look around (retry)
  • 2. look around to first node (no answer)
  • 3. say hello to 4. gossip
  • first node to 4. gossip
  • 4. gossip to 5. fill gaps
  • 5. fill gaps and take the role?, both directions
  • take the role? to 4. gossip (accepted)
class Discover:
    DISCOVER_PORT = 28149
    MAGIC = b"OK"

    @staticmethod
    def listen(real_port, host="0.0.0.0"):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((host, Discover.DISCOVER_PORT))
        s.listen()

        while True:
            conn, _ = s.accept()
            try:
                data = conn.recv(2)
                if data == Discover.MAGIC:
                    conn.sendall(struct.pack("!H", real_port))
            finally:
                conn.close()

    @staticmethod
    def request(target_host, timeout=3):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        s.connect((target_host, Discover.DISCOVER_PORT))
        s.sendall(Discover.MAGIC)
        data = s.recv(2)
        s.close()
        return struct.unpack("!H", data)[0]
The device being already in the network sends a request to a potential node. All the potential nodes listen on port 28149 and if they hear the message "OK" (bytes) then they send back the port on which the actual colony stuff happens.
class Foreman(Role):
    CONTINUOUS = True
    LOOP_SECONDS_WAIT = 60

    class COMMANDS(Enum):
        pass

	...

    def tick(self):
        membership = self.context.membership()
        if membership.denied:
            return None

        now = membership.now()
        records = membership.alive_records()
        roles = self.context.get_role_registry().roles

        unmet = advice.unmet_roles(records, roles)
        self._needed_since = {name: self._needed_since.get(name, now) for name in unmet}

        self._proposals = {
            key: (at, accepted) for key, (at, accepted) in self._proposals.items()
            if now - at < self.proposal_cooldown
        }

        alone = len(records) == 1
        rank = advice.rank_of(membership.key, records)
        wait = 0 if alone else self.acquire_delay + min(rank, 3) * self.proposal_stagger

        for role_name in unmet:
            if now - self._needed_since[role_name] < wait:
                continue

            recent = {key: accepted for (name, key), (_, accepted) in self._proposals.items() if name == role_name}
            if any(recent.values()):
                continue

            target = advice.pick_candidate(role_name, roles[role_name], records, exclude=set(recent))
            if target is None:
                if not recent:
                    self.logger.warning(f"No node is willing to take role {role_name}")
                continue

            result = self.propose(role_name, target)
            self._proposals[(role_name, target.key)] = (now, result[2] is True)
            return result

        return None

Then it initializes the basic roles, either the basic ones that it needs for network to work properly or roles given to it by the network's already existing Foreman role.

All roles

There are several roles that nodes take
available_roles = [
    RoleStats(
        cls=Unit,
        priority=RolePriority.GENERAL,
    ),
    RoleStats(
        cls=Gossip,
        priority=RolePriority.GENERAL,
    ),
    RoleStats(
        cls=Foreman,
        priority=RolePriority.GENERAL,
    ),
    RoleStats(
        cls=VaultWarden,
        priority=RolePriority.CRITICAL,
        hardware_preferences=HardwarePreferences(
            {
                HardwareType.DISK_SPACE: 1,
                HardwareType.LONGEVITY: 3,
            }
        )
    ),

	...

    RoleStats(
        cls=Chatter,
        priority=RolePriority.LOW,
        on_demand=True,
    )
]
	
  • Unit - general utilities for a node to communicate
  • Gossip - informs other nodes about it's peers
  • Foreman - decides who should take what role
  • VaultWarden - keeps files in order
  • Explorer - pings all the devices to see if they host colony
  • Gateway - acts as a bridge between nodes that can not communicate directly
  • Replicator - replicates VaultWarden files
  • WebView - shows stats in a web view hosted on configurable port
  • Chatter - lets user message other nodes (either through cli or WebView)

What still needs to be done

The project right now works as a proof of concept. It lacks some security features (each node blindly trusts each node that has a cert) and lacks some worker role that will actually benefit from being a network. The most useful features are replicating data and chatting but while functional it does not use the infrastructure to the full potential. For a full release please wait maybe 15 years more and I might finish it by then.