Bondy Connect
Skip to content

Beyond the Basics ​

Beyond WAMP's basic RPC and Publish/Subscribe patterns, this guide covers the advanced features a production deployment typically needs: load balancing, progressive results, and pattern-based routing.

RPC Load Balancing ​

In production systems, you often need multiple instances of a service for capacity, reliability, or geographic distribution. WAMP's shared registrations enable built-in load balancing without external infrastructure.

How Shared Registrations Work ​

Multiple callees can register the same procedure URI. When a call arrives, the router selects one callee using a configured invocation policy:

javascript
// Service Instance 1
wampy.register('com.myapp.process_order', {
    rpc: function(args, kwargs) {
        // Process order logic
        return {order_id: kwargs.order_id, processed_by: 'instance_1'};
    },
    invoke: 'roundrobin'  // Load balancing policy
});

// Service Instance 2
wampy.register('com.myapp.process_order', {
    rpc: function(args, kwargs) {
        // Same logic, different instance
        return {order_id: kwargs.order_id, processed_by: 'instance_2'};
    },
    invoke: 'roundrobin'  // Same policy
});

// Callers don't know or care which instance handles the call
wampy.call('com.myapp.process_order', null, {order_id: 123});

Invocation Policies ​

Bondy supports multiple invocation policies:

PolicyBehaviorUse Case
singleOnly one callee allowedEnsure singleton service
roundrobinDistribute calls evenly across calleesBalanced load distribution
randomRandom callee selectionSimple distribution with no ordering
firstAlways use first registered calleePrimary/backup pattern
lastAlways use last registered calleeRolling deployment with new version

Round Robin Load Balancing ​

Round robin ensures even distribution:

Call 1 → Instance 1
Call 2 → Instance 2
Call 3 → Instance 3
Call 4 → Instance 1  (wraps around)
Call 5 → Instance 2
...

Perfect for stateless services where any instance can handle any request.

Example use case:

javascript
// Image processing service with 5 instances
// All register with roundrobin policy
wampy.register('com.myapp.process_image', {
    rpc: processImage,
    invoke: 'roundrobin'
});

// Clients call normally
// Bondy automatically distributes across instances
wampy.call('com.myapp.process_image', [imageData]);

Random Distribution ​

Random selection provides statistical distribution without tracking state:

javascript
wampy.register('com.myapp.analytics.track', {
    rpc: trackEvent,
    invoke: 'random'  // Random distribution
});

Good for fire-and-forget operations where exact distribution doesn't matter.

First/Last Policies ​

Use first or last for primary/backup or canary deployment patterns:

Primary/Backup Pattern:

javascript
// Primary instance (registered first)
wampy.register('com.myapp.cache.get', {
    rpc: getCacheValue,
    invoke: 'first'
});

// Backup instance (registered second)
// Only receives calls if primary unregisters
wampy.register('com.myapp.cache.get', {
    rpc: getCacheValue,
    invoke: 'first'
});

Canary Deployment:

javascript
// Existing version (multiple instances)
wampy.register('com.myapp.api.process', {
    rpc: processV1,
    invoke: 'last'
});

// New version (canary instance)
wampy.register('com.myapp.api.process', {
    rpc: processV2,
    invoke: 'last'
});
// All new calls go to canary
// If it works, deploy more v2 instances

Geographic Distribution ​

Combine registration with Bondy clustering for geographic load balancing:

US-East Cluster:
  - Instance 1 registers procedure
  - Instance 2 registers procedure

US-West Cluster:
  - Instance 3 registers procedure
  - Instance 4 registers procedure

Clients in US-East connect to US-East cluster
  → Calls distributed to Instances 1 & 2

Clients in US-West connect to US-West cluster
  → Calls distributed to Instances 3 & 4

Each cluster handles its local clients, reducing latency and improving fault tolerance.

RPC Failover ​

Failover ensures service availability even when individual instances fail. WAMP provides automatic failover through dynamic registration and retry mechanisms.

Automatic Failover ​

When a callee disconnects, its registrations are automatically removed:

1. Instance 1 registers procedure
2. Instance 2 registers procedure (roundrobin)
3. Calls distributed between instances
4. Instance 1 crashes or disconnects
5. Bondy removes Instance 1's registration
6. All subsequent calls go to Instance 2
7. New Instance 3 registers procedure
8. Distribution resumes between Instances 2 & 3

No manual intervention required. The router adapts automatically. A call the departing instance was still handling fails with wamp.error.canceled, unless Bondy routes it again because it set CALL.Options._retries and the registration is idempotent.

Retry Logic ​

Implement client-side retry for transient failures:

