A series of operations to perform once a connection count has changed.

        - Set f to max number of failures this system can handle.
        - Set status to one of started, started_hungry or starting depending on
            the number of protocol instances.
        - Check protocol instances. See `checkInstances()`

def onConnsChanged(self, joined: Set[str], left: Set[str]):
        """
        A series of operations to perform once a connection count has changed.

        - Set f to max number of failures this system can handle.
        - Set status to one of started, started_hungry or starting depending on
            the number of protocol instances.
        - Check protocol instances. See `checkInstances()`

        """
        _prev_status = self.status
        if self.isGoing():
            if self.connectedNodeCount == self.totalNodes:
                self.status = Status.started
            elif self.connectedNodeCount >= self.minimumNodes:
                self.status = Status.started_hungry
            else:
                self.status = Status.starting
        self.elector.nodeCount = self.connectedNodeCount

        if self.master_primary_name in joined:
            self.primaries_disconnection_times[self.master_replica.instId] = None
        if self.master_primary_name in left:
            logger.display('{} lost connection to primary of master'.format(self))
            self.lost_master_primary()
        elif _prev_status == Status.starting and self.status == Status.started_hungry \
                and self.primaries_disconnection_times[self.master_replica.instId] is not None \
                and self.master_primary_name is not None:
            """
            Such situation may occur if the pool has come back to reachable consensus but
            primary is still disconnected, so view change proposal makes sense now.
            """
            self._schedule_view_change()

        for inst_id, replica in self.replicas.items():
            if not replica.isMaster and replica.primaryName is not None:
                primary_node_name = replica.primaryName.split(':')[0]
                if primary_node_name in joined:
                    self.primaries_disconnection_times[inst_id] = None
                elif primary_node_name in left:
                    self.primaries_disconnection_times[inst_id] = time.perf_counter()
                    self._schedule_replica_removal(inst_id)

        if self.isReady():
            self.checkInstances()
        else:
            logger.info("{} joined nodes {} but status is {}".format(self, joined, self.status))
        # Send ledger status whether ready (connected to enough nodes) or not
        for node in joined:
            self.send_ledger_status_to_newly_connected_node(node)

        for node in left:
            self.network_i3pc_watcher.disconnect(node)

        for node in joined:
            self.network_i3pc_watcher.connect(node)