+ Change createAgent into serveAgent calls, previous one is still available. + Move tick mgr into client, and add auto disposable to properly close clients.
17 KiB
Core Concepts
Understanding the fundamental concepts of Huly Virtual Network is essential for building robust distributed systems.
Table of Contents
- Architecture Overview
- Network
- Agents
- Containers
- Clients
- Communication Patterns
- Lifecycle Management
- Endpoint References
- Container Kinds and Labels
Architecture Overview
Huly Network uses a hub-and-spoke architecture with three main components:
graph TB
subgraph "Network (Hub)"
N[Network Server]
R[Router]
REG[Container Registry]
end
subgraph "Agent 1"
A1[Agent]
C1A[Container A]
C1B[Container B]
end
subgraph "Agent 2"
A2[Agent]
C2A[Container C]
C2B[Container D]
end
subgraph "Clients"
CLIENT1[Client 1]
CLIENT2[Client 2]
end
CLIENT1 -->|Request Container| N
CLIENT2 -->|Request Container| N
N -->|Route to Agent| A1
N -->|Route to Agent| A2
A1 --> C1A
A1 --> C1B
A2 --> C2A
A2 --> C2B
Key Principles
- Centralized Coordination: The network server coordinates all agents and containers
- Distributed Execution: Containers run on agents, which can be distributed across multiple machines
- Dynamic Discovery: Clients discover and connect to containers through the network
- Automatic Lifecycle: The network manages container lifecycles based on client references
- Fault Tolerance: Automatic cleanup of failed agents and orphaned containers
Network
The Network is the central coordinator that:
- Maintains a registry of all agents and their capabilities
- Tracks all active containers and their locations
- Routes client requests to appropriate containers
- Manages container lifecycles (creation, reference counting, cleanup)
- Provides service discovery and load balancing
- Broadcasts events to clients about system changes
Network Responsibilities
interface Network {
// Agent management
register(record: AgentRecord, agent: NetworkAgentApi): Promise<ContainerUuid[]>
unregister(agentId: AgentUuid): Promise<void>
ping(agentId: AgentUuid): Promise<void>
// Container management
get(client: ClientUuid, kind: ContainerKind, options: GetOptions): Promise<[ContainerUuid, ContainerEndpointRef]>
release(client: ClientUuid, uuid: ContainerUuid): Promise<void>
list(kind?: ContainerKind): Promise<ContainerRecord[]>
// Communication
request(target: ContainerUuid, operation: string, data?: any): Promise<any>
// Discovery
agents(): AgentRecord[]
kinds(): ContainerKind[]
}
Network Server
The network server listens on a TCP port (default: 3737) and:
- Accepts connections from clients and agents
- Uses ZeroMQ for high-performance messaging
- Handles concurrent requests efficiently
- Maintains connection health through ping/pong
Example: Starting a Network Server
import { NetworkImpl, TickManagerImpl } from '@hcengineering/network-core'
import { NetworkServer } from '@hcengineering/network-server'
const tickManager = new TickManagerImpl(1000) // 1000 ticks/sec
tickManager.start()
const network = new NetworkImpl(tickManager)
const server = new NetworkServer(
network,
tickManager,
'*', // Bind to all interfaces
3737 // Port
)
console.log('Network server running on port 3737')
Agents
Agents are worker processes that:
- Host and manage containers
- Register with the network to advertise capabilities
- Create containers on-demand or pre-provision them (stateless)
- Handle container lifecycle (start, stop, health checks)
- Route requests between clients and containers
Agent Capabilities
Each agent declares which container kinds it can create:
// Note: For production code, use serveAgent() on the client
// This example uses AgentImpl directly for educational purposes
const agent = new AgentImpl('my-agent' as AgentUuid, {
session: async (options) => {
/* create session container */
},
workspace: async (options) => {
/* create workspace container */
},
query: async (options) => {
/* create query container */
}
})
Agent Registration
Agents register with the network to announce availability:
import { NetworkAgentServer } from '@hcengineering/network-client'
const agentServer = new NetworkAgentServer(
tickManager,
'localhost', // Network host
'*', // Bind address
3738 // Agent port for container connections
)
await agentServer.start(agent)
// Register with network
await client.register(agent)
Agent Health
Agents must:
- Send regular pings to the network (default: every 1 second)
- Respond to health checks
- Handle graceful shutdown
- Clean up containers on termination
If an agent fails to ping within the aliveTimeout (default: 3 seconds), the network:
- Marks the agent as dead
- Removes all its containers
- Broadcasts removal events
- Allows standby agents to take over (for stateless containers)
Containers
Containers are the workhorses of the system. They:
- Implement specific business logic
- Process requests from clients
- Maintain internal state
- Broadcast events to connected clients
- Are automatically created and destroyed based on demand
Container Interface
Every container must implement:
interface Container {
// Handle incoming requests
request(operation: string, data?: any, clientId?: ClientUuid): Promise<any>
// Health check
ping(): Promise<void>
// Cleanup and shutdown
terminate(): Promise<void>
// Event broadcasting support
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void
disconnect(clientId: ClientUuid): void
// Optional termination callback
onTerminated?(): void
}
Container Lifecycle
stateDiagram-v2
[*] --> Creating: Client requests container
Creating --> Active: Factory creates instance
Active --> Referenced: Client holds reference
Referenced --> Active: Client releases reference
Active --> Terminating: Timeout expires (no references)
Referenced --> Terminating: All clients released
Terminating --> [*]: Cleanup complete
Key lifecycle events:
- Creation: Factory method called when container is requested
- Active: Container is running and processing requests
- Referenced: At least one client holds a reference
- Idle Timeout: Container kept alive for
containerTimeoutafter last reference released - Termination:
terminate()called, then removed from network
Container Types
Stateful Containers (Dynamic)
Created on-demand when requested:
// Note: For production code, use serveAgent() on the client
// This example uses AgentImpl directly for educational purposes
const agent = new AgentImpl('agent-1' as any, {
'user-session': async (options: GetOptions) => {
const uuid = options.uuid ?? generateUuid()
const container = new UserSessionContainer(uuid, options)
return {
uuid,
container,
endpoint: `session://agent1/${uuid}` as any
}
}
})
Stateless Containers (Pre-provisioned)
Pre-created for high availability:
const leaderContainer = new LeaderContainer('leader-001' as ContainerUuid)
agent.addStatelessContainer(
'leader-001' as ContainerUuid,
'leader' as ContainerKind,
'leader://agent1/leader-001' as ContainerEndpointRef,
leaderContainer
)
Multiple agents can register the same stateless container UUID. The network accepts the first and rejects others, enabling automatic failover.
Container Example
import type { Container, ContainerUuid, ClientUuid } from '@hcengineering/network-core'
class DataStoreContainer implements Container {
private data = new Map<string, any>()
private connections = new Map<ClientUuid, (data: any) => Promise<void>>()
constructor(readonly uuid: ContainerUuid) {}
async request(operation: string, data?: any): Promise<any> {
switch (operation) {
case 'set':
this.data.set(data.key, data.value)
await this.broadcast({ type: 'dataChanged', key: data.key })
return { success: true }
case 'get':
return { value: this.data.get(data.key) }
default:
return { error: 'Unknown operation' }
}
}
async ping(): Promise<void> {}
async terminate(): Promise<void> {
this.data.clear()
this.connections.clear()
}
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void {
this.connections.set(clientId, broadcast)
}
disconnect(clientId: ClientUuid): void {
this.connections.delete(clientId)
}
private async broadcast(event: any): Promise<void> {
const promises = Array.from(this.connections.values()).map((fn) => fn(event))
await Promise.all(promises)
}
}
Clients
Clients are applications or services that:
- Connect to the network server
- Request containers by kind and optional criteria
- Send requests to containers
- Receive events from containers
- Manage container references (acquire/release)
Client Connection
import { createNetworkClient } from '@hcengineering/network-client'
const client = createNetworkClient(
'localhost:3737', // Network address
3600 // Alive timeout in seconds (optional)
)
await client.waitConnection(5000) // Wait up to 5 seconds
Requesting Containers
Clients request containers by kind and optional criteria:
// Get any container of this kind
const ref = await client.get('user-session' as ContainerKind, {})
// Get specific container by UUID
const ref = await client.get('user-session' as ContainerKind, {
uuid: 'session-123' as ContainerUuid
})
// Get container with labels
const ref = await client.get('workspace' as ContainerKind, {
labels: ['premium', 'us-west']
})
// Get container with extra data
const ref = await client.get('query-engine' as ContainerKind, {
extra: { database: 'analytics', userId: 'user-456' }
})
Client API
interface NetworkClient {
// Container management
get(kind: ContainerKind, request: GetOptions): Promise<ContainerReference>
list(kind?: ContainerKind): Promise<ContainerRecord[]>
// Agent management
register(agent: NetworkAgent): Promise<void>
unregister(agentId: AgentUuid): Promise<void>
// Discovery
agents(): AgentRecord[]
kinds(): ContainerKind[]
// Events
onUpdate(listener: NetworkUpdateListener): () => void
// Connection
close(): Promise<void>
}
Communication Patterns
Huly Network supports multiple communication patterns:
1. Request/Response (Synchronous)
Direct request to container with response:
const result = await containerRef.request('processData', {
value: 42
})
console.log(result) // { processed: true, result: 84 }
2. Fire-and-Forget (Asynchronous)
Send data without waiting for response:
await containerRef.request('logEvent', {
event: 'user_login',
timestamp: Date.now()
})
3. Event Broadcasting (Publish/Subscribe)
Container broadcasts events to all connected clients:
// Client side
const connection = await containerRef.connect()
connection.on = async (event) => {
console.log('Received:', event)
}
// Container side
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void {
this.clients.set(clientId, broadcast)
}
// Broadcast to all clients
for (const broadcast of this.clients.values()) {
await broadcast({ type: 'update', data: changes })
}
4. Bidirectional Streaming
Establish persistent connection for streaming:
const connection = await containerRef.connect()
// Receive stream
connection.on = async (chunk) => {
console.log('Chunk:', chunk)
}
// Send requests
await connection.request('subscribe', { topic: 'updates' })
await connection.request('getData', { range: [0, 100] })
Lifecycle Management
Reference Counting
The network uses reference counting to manage container lifecycles:
- Each
client.get()increments the reference count - Each
containerRef.close()decrements it - Containers with zero references are kept alive for
containerTimeout - After timeout, containers are automatically terminated
// Acquire reference (ref count = 1)
const ref1 = await client.get('service' as any, { uuid: 'svc-1' })
// Acquire another reference to same container (ref count = 2)
const ref2 = await client.get('service' as any, { uuid: 'svc-1' })
// Release first reference (ref count = 1)
await ref1.close()
// Release second reference (ref count = 0)
await ref2.close()
// Container kept alive for containerTimeout, then terminated
Health Monitoring
The network continuously monitors health:
Agent Health:
- Agents must ping every
pingInterval(default: 1 second) - Network marks agent as dead after
aliveTimeout(default: 3 seconds) - Dead agents' containers are removed
Container Health:
- Containers respond to
ping()calls - Failed pings may trigger termination
- Unhealthy containers are removed from registry
Graceful Shutdown
Proper cleanup on shutdown:
// Container cleanup
async terminate(): Promise<void> {
// 1. Notify connected clients
await this.broadcastShutdown()
// 2. Close external connections
await this.database.close()
// 3. Clear internal state
this.data.clear()
// 4. Release resources
this.connections.clear()
}
// Agent cleanup
await agentServer.close()
tickManager.stop()
// Client cleanup
await containerRef.close()
await client.close()
// Server cleanup
await server.close()
tickManager.stop()
Endpoint References
Containers are addressed by endpoint references:
type ContainerEndpointRef = string & { _containerEndpointRef: true }
Endpoint Types
-
Direct Endpoint: Direct connection to container
tcp://host:port/uuid -
Routed Endpoint: Connection through agent
agent://host:port:agentId/uuid -
No-Connect Endpoint: Request-only, no persistent connection
noconnect://host:port/uuid
Parsing Endpoints
import { parseEndpointRef, EndpointKind } from '@hcengineering/network-core'
const parsed = parseEndpointRef(endpoint)
console.log(parsed.kind) // EndpointKind.direct | routed | noconnect
console.log(parsed.host) // Host address
console.log(parsed.port) // Port number
console.log(parsed.uuid) // Container UUID
console.log(parsed.agentId) // Agent ID (for routed)
Container Kinds and Labels
Container Kinds
Containers are categorized by kind (string type):
type ContainerKind = string & { _containerKind: true }
Examples:
'user-session'- User session management'workspace'- Workspace containers'query-engine'- Query processing'transactor'- Transaction handling
Agents declare which kinds they support:
// Note: For production code, use serveAgent() on the client
// This example uses AgentImpl directly for educational purposes
const agent = new AgentImpl('agent-1' as any, {
'user-session': sessionFactory,
workspace: workspaceFactory,
'query-engine': queryFactory
})
Labels
Containers can have labels for fine-grained selection:
// Create container with labels
await client.get('workspace' as any, {
labels: ['premium', 'us-west', 'production']
})
// Labels enable:
// - Multi-tenancy (tenant ID as label)
// - Geographic routing (region labels)
// - Tier-based selection (free, premium, enterprise)
// - Environment separation (dev, staging, production)
Get Options
interface GetOptions {
uuid?: ContainerUuid // Specific container UUID
extra?: Record<string, any> // Additional parameters for factory
labels?: string[] // Labels for selection
}
Summary
Key concepts to remember:
- Network = Central coordinator
- Agent = Worker node hosting containers
- Container = Service instance with business logic
- Client = Application requesting containers
- Reference Counting = Automatic lifecycle management
- Endpoint = Address for reaching a container
- Kind = Container type/category
- Labels = Fine-grained selection criteria
These concepts form the foundation for building scalable, fault-tolerant distributed systems with Huly Network.
Next Steps
- Container Development Guide - Build your first container
- Agent Development Guide - Create custom agents
- Client Usage Guide - Connect and use containers
- Production Deployment - Deploy to production
Need more help? Check the Troubleshooting Guide or Examples.