javascript
async function callWithRetry(procedure, args, options = {}, maxRetries = 3) {
    let lastError;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await new Promise((resolve, reject) => {
                wampy.call(procedure, args, {
                    ...options,
                    onSuccess: resolve,
                    onError: reject
                });
            });
        } catch (error) {
            lastError = error;

            if (attempt < maxRetries) {
                // Exponential backoff
                const delay = Math.pow(2, attempt) * 100;
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }

    throw lastError;
}

// Usage
try {
    const result = await callWithRetry('com.myapp.api.process', [data]);
    console.log('Success:', result);
} catch (error) {
    console.error('Failed after retries:', error);
}

Health Checks ​

Implement health check procedures for monitoring:

javascript
// Service registers health check
wampy.register('com.myapp.service.health', {
    rpc: function() {
        return {
            status: 'healthy',
            timestamp: Date.now(),
            version: '1.2.3',
            uptime: process.uptime()
        };
    }
});

// Monitor calls health checks
setInterval(async () => {
    try {
        const health = await callWithRetry('com.myapp.service.health', [], {}, 1);
        console.log('Service healthy:', health);
    } catch (error) {
        console.error('Service unhealthy:', error);
        // Alert operations team
    }
}, 30000);  // Check every 30 seconds

Circuit Breaker Pattern ​

Prevent cascading failures with circuit breakers:

javascript
class CircuitBreaker {
    constructor(procedure, threshold = 5, timeout = 60000) {
        this.procedure = procedure;
        this.threshold = threshold;
        this.timeout = timeout;
        this.failures = 0;
        this.state = 'CLOSED';  // CLOSED, OPEN, HALF_OPEN
        this.nextAttempt = null;
    }

    async call(args, kwargs) {
        if (this.state === 'OPEN') {
            if (Date.now() < this.nextAttempt) {
                throw new Error('Circuit breaker OPEN');
            }
            this.state = 'HALF_OPEN';
        }

        try {
            const result = await new Promise((resolve, reject) => {
                wampy.call(this.procedure, args, {
                    ...kwargs,
                    onSuccess: resolve,
                    onError: reject
                });
            });

            // Success: reset or close circuit
            if (this.state === 'HALF_OPEN') {
                this.state = 'CLOSED';
                this.failures = 0;
            }

            return result;
        } catch (error) {
            this.failures++;

            if (this.failures >= this.threshold) {
                this.state = 'OPEN';
                this.nextAttempt = Date.now() + this.timeout;
            }

            throw error;
        }
    }
}

// Usage
const breaker = new CircuitBreaker('com.myapp.external.api', 5, 60000);

try {
    const result = await breaker.call([data]);
} catch (error) {
    console.log('Call failed or circuit open:', error);
}

Pattern-Based Registration ​

Register procedures using URI patterns instead of exact URIs, enabling dynamic, flexible service implementations.

Prefix Matching ​

Handle all procedures under a namespace:

javascript
wampy.register('com.myapp.users.', {
    rpc: function(args, kwargs, details) {
        // details.procedure contains exact URI called
        const operation = details.procedure.split('.').pop();

        switch (operation) {
            case 'get':
                return getUser(args[0]);
            case 'list':
                return listUsers();
            case 'create':
                return createUser(kwargs);
            case 'delete':
                return deleteUser(args[0]);
            default:
                throw new Error('Unknown operation');
        }
    },
    match: 'prefix'
});

// All these calls route to the same handler
wampy.call('com.myapp.users.get', [123]);
wampy.call('com.myapp.users.list');
wampy.call('com.myapp.users.create', null, {name: 'Alice'});
wampy.call('com.myapp.users.delete', [123]);

Wildcard Matching ​

Implement cross-cutting concerns:

javascript
// Logging middleware using wildcard
wampy.register('com.myapp..audit', {
    rpc: function(args, kwargs, details) {
        console.log('Audit called:', {
            procedure: details.procedure,
            caller: details.caller,
            timestamp: Date.now()
        });

        // Forward to actual handler
        const actualProcedure = details.procedure.replace('.audit', '');
        return wampy.call(actualProcedure, args, kwargs);
    },
    match: 'wildcard'
});

// Matches:
// com.myapp.users.audit
// com.myapp.orders.audit
// com.myapp.products.audit

Event Filtering ​

Control which subscribers receive which events based on filters and rules.

Publisher Exclusion ​

Don't receive your own publications:

javascript
wampy.publish('com.myapp.chat.message', ['Hello everyone!'], {}, {
    exclude_me: true  // Don't receive this event
});

wampy.subscribe('com.myapp.chat.message', function(args) {
    // Won't receive messages published by this session
    console.log('Message from someone else:', args[0]);
});

Subscriber Filtering ​

