A distributed computing platform, designed to scale to millions of nodes and providing metaphoric abstractions.
Author Circo-dev
2 Stars
Updated Last
1 Year Ago
Started In
December 2019


Build Status

WARNING: Circo is in its infancy, not yet useful for real projects. The actor system will be released soon (See CircoCore, but the following description will only be true for v0.5, planned to be released in Q3 2020. Check the GitHub Projects for the roadmap.

Circo is a distributed computing platform, designed to scale to millions of nodes while providing metaphoric abstractions that help the programmer reason about such a complex system.

Circo achieves this by integrating an Actor model with a loosened Bulk synchronous parallel model into a component system that is driven by a distributed microkernel.

Components can send messages to each other, spawn new components and self-organize into a computing graph resembling a neural network. Components are "grounded" to a 3D space, their position is optimized to minimize approximated communication overhead.

Circo systems typically build themself from a single spawn and their structure responds dynamically to changes of the environment, e.g. they grow new components where load is high while unneeded components die. This dynamics can be described using a high level graph grammar.

Not much of that is available at the time, but you may feel the flavor if you check the following, working example:

# This test builds a binary tree of actors, growing a new level for every
# Start() message received by the original spawn (the TreeCreator).
# The growth of every leaf is reported back by its parent to the
# TreeCreator, which counts the nodes in the tree.

using Test
using Circo
import Circo.onmessage

GrowRequest = Message{ComponentId}
GrowResponse = Message{Vector{ComponentId}}
Start = Message{Nothing}

@component mutable struct TreeActor
    TreeActor() = new([])

Messages that components can send to each other are typed. The @component macro is a simple helper to generate the required fields and mark the component as subtype of Component.

function onmessage(me::TreeActor, message::GrowRequest, service)
    if length(me.children) == 0
        push!(me.children, spawn(service, TreeActor()))
        push!(me.children, spawn(service, TreeActor()))
        send(service, GrowResponse(me, body(message), me.children))
        for child in me.children
            send(service, redirect(message, child)) 
        end # TODO Would be nice to allow this instead: me.children |> redirect(message) |> send(service))

Leafs and inner nodes handle GrowRequests differently:

  • Leafs (nodes that have no children yet) grow two new leafs and report this event as a GrowResponse back to the address found in the body of the request.
  • Inner nodes forward the message to all of their children.
@component mutable struct TreeCreator
    TreeCreator() = new(0, 0)

function onmessage(me::TreeCreator, ::Start, service)
    if me.root == 0
        me.root = spawn(service, TreeActor())
        me.nodecount = 1
    send(service, GrowRequest(me, me.root, id(me)))

function onmessage(me::TreeCreator, message::GrowResponse, service)
    me.nodecount += length(body(message))

The TreeCreator handles communication with both the user (by handling Start messages) and the tree (by sending a single GrowRequest to the root and receiving a GrowResponse from every node that was a leaf before this grow).

@testset "Actor-Tree" begin
    creator = TreeCreator()
    machine = Machine(creator) # Create the machine and spawn creator
    for i in 1:10
        machine(Start()) # The Start signal will be delivered to the firstly spawned component
        @test creator.nodecount == 2^(i+1)-1

The Machine is your interface for creating and running the actor system. Its default behavior is to run synchronously until the message queue empties, so here we can grow a new level of the tree by sending a Start message. Check the actor tests and benchmarks for more examples.

Used By Packages