{"id":19541200,"url":"https://github.com/ysbaddaden/earl","last_synced_at":"2025-04-26T16:32:15.900Z","repository":{"id":66263901,"uuid":"119412313","full_name":"ysbaddaden/earl","owner":"ysbaddaden","description":"Service Objects for Crystal (Agents, Artists, Supervisors, Pools, ...)","archived":false,"fork":false,"pushed_at":"2025-03-10T10:20:43.000Z","size":131,"stargazers_count":101,"open_issues_count":9,"forks_count":6,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-04-04T16:11:58.921Z","etag":null,"topics":["actor","agent","crystal","pool","supervisor"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ysbaddaden.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-01-29T16:56:50.000Z","updated_at":"2025-03-16T01:32:39.000Z","dependencies_parsed_at":"2023-04-24T15:46:44.352Z","dependency_job_id":"e3c325d0-6781-4c35-a90e-5bc8b83dd9b3","html_url":"https://github.com/ysbaddaden/earl","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fearl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fearl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fearl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ysbaddaden%2Fearl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ysbaddaden","download_url":"https://codeload.github.com/ysbaddaden/earl/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251017755,"owners_count":21523633,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actor","agent","crystal","pool","supervisor"],"created_at":"2024-11-11T03:09:16.731Z","updated_at":"2025-04-26T16:32:15.893Z","avatar_url":"https://github.com/ysbaddaden.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Earl\n\nService objects for Crystal, aka Agents.\n\nCrystal provides primitives for achieving concurrent applications, but doesn't\nhave advanced layers for structuring applications. Earl tries to fill that gap\nwith a simple object-based API that's easy to grasp and understand.\n\n## Is Earl for me?\n\n- Your application has different, interconnected, objects that should always be\n  alive, until they decide or are asked to stop.\n- These different objects must communicate together.\n- You feel that you `spawn` and `loop` and must `rescue` exceptions and restart\n  objects too often.\n-  You need a pool of workers to dispatch work to.\n- ...\n\nIf so, then Earl is for you.\n\n\n## Status\n\nEarl is still in its infancy, but is fairly useable already.\n\nIf you believe Earl could help structure your application(s) please try it, and\nreport any shortcomings and successes you had!\n\n\n## Usage\n\nAdd the `earl` shard to your dependencies then run `shards install`:\n\n```yaml\ndependencies:\n  earl:\n    github: ysbaddaden/earl\n```\n\nFor a formal depiction of the Earl library, you can read \u003cSPEC.md\u003e. For an\ninformal introduction filled with examples, keep reading. For usage examples see\nthe \u003csamples\u003e directory.\n\n\n## Getting Started\n\n### Agents\n\nA simple agent is a class that includes `Earl::Agent` and implements a `#call`\nmethod. For example:\n\n```crystal\nrequire \"earl\"\n\nclass Foo\n  include Earl::Agent\n\n  @count = 0\n\n  def call\n    while running?\n      @count += 1\n      sleep 1\n    end\n  end\nend\n```\n\nEarl monitors the agent's state, and provides facilities to start and stop\nagents, to trap an agent crash or normal stop, as well as recycling them.\n\nCommunication (`Earl::Mailbox`) is an opt-in extension, and introduced below.\n\n#### Start Agents\n\nYou can start this agent in the current fiber with `#start`. This will block\nuntil the agent is stopped:\n\n```crystal\nfoo = Foo.new\nfoo.start\n```\nAlternatively you can call `#spawn` to start the agent in its own fiber, and\nreturn immediately:\n\n```crystal\nfoo = Foo.new\nfoo.spawn\n\ndo_something_else_concurrently\n```\n\nDepending on the context, it can be useful to block the current fiber. A\nlibrary, for example, already spawned a dedicated fiber (e.g. `HTTP::Server`\nconnections). Sometimes we need to start services in the background instead, and\ncontinue on.\n\n#### Stop Agents\n\nWe can ask an agent to stop gracefully with `#stop`. Each agent must return\nquickly from the `#call` method when the agent's state changes. Hence the\n`running?` call in the `Foo` agent above to break out of the loop, for example.\n\n```crystal\nfoo.stop\n```\n\nWhen an agent is stopped its `#terminate` method hook is called, allowing the\nagent to act upon termination. For example notify other services, closing\nconnections, or cleaning up.\n\n#### Link \u0026 Trap Agents\n\nWhen starting or spawning an agent `A` we can link another agent `B` to be\nnotified when the agent `A` stopped or crashed (raised an unhandled exception).\nThe linked agent `B` must implement the `#trap(Agent, Exception?`) method. If\nagent `A` crashed, then the unhandled exception is passed, otherwise it's `nil`.\nIn all cases, the stopped/crashed agent is passed.\n\nFor example:\n```crystal\nrequire \"earl\"\n\nclass A\n  include Earl::Agent\n\n  def call\n    # ...\n  end\nend\n\nclass B\n  include Earl::Agent\n\n  def call\n    # ...\n  end\n\n  def trap(agent, exception = nil)\n    log.error(\"crashed with #{exception.message}\") if exception\n  end\nend\n\na = A.new\nb = B.new\n\na.start\nb.start(link: a)\n```\n\nThe `Earl::Supervisor` and `Earl::Pool` agents use links and traps to keep\nservices alive for instance.\n\n#### Recycle Agents\n\nA stopped or crashed agent can be recycled to be restarted. Agents meant to be\nrecycled must implement the `#reset` method, and return the agent's internal\nstate to its pristine condition. A recycled agent must be indistinguishable from\na created agent.\n\nA recycled agent will return to the initial starting state, allowing it to\nrestart. `Earl::Supervisor`, for example, expects the agents it monitors to\nproperly reset themselves.\n\n\n### Agent Extensions\n\n#### Mailbox\n\nThe `Earl::Mailbox(M)` module extends an agent with a `Channel(M)` along with\nmethods to `#send(M)` a message to an agent and to receive them (concurrency\nsafe).\n\nThe module merely wraps a `Channel(M)` but proposes a standard structure for\nagents to have an incoming mailbox of messages. All agents thus behave the\nsame, and we can assume that an agent that expects to receive messages has a\n`#send(M)` method.\n\nAn agent's mailbox will be closed when the agent is asked to stop. An agent can\nsimply loop over `#receive?` until it returns `nil`, without having to check for\nthe agent's state.\n\n### Specific Agents\n\n#### Supervisor\n\nThe `Earl::Supervisor` agent monitors other agents (including other\nsupervisors). Monitored agents are spawned in their own fiber when the\nsupervisor starts. If a monitored agent crashes it's recycled then restarted\nin its own fiber.\n\nA supervisor can keep indefinitely running concurrent agents. It can also\nprevent the main thread from exiting.\n\nFor example:\n\n```crystal\nsupervisor = Supervisor.new\n\nproducer = Producer.new\nsupervisor.monitor(producer)\n\na = Consumer.new\nproducer.register(a)\nsupervisor.monitor(a)\n\nb = Consumer.new\nproducer.register(b)\nsupervisor.monitor(b)\n\nSignal::INT.trap { supervisor.stop }\nsupervisor.start\n```\n\nThe supervisor will start in the current fiber, and spawn a fiber for each of\nthe supervised actor: `producer`, `a` and `b`. If any of the actors crashes, the\nerror will be logged, the actor be restarted and the application will continue\nto run.\n\n#### Pool\n\nThe `Earl::Pool(A, M)` agent spawns a fixed size list of agents of type `A`, to\nwhich we can dispatch messages (of type `M`). Messages are delivered to a single\nworker of the pool in an exactly-once manner.\n\nWhenever a worker agent crashes, the pool will recycle and restart it. A worker\ncan stop normally, but it should only do so when asked to stop.\n\nWorker agents (of type `A`) must be capable to receive messages of type `M`.\nI.e. they include `Earl::Mailbox(M)` or `Earl::Artist(M)`. They must also\noverride their `#reset` method to properly reset an agent.\n\nNote that `Earl::Pool` will replace the workers' mailbox. All workers then share\na single `Channel(M)` for an exactly-once delivery of messages.\n\nFor example:\n\n```crystal\nclass Worker\n  include Earl::Agent\n  include Earl::Mailbox(String)\n\n  def call\n    while message = receive?\n      p message\n    end\n  end\nend\n\npool = Earl::Pool(Worker, String).new(capacity: 10)\n\nspawn do\n  5.times do |i|\n    pool.send(\"message #{i}\")\n  end\n  pool.stop\nend\n\npool.start\n# =\u003e message 1\n# =\u003e message 2\n# =\u003e message 3\n# =\u003e message 4\n# =\u003e message 5\n```\n\nPools are regular agents, so we can have pools of pools, but we discourage such\nusage. It'll only increase the complexity of your application for little or no\nreal benefit.\n\nYou can supervise pools with `Earl::Supervisor`. It can feel redundant because\npools already monitor other agents, but it can be useful to only have a few\nsupervisors to start (and stop).\n\n\n## Credits\n\n- Author: Julien Portalier (@ysbaddaden)\n\nSomewhat inspired by my very limited knowledge of Erlang OTP \u0026 Elixir.\n\n\n## License\n\nDistributed under the Apache Software License 2.0. See LICENSE for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fysbaddaden%2Fearl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fysbaddaden%2Fearl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fysbaddaden%2Fearl/lists"}