Create OTP Service
Use MetaScript's Erlang backend to build distributed, fault-tolerant systems with OTP supervision trees.
Why Erlang Backend?
- Nine nines: 99.9999999% uptime proven in production
- Hot code reload: Update code without dropping connections
- Millions of processes: Lightweight concurrency
- Distributed by default: Cluster multiple nodes seamlessly
Getting Started
Create OTP Project
npm create metascript@latest my-service -- --template otp
cd my-serviceProject Structure
my-service/
├── src/
│ ├── main.ms # Application entry
│ ├── supervisor.ms # Supervision tree
│ └── workers/ # Worker processes
├── build.ms # Build config
└── rebar.config # Erlang build configOTP Concepts in MetaScript
GenServer
A GenServer is a process that handles synchronous and asynchronous messages:
import { GenServer, call, cast } from "@metascript/otp"
@derive(GenServer)
class Counter {
private count: number = 0
// Synchronous call - caller waits for response
@call
increment(): number {
this.count += 1
return this.count
}
@call
get(): number {
return this.count
}
// Asynchronous cast - fire and forget
@cast
reset(): void {
this.count = 0
}
}
// Usage
const counter = await Counter.start()
console.log(await counter.increment()) // 1
console.log(await counter.increment()) // 2
counter.reset() // Returns immediately
console.log(await counter.get()) // 0Supervisor
Supervisors manage child processes and restart them on failure:
import { Supervisor, RestartStrategy } from "@metascript/otp"
const supervisor = new Supervisor({
strategy: RestartStrategy.OneForOne,
maxRestarts: 3,
maxSeconds: 5,
children: [
{
id: "counter",
start: () => Counter.start(),
restart: "permanent",
},
{
id: "cache",
start: () => Cache.start({ maxSize: 1000 }),
restart: "transient",
},
],
})
await supervisor.start()Restart Strategies
| Strategy | Behavior |
|---|---|
OneForOne | Restart only the failed child |
OneForAll | Restart all children when one fails |
RestForOne | Restart failed child and all after it |
Building a Chat Server
A complete example of a distributed chat server:
User Process
import { GenServer, call, cast, info } from "@metascript/otp"
@derive(GenServer)
class UserSession {
private socket: WebSocket
private username: string
private rooms: Set<string> = new Set()
constructor(socket: WebSocket, username: string) {
this.socket = socket
this.username = username
}
@call
joinRoom(room: string): void {
this.rooms.add(room)
RoomRegistry.join(room, this.pid)
}
@cast
receiveMessage(from: string, message: string): void {
this.socket.send(JSON.stringify({
type: "message",
from,
message,
}))
}
@info
handleSocketMessage(data: string): void {
const msg = JSON.parse(data)
if (msg.type === "send") {
RoomRegistry.broadcast(msg.room, this.username, msg.text)
}
}
}Room Registry
import { GenServer, call } from "@metascript/otp"
@derive(GenServer)
class RoomRegistry {
private rooms: Map<string, Set<Pid>> = new Map()
@call
join(room: string, pid: Pid): void {
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
}
this.rooms.get(room)!.add(pid)
}
@call
leave(room: string, pid: Pid): void {
this.rooms.get(room)?.delete(pid)
}
@call
broadcast(room: string, from: string, message: string): void {
const members = this.rooms.get(room)
if (members) {
for (const pid of members) {
pid.cast("receiveMessage", from, message)
}
}
}
}Application Supervisor
import { Application, Supervisor } from "@metascript/otp"
class ChatApp extends Application {
start() {
return new Supervisor({
strategy: RestartStrategy.OneForOne,
children: [
{
id: "room_registry",
start: () => RoomRegistry.start(),
},
{
id: "session_supervisor",
start: () => SessionSupervisor.start(),
},
{
id: "web_server",
start: () => WebServer.start({ port: 8080 }),
},
],
})
}
}Hot Code Reload
Update running code without dropping connections:
# Build new release
msc build --target=erlang --release
# Hot upgrade
msc otp upgrade --node my_service@localhost// Define upgrade path in your module
@derive(GenServer)
class Counter {
// ...
@codeChange
upgrade(oldState: any, oldVersion: string): this {
// Migrate state from old version
return this
}
}Distributed Clustering
Connect Nodes
# Start first node
msc otp start --name node1@192.168.1.1
# Start second node and connect
msc otp start --name node2@192.168.1.2 --connect node1@192.168.1.1Distributed Registry
import { global } from "@metascript/otp"
// Register process globally across cluster
await global.register("leader", LeaderProcess.start())
// Find process on any node
const leader = await global.whereis("leader")
await leader.call("doWork")Partition Handling
import { net_kernel } from "@metascript/otp"
net_kernel.monitor((event) => {
if (event.type === "nodedown") {
console.log(`Node ${event.node} disconnected`)
// Handle partition
}
})Deployment
Build Release
# Create production release
msc build --target=erlang --release
# Output in _build/prod/rel/my_serviceDocker Deployment
FROM erlang:26-alpine
COPY _build/prod/rel/my_service /app
WORKDIR /app
CMD ["bin/my_service", "foreground"]Kubernetes
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-service
spec:
serviceName: my-service
replicas: 3
template:
spec:
containers:
- name: my-service
image: my-service:latest
env:
- name: ERLANG_COOKIE
valueFrom:
secretKeyRef:
name: erlang-secret
key: cookieNext Steps
- Three Runtimes - Compare all backends
- Memory Model - Understand BEAM memory
- Deploy to Lambda - Serverless with C backend