Control event delivery to specific sessions:

javascript
// Only send to specific sessions
wampy.publish('com.myapp.admin.alert', ['System issue'], {}, {
    eligible: [session_id_1, session_id_2]  // Only these sessions receive
});

// Exclude specific sessions
wampy.publish('com.myapp.broadcast', ['Update'], {}, {
    exclude: [session_id_3]  // Everyone except this session
});

Pattern-Based Subscriptions ​

Subscribe to multiple topics with patterns:

javascript
// Subscribe to all user events with prefix
wampy.subscribe('com.myapp.users.', {
    onEvent: function(args, kwargs, details) {
        console.log('User event:', details.topic, args);
    },
    match: 'prefix'
});

// Receives events from:
// com.myapp.users.created
// com.myapp.users.updated
// com.myapp.users.deleted

Call Timeouts ​

Prevent indefinite waiting with timeouts:

javascript
wampy.call('com.myapp.slow_operation', [data], {
    timeout: 5000,  // 5 second timeout
    onSuccess: function(result) {
        console.log('Completed:', result);
    },
    onError: function(error) {
        if (error.error === 'wamp.error.timeout') {
            console.log('Operation timed out');
        } else {
            console.error('Operation failed:', error);
        }
    }
});

Timeouts ensure callers don't wait indefinitely for unresponsive services.

Caller Identification ​

Know who's calling your procedures:

javascript
wampy.register('com.myapp.restricted.operation', {
    rpc: function(args, kwargs, details) {
        // details.caller contains caller's session ID
        console.log('Called by session:', details.caller);

        // Can look up caller identity
        // Check additional authorization
        // Log for audit purposes

        return performOperation(args, kwargs);
    }
});

Bondy discloses the caller by default. It omits the caller only when the registration sets disclose_caller: false and the call sets disclose_me: false. See Caller Identification.

Useful for:

  • Audit logging
  • Additional authorization checks
  • Rate limiting per caller
  • Usage analytics

Publisher Identification ​

Know who published events:

javascript
wampy.subscribe('com.myapp.events', {
    onEvent: function(args, kwargs, details) {
        // details.publisher contains publisher's session ID
        console.log('Published by session:', details.publisher);
        console.log('Event data:', args, kwargs);
    }
});

The subscriber does not ask for this information. Bondy discloses the publisher unless the publisher sets disclose_me: false. See Publisher Identification.

Best Practices ​

Design for Failure ​

Assume services will fail:

  • Use shared registrations for redundancy
  • Implement client-side retries
  • Use circuit breakers for external dependencies
  • Monitor service health

Start Simple, Scale Gradually ​

Begin with exact registrations and single instances:

javascript
// Start simple
wampy.register('com.myapp.process', {rpc: handler});

Add complexity only when needed:

javascript
// Scale to multiple instances
wampy.register('com.myapp.process', {
    rpc: handler,
    invoke: 'roundrobin'
});

Monitor and Measure ​

Track key metrics:

  • Call latency and success rates
  • Registration/unregistration events
  • Load distribution across instances
  • Error rates and types

Use WAMP meta events to build monitoring. Bondy publishes wamp.registration.on_register and wamp.registration.on_unregister, but not on_create or on_delete:

javascript
// Subscribe to registration changes
wampy.subscribe('wamp.registration.on_register', function(args, kwargs) {
    console.log('New registration:', kwargs);
});

wampy.subscribe('wamp.registration.on_unregister', function(args, kwargs) {
    console.log('Registration removed:', kwargs);
});

Test Failure Scenarios ​

Don't wait for production to test failover:

  1. Kill instances - Verify others take over
  2. Simulate network issues - Test retry logic
  3. Overload services - Validate circuit breakers
  4. Partition clusters - Ensure graceful degradation

Document Invocation Policies ​

Make registration policies explicit in service documentation:

javascript
/**
 * Process Order API
 *
 * Procedure: com.myapp.order.process
 * Invocation: roundrobin (multiple instances)
 * Timeout: 30 seconds recommended
 * Idempotent: Yes (safe to retry)
 */
wampy.register('com.myapp.order.process', {
    rpc: processOrder,
    invoke: 'roundrobin'
});

Summary ​

WAMP's beyond-the-basics features enable production-ready distributed systems:

  • Load balancing through shared registrations with configurable policies
  • Automatic failover as instances come and go
  • Pattern-based registration for flexible service implementations
  • Event filtering for fine-grained control over event delivery
  • Timeouts to prevent indefinite waiting
  • Caller/publisher identification for audit and authorization

These capabilities are built into the protocol — Bondy provides all of them without a separate load balancer, service registry, or other external infrastructure.

Explore the deep dives for more details: