Services
Registers game systems and boots them in dependency order, every init before any start, then stops them in reverse and unwinds a boot that fails.
- Realm
- Shared
- Require
ReplicatedStorage.ChloeKernel .ServiceManager - Since
- v0.1.0
- Source
- ServiceManager.luau
Services are the game’s systems: plain tables registered with the kernel before start(), booted in dependency order
and stopped in reverse. The boot has two phases. Every service’s init runs before any service’s start, so by the time
a start runs, every service it depends on is fully built and safe to call. There is no lazy loading and no proxy
object. A boot that fails partway unwinds the services it already touched, and on the server the stop phase runs when
the server closes. The kernel side is on Kernel & Boot; per-player state belongs to
sessions and grouped cleanup to scopes. How a game lays out its services and
controllers is covered in Services and controllers.
Mental model
Section titled “Mental model”The ServiceManager is a registry, a topological sort and a list of services that have begun booting.
- Registration. Between
boot()andstart(),kernel:registerService(definition)records the definition by name and publishesKernel.ServiceRegistered(name)on the Bus. In the workspace,Game/init.luauregisters every entry ofGame/Registry.luauafter injecting the channel handles asself.Net. Nothing runs yet. - Order.
kernel:start()resolves the boot order with a depth-first walk overDependencies. Root names are visited in alphabetical order, so the order is the same on every run. The walk tracks its active path, so a cycle errors with the full chain (service dependency cycle: A -> B -> C -> A). - Init. Each service joins the active list just before its
init(self, kernel)runs, in boot order. A service builds its own state here. - Start. Every
start(self)runs in the same order. A service reaches for other services here:getServiceon a dependency returns a table whoseinitalready ran. - Stop.
stopAll()pops services off the active list, last booted first, and calls eachstop(self)in isolation. It runs on server close (see Stop on close), duringkernel:shutdown(), and when a boot fails.
init and start run under xpcall with debug.traceback. A service that throws aborts the boot with its name and
traceback in the error. A game that half-booted is worse than one that refuses to boot.
Failed boot unwinding
Section titled “Failed boot unwinding”Since 0.8.0 a boot that fails unwinds before it rethrows, so a half-built game does not leave connections, timers or instances behind:
initthrows: every service whoseinitwas attempted, including the one that threw, is stopped in reverse order. Aninitmay have allocated resources before it failed. Services whoseinitnever ran are not touched.startthrows: everyinitalready ran, so every service is stopped in reverse boot order, including services whosestartnever ran.
The boot then rethrows service "X" failed during init: or service "X" failed during start: with the traceback.
kernel:start() propagates it, the kernel never marks itself booted and the boot line never prints. After the unwind the
manager’s BootOrder and Timings are empty and it can boot again.
Because of this, stop(self) must tolerate init or start never having finished. Check every field before releasing
it:
function QuestService.stop(self: any) if self.Connection then self.Connection:disconnect() self.Connection = nil end if self.Lifetime then self.Lifetime:destroy() self.Lifetime = nil endendStop on close
Section titled “Stop on close”On a live server the kernel binds one BindToClose at boot. When the server closes it publishes Kernel.Closing, runs
the stop phase in reverse boot order within ServerKernel.CloseBudgetSeconds (25 s), then publishes Kernel.Stopped.
Before 0.9.0 service stop() never ran on a live server close. The sequence and the budget are on
Kernel & Boot.
A stop() may yield briefly for a final flush, such as a leaderboard drain. Profile writes made in stop() reach the
final saves, because the DataDriver close drain waits for Kernel.Stopped (up to 5 s).
A service stops at most once. stopAll() removes each service from the active list before calling its stop, so a
close followed by shutdown(), or a stop that calls stopAll() again, never stops a service twice, even when the
first call threw.
One service per module, listed in Game/Registry.luau:
--!strict-- InventoryService: per-visit item lists.
local InventoryService = { Name = "InventoryService",}
function InventoryService.init(self: any, kernel: any) self.Kernel = kernel self.Items = {} :: { [Player]: { string } }end
function InventoryService.give(self: any, player: Player, itemId: string) local Items = self.Items[player] if not Items then Items = {} self.Items[player] = Items end table.insert(Items, itemId)end
return InventoryService--!strict-- QuestService: rewards quest completions through InventoryService.
local QuestService = { Name = "QuestService", Dependencies = { "InventoryService" },}
function QuestService.init(self: any, kernel: any) self.Kernel = kernelend
function QuestService.start(self: any) -- Safe: InventoryService's init already ran self.Inventory = self.Kernel:getService("InventoryService") self.Connection = self.Kernel.Bus:subscribe("Quest.Completed", function(_topic, player: Player, questId: string) self.Inventory:give(player, `Reward_{questId}`) end)end
function QuestService.stop(self: any) -- Reverse boot order: runs before InventoryService's stop if self.Connection then self.Connection:disconnect() endend
return QuestServiceRegistration order does not matter. QuestService may be listed before InventoryService; only Dependencies orders
the boot.
A service is its definition table, so any extra field or method on it is the service’s public surface, as give is
above:
local ServerKernel = require(game:GetService("ServerScriptService").ChloeKernelServer)
local Inventory = ServerKernel.current():getService("InventoryService")Inventory:give(SomePlayer, "Sword")The service definition
Section titled “The service definition”export type ServiceDefinition = { Name: string, Dependencies: { string }?, init: ((self: any, kernel: any) -> ())?, start: ((self: any) -> ())?, stop: ((self: any) -> ())?, [any]: any,}| Field | Description |
|---|---|
Name |
Required, non-empty, unique per kernel. Game services end in Service and client controllers in Controller. |
Dependencies |
Names of services whose init and start must run before this one’s. Every listed name must be registered. |
init(self, kernel) |
Phase 1. Build your own state; do not call other services or yield. kernel is the booted kernel, and on the server it is the server kernel with the session API. |
start(self) |
Phase 2. Connect events, bind channel handlers and use other services. |
stop(self) |
Reverse boot order on close, on shutdown and when a boot unwinds. Must tolerate init or start not having finished. |
Net |
Injected by Game/init.luau before registration: the typed channel handles from Game.Channels. |
| anything else | Yours. The definition table is the service instance; getService returns it directly. |
All three lifecycle functions are optional. A service with only a Name is valid.
API reference
Section titled “API reference”Game code uses the kernel wrappers (kernel:registerService, kernel:start(), kernel:getService). The manager itself
is kernel.Services.
| Member | Description |
|---|---|
ServiceManager.new(kernel): ServiceManager |
Built by the kernel at boot. |
manager:register(definition: ServiceDefinition) |
Records a definition. Errors on a missing Name, a duplicate, registration after boot and registration during a lifecycle transition. |
manager:get(name: string): ServiceDefinition |
The service table. Errors unknown service "name"; never returns nil. |
manager:boot() |
Resolves the order, runs every init, then every start. Unwinds and rethrows on failure. Called by kernel:start(). |
manager:stopAll() |
Stops every active service in reverse order, each at most once. Clears BootOrder and Timings. |
manager.Definitions |
{ [string]: ServiceDefinition }, every registered service. |
manager.BootOrder |
{ string }, the resolved order. Empty before boot and after stopAll(). |
manager.ActiveOrder |
Services whose init began and that have not been stopped yet. |
manager.Timings |
Per service { Init: number, Start: number } in seconds. |
manager.Booted |
true after a successful boot, false again after stopAll(). |
manager.Booting, manager.Stopping |
true while a boot or a stop phase runs. |
manager.Kernel |
The kernel passed to every init. The server kernel sets it to itself. |
Error behavior
Section titled “Error behavior”Every failure has its own message. Exact strings from the source:
| Situation | Result |
|---|---|
Definition without a Name |
service definition requires a Name (assert at registration). |
| Duplicate name | service "X" is already registered. |
| Registration after boot | cannot register service "X" after boot. |
Registration from inside an init or start |
cannot register service "X" during a lifecycle transition. |
| Dependency cycle | service dependency cycle: A -> B -> C -> A, the full chain in path order. |
| Dependency on an unregistered name | service "A" depends on unregistered service "Ghost". |
init throws |
Attempted services stop in reverse, then service "X" failed during init: plus the traceback. |
start throws |
Every service stops in reverse, then service "X" failed during start: plus the traceback. |
stop throws |
Logs [ServiceManager] service "X" failed during stop: with the traceback and keeps stopping the rest. |
boot() twice |
services already booted (kernel:start() reports kernel already started first). |
boot() from inside a boot or stop |
services are already transitioning. |
stopAll() from inside a boot |
cannot stop services while boot is in progress. |
get with an unknown name |
unknown service "name". |
Service timings in stats
Section titled “Service timings in stats”boot() clocks each phase of each service. manager.Timings[name] holds { Init, Start } in seconds, and the table is
ServiceTimings in kernel:stats(). It is the first place to look when boot time grows:
for Name, Timing in kernel:stats().ServiceTimings do print(string.format("%s: init %.2fms, start %.2fms", Name, Timing.Init * 1000, Timing.Start * 1000))endThe boot line ([ChloeKernel 0.9.1] Server booted N services in Xms) is the total. The timings are cleared when the
services stop.
Bus topics
Section titled “Bus topics”| Topic | Arguments | Published |
|---|---|---|
Kernel.ServiceRegistered |
name |
After each successful registration. A plain Bus topic: a subscriber added later does not receive earlier registrations, so read kernel.Services.Definitions for those. |
Kernel.Closing |
closeReason? |
Server close begins; the stop phase follows. |
Kernel.Stopped |
closeReason? |
The server close stop phase finished. |