long_polling

package
v1.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 1, 2026 License: Apache-2.0 Imports: 9 Imported by: 2

README

Long Polling

HTTP long polling server and client implementation for real-time event delivery.

Overview

The long-polling package provides a production-ready long polling solution built on top of golongpoll. It enables real-time communication between servers and clients using HTTP without requiring WebSocket connections, making it ideal for environments where WebSockets are blocked or unavailable.

Features

  • Long Polling Server - Event-driven server with configurable timeouts
  • Event Categories - Organize events into logical categories
  • File Persistence - Optional file-based event storage for durability
  • Custom Middleware - Authentication and validation hooks
  • Client Helpers - Simple subscription and publishing functions
  • Graceful Shutdown - Proper server cleanup with timeout control

Installation

go get -u github.com/common-library/go/long-polling

Quick Start

Server
server := &long_polling.Server{}
err := server.Start(long_polling.ServerInfo{
    Address: ":8080",
    TimeoutSeconds: 120,
    SubscriptionURI: "/events",
    PublishURI: "/publish",
}, long_polling.FilePersistorInfo{Use: false}, nil)
Client
// Subscribe
response, err := long_polling.Subscription(
    "http://localhost:8080/events",
    nil,
    long_polling.SubscriptionRequest{
        Category: "notifications",
        TimeoutSeconds: 60,
    },
    "", "", nil,
)

// Publish
_, err = long_polling.Publish(
    "http://localhost:8080/publish",
    10 * time.Second,
    nil,
    long_polling.PublishRequest{
        Category: "notifications",
        Data: `{"message": "Hello"}`,
    },
    "", "", nil,
)

API Reference

Server Types
ServerInfo
type ServerInfo struct {
    Address        string
    TimeoutSeconds int
    SubscriptionURI                string
    HandlerToRunBeforeSubscription func(w http.ResponseWriter, r *http.Request) bool
    PublishURI                string
    HandlerToRunBeforePublish func(w http.ResponseWriter, r *http.Request) bool
}

Server configuration parameters.

FilePersistorInfo
type FilePersistorInfo struct {
    Use                     bool
    FileName                string
    WriteBufferSize         int
    WriteFlushPeriodSeconds int
}

File persistence configuration for event durability.

Server Methods
Start
func (s *Server) Start(serverInfo ServerInfo, filePersistorInfo FilePersistorInfo, 
    listenAndServeFailureFunc func(err error)) error

Starts the long polling server.

Stop
func (s *Server) Stop(shutdownTimeout time.Duration) error

Gracefully shuts down the server.

Client Types
SubscriptionRequest
type SubscriptionRequest struct {
    Category       string
    TimeoutSeconds int
    SinceTime      int64
    LastID         string
}

Subscription parameters.

SubscriptionResponse
type SubscriptionResponse struct {
    Header     http.Header
    StatusCode int
    Events     []Event
}

Subscription response with events.

PublishRequest
type PublishRequest struct {
    Category string
    Data     string
}

Event publishing parameters.

Client Functions
Subscription
func Subscription(url string, header map[string][]string, request SubscriptionRequest,
    username, password string, transport *http.Transport) (SubscriptionResponse, error)

Subscribes to server events.

Publish
func Publish(url string, timeout time.Duration, header map[string][]string,
    publishRequest PublishRequest, username, password string, 
    transport *http.Transport) (http.Response, error)

Publishes an event to the server.

Complete Examples

Basic Server
package main

import (
    "log"
    "time"
    "github.com/common-library/go/long-polling"
)

func main() {
    server := &long_polling.Server{}
    
    err := server.Start(long_polling.ServerInfo{
        Address:         ":8080",
        TimeoutSeconds:  120,
        SubscriptionURI: "/events",
        PublishURI:      "/publish",
    }, long_polling.FilePersistorInfo{
        Use: false,
    }, func(err error) {
        log.Fatalf("Server error: %v", err)
    })
    
    if err != nil {
        log.Fatalf("Failed to start: %v", err)
    }
    
    // Wait for shutdown signal
    // ...
    
    server.Stop(10 * time.Second)
}
Server with Authentication
package main

import (
    "log"
    "net/http"
    "github.com/common-library/go/long-polling"
)

func authenticateSubscription(w http.ResponseWriter, r *http.Request) bool {
    token := r.Header.Get("Authorization")
    if token != "Bearer secret-token" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return false
    }
    return true
}

func authenticatePublish(w http.ResponseWriter, r *http.Request) bool {
    token := r.Header.Get("Authorization")
    if token != "Bearer admin-token" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return false
    }
    return true
}

func main() {
    server := &long_polling.Server{}
    
    err := server.Start(long_polling.ServerInfo{
        Address:                        ":8080",
        TimeoutSeconds:                 120,
        SubscriptionURI:                "/events",
        HandlerToRunBeforeSubscription: authenticateSubscription,
        PublishURI:                     "/publish",
        HandlerToRunBeforePublish:      authenticatePublish,
    }, long_polling.FilePersistorInfo{
        Use: false,
    }, nil)
    
    if err != nil {
        log.Fatal(err)
    }
}
Server with File Persistence
package main

import (
    "log"
    "github.com/common-library/go/long-polling"
)

func main() {
    server := &long_polling.Server{}
    
    err := server.Start(long_polling.ServerInfo{
        Address:         ":8080",
        TimeoutSeconds:  120,
        SubscriptionURI: "/events",
        PublishURI:      "/publish",
    }, long_polling.FilePersistorInfo{
        Use:                     true,
        FileName:                "/var/lib/longpoll/events.db",
        WriteBufferSize:         1000,
        WriteFlushPeriodSeconds: 5,
    }, nil)
    
    if err != nil {
        log.Fatal(err)
    }
}
Basic Client Subscription
package main

import (
    "fmt"
    "log"
    "github.com/common-library/go/long-polling"
)

func main() {
    for {
        response, err := long_polling.Subscription(
            "http://localhost:8080/events",
            nil,
            long_polling.SubscriptionRequest{
                Category:       "notifications",
                TimeoutSeconds: 60,
            },
            "", "", nil,
        )
        
        if err != nil {
            log.Printf("Subscription error: %v", err)
            continue
        }
        
        if response.StatusCode == 200 {
            for _, event := range response.Events {
                fmt.Printf("Event: %s - %s\n", event.Category, event.Data)
            }
        }
    }
}
Client with Authentication
package main

import (
    "fmt"
    "log"
    "net/http"
    "github.com/common-library/go/long-polling"
)

func main() {
    headers := map[string][]string{
        "Authorization": {"Bearer secret-token"},
    }
    
    for {
        response, err := long_polling.Subscription(
            "http://localhost:8080/events",
            headers,
            long_polling.SubscriptionRequest{
                Category:       "notifications",
                TimeoutSeconds: 60,
            },
            "", "", nil,
        )
        
        if err != nil {
            log.Printf("Error: %v", err)
            continue
        }
        
        if response.StatusCode == http.StatusUnauthorized {
            log.Fatal("Authentication failed")
        }
        
        for _, event := range response.Events {
            fmt.Printf("Event: %s\n", event.Data)
        }
    }
}
Client Publishing Events
package main

import (
    "log"
    "time"
    "github.com/common-library/go/long-polling"
)

func main() {
    response, err := long_polling.Publish(
        "http://localhost:8080/publish",
        10 * time.Second,
        nil,
        long_polling.PublishRequest{
            Category: "notifications",
            Data:     `{"type": "alert", "message": "System maintenance"}`,
        },
        "", "", nil,
    )
    
    if err != nil {
        log.Fatalf("Publish error: %v", err)
    }
    
    if response.StatusCode == 200 {
        log.Println("Event published successfully")
    }
}
Client with Incremental Updates
package main

import (
    "fmt"
    "log"
    "github.com/common-library/go/long-polling"
)

func main() {
    var lastEventID string
    
    for {
        request := long_polling.SubscriptionRequest{
            Category:       "updates",
            TimeoutSeconds: 60,
        }
        
        if lastEventID != "" {
            request.LastID = lastEventID
        }
        
        response, err := long_polling.Subscription(
            "http://localhost:8080/events",
            nil,
            request,
            "", "", nil,
        )
        
        if err != nil {
            log.Printf("Error: %v", err)
            continue
        }
        
        for _, event := range response.Events {
            fmt.Printf("Update: %s\n", event.Data)
            lastEventID = event.ID
        }
    }
}
Multi-Category Subscription
package main

import (
    "fmt"
    "log"
    "sync"
    "github.com/common-library/go/long-polling"
)

func subscribe(category string, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for {
        response, err := long_polling.Subscription(
            "http://localhost:8080/events",
            nil,
            long_polling.SubscriptionRequest{
                Category:       category,
                TimeoutSeconds: 60,
            },
            "", "", nil,
        )
        
        if err != nil {
            log.Printf("[%s] Error: %v", category, err)
            continue
        }
        
        for _, event := range response.Events {
            fmt.Printf("[%s] Event: %s\n", category, event.Data)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    
    categories := []string{"notifications", "alerts", "updates"}
    
    for _, category := range categories {
        wg.Add(1)
        go subscribe(category, &wg)
    }
    
    wg.Wait()
}
Complete Chat Application
package main

import (
    "bufio"
    "fmt"
    "log"
    "os"
    "time"
    "github.com/common-library/go/long-polling"
)

func receiveMessages() {
    for {
        response, err := long_polling.Subscription(
            "http://localhost:8080/events",
            nil,
            long_polling.SubscriptionRequest{
                Category:       "chat",
                TimeoutSeconds: 60,
            },
            "", "", nil,
        )
        
        if err != nil {
            continue
        }
        
        for _, event := range response.Events {
            fmt.Printf("Message: %s\n", event.Data)
        }
    }
}

func sendMessages() {
    scanner := bufio.NewScanner(os.Stdin)
    
    for {
        fmt.Print("Enter message: ")
        if !scanner.Scan() {
            break
        }
        
        message := scanner.Text()
        
        _, err := long_polling.Publish(
            "http://localhost:8080/publish",
            10 * time.Second,
            nil,
            long_polling.PublishRequest{
                Category: "chat",
                Data:     message,
            },
            "", "", nil,
        )
        
        if err != nil {
            log.Printf("Failed to send: %v", err)
        }
    }
}

func main() {
    go receiveMessages()
    sendMessages()
}

Best Practices

1. Set Appropriate Timeouts
// Good: Reasonable timeout for user interactions
ServerInfo{
    TimeoutSeconds: 60,  // 1 minute
}

// Avoid: Too short (excessive requests)
ServerInfo{
    TimeoutSeconds: 5,   // Too short
}

// Avoid: Too long (resource waste)
ServerInfo{
    TimeoutSeconds: 600, // 10 minutes
}
2. Handle Subscription Loops
// Good: Continue on errors
for {
    response, err := long_polling.Subscription(...)
    if err != nil {
        time.Sleep(1 * time.Second)
        continue
    }
    processEvents(response.Events)
}

// Avoid: Exit on first error
response, err := long_polling.Subscription(...)
if err != nil {
    return err // Don't give up
}
3. Use Categories Wisely
// Good: Specific categories
PublishRequest{
    Category: "user.1234.notifications",
    Data: "...",
}

// Avoid: Generic categories
PublishRequest{
    Category: "events", // Too broad
    Data: "...",
}
4. Implement Graceful Shutdown
// Good: Graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan

server.Stop(10 * time.Second)

// Avoid: Abrupt termination
os.Exit(0) // No cleanup
5. Enable Persistence for Critical Events
// Good: Persist important events
FilePersistorInfo{
    Use: true,
    FileName: "/var/lib/app/events.db",
    WriteBufferSize: 1000,
    WriteFlushPeriodSeconds: 5,
}

// Consider: No persistence for transient events
FilePersistorInfo{
    Use: false, // OK for temporary notifications
}

Performance Tips

  1. Connection Pooling - Use custom transport with connection pooling for multiple requests
  2. Timeout Configuration - Balance between latency and resource usage
  3. File Persistence - Use reasonable buffer size and flush period
  4. Event Categories - Use specific categories to reduce client filtering
  5. Graceful Shutdown - Always call Stop() to flush pending events

Testing

func TestLongPolling(t *testing.T) {
    server := &long_polling.Server{}
    
    go server.Start(long_polling.ServerInfo{
        Address:         ":8081",
        TimeoutSeconds:  10,
        SubscriptionURI: "/events",
        PublishURI:      "/publish",
    }, long_polling.FilePersistorInfo{Use: false}, nil)
    
    time.Sleep(100 * time.Millisecond)
    defer server.Stop(1 * time.Second)
    
    // Publish event
    _, err := long_polling.Publish(
        "http://localhost:8081/publish",
        5 * time.Second,
        nil,
        long_polling.PublishRequest{
            Category: "test",
            Data:     "hello",
        },
        "", "", nil,
    )
    
    if err != nil {
        t.Fatalf("Publish failed: %v", err)
    }
    
    // Subscribe
    response, err := long_polling.Subscription(
        "http://localhost:8081/events",
        nil,
        long_polling.SubscriptionRequest{
            Category:       "test",
            TimeoutSeconds: 5,
        },
        "", "", nil,
    )
    
    if err != nil {
        t.Fatalf("Subscription failed: %v", err)
    }
    
    if len(response.Events) == 0 {
        t.Error("No events received")
    }
}

Dependencies

  • github.com/jcuga/golongpoll - Core long polling implementation
  • github.com/gorilla/mux - HTTP router
  • github.com/common-library/go/http - HTTP utilities
  • github.com/common-library/go/json - JSON utilities
  • github.com/google/go-querystring - Query string encoding

Further Reading

Documentation

Overview

Package long_polling provides HTTP long polling server and client implementations.

This package enables real-time communication using HTTP long polling patterns, allowing clients to receive server-side events with minimal latency without WebSocket connections. It wraps golongpoll for easy server setup and client communication.

Features

  • Long polling server with configurable timeouts
  • Event subscription and publishing
  • File-based persistence for event durability
  • Custom middleware support for authentication and validation
  • Client helpers for subscription and publishing

Basic Client Example

response, err := long_polling.Subscription(
    "http://localhost:8080/events",
    nil,
    long_polling.SubscriptionRequest{
        Category: "notifications",
        TimeoutSeconds: 60,
    },
    "", "", nil,
)

Package long_polling provides HTTP long polling server and client implementations.

This package enables real-time communication using HTTP long polling patterns, allowing clients to receive server-side events with minimal latency without WebSocket connections. It wraps golongpoll for easy server setup and client communication.

Features

  • Long polling server with configurable timeouts
  • Event subscription and publishing
  • File-based persistence for event durability
  • Custom middleware support for authentication and validation
  • Client helpers for subscription and publishing

Basic Server Example

server := &long_polling.Server{}
err := server.Start(long_polling.ServerInfo{
    Address: ":8080",
    TimeoutSeconds: 120,
    SubscriptionURI: "/events",
    PublishURI: "/publish",
}, long_polling.FilePersistorInfo{Use: false}, nil)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Publish

func Publish(rawURL string, timeout time.Duration, header map[string][]string, publishRequest PublishRequest, username, password string, transport *net_http.Transport) (http.Response, error)

Publish publishes an event to the long polling server.

This function sends an event to the server, which distributes it to all subscribed clients in the specified category.

Parameters

  • rawURL: Server publish endpoint URL (e.g., "http://localhost:8080/publish")
  • timeout: Maximum duration to wait for publish operation
  • header: Optional HTTP headers (e.g., custom headers or authorization)
  • publishRequest: Event data (category and data payload)
  • username: Optional HTTP basic authentication username
  • password: Optional HTTP basic authentication password
  • transport: Optional custom HTTP transport for connection pooling or proxies

Returns

  • http.Response: HTTP response with headers, status code, and body
  • error: Error if request fails, nil on successful publish

Behavior

The publish request:

  • Sends event to server via POST request
  • Server assigns unique event ID and timestamp
  • Event is queued for subscribed clients
  • Returns immediately (non-blocking)

Examples

Publish notification:

response, err := long_polling.Publish(
    "http://localhost:8080/publish",
    10 * time.Second,
    nil,
    long_polling.PublishRequest{
        Category: "notifications",
        Data: `{"message": "Hello"}`
    },
    "", "", nil,
)

Types

type FilePersistorInfo

type FilePersistorInfo struct {
	Use                     bool
	FileName                string
	WriteBufferSize         int
	WriteFlushPeriodSeconds int
}

FilePersistorInfo is file persistor information.

type PublishRequest

type PublishRequest struct {
	Category string `json:"category"`
	Data     string `json:"data"`
}

PublishRequest is publish request information.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a struct that provides server related methods.

func (*Server) Start

func (s *Server) Start(serverInfo ServerInfo, filePersistorInfo FilePersistorInfo, listenAndServeFailureFunc func(err error)) error

Start initializes and starts the long polling server.

This method creates a long polling manager, sets up subscription and publish handlers, and starts the HTTP server. It supports optional file persistence for event durability across server restarts.

Parameters

  • serverInfo: Server configuration including address, timeout, URIs, and middleware
  • filePersistorInfo: File persistence configuration for event durability
  • listenAndServeFailureFunc: Optional callback for listen and serve failures

Returns

  • error: Error if server initialization or start fails, nil on success

Behavior

The server creates two endpoints:

  • Subscription endpoint (GET): Clients subscribe for events on specific categories
  • Publish endpoint (POST): Server or authorized clients publish events

Custom handlers can be provided to run before subscription or publish operations, enabling authentication, validation, or logging. If a handler returns false, the request is rejected.

Examples

Basic server:

server := &long_polling.Server{}
err := server.Start(long_polling.ServerInfo{
    Address: ":8080",
    TimeoutSeconds: 120,
    SubscriptionURI: "/events",
    PublishURI: "/publish",
}, long_polling.FilePersistorInfo{Use: false}, nil)

func (*Server) Stop

func (s *Server) Stop(shutdownTimeout time.Duration) error

Stop gracefully shuts down the long polling server.

This method stops the HTTP server with a timeout for existing connections and shuts down the long polling manager, ensuring all events are flushed if file persistence is enabled.

Parameters

  • shutdownTimeout: Maximum duration to wait for active connections to close

Returns

  • error: Error if shutdown fails, nil on successful shutdown

Behavior

The shutdown process:

  1. Stops accepting new connections
  2. Waits up to shutdownTimeout for active connections to close
  3. Shuts down the long polling manager (flushes pending events)
  4. Closes all resources

If the timeout is reached before all connections close, the server forcibly terminates remaining connections.

Examples

Graceful shutdown with 10 second timeout:

err := server.Stop(10 * time.Second)
if err != nil {
    log.Printf("Shutdown error: %v", err)
}

type ServerInfo

type ServerInfo struct {
	Address        string
	TimeoutSeconds int

	SubscriptionURI                string
	HandlerToRunBeforeSubscription func(w http.ResponseWriter, r *http.Request) bool

	PublishURI                string
	HandlerToRunBeforePublish func(w http.ResponseWriter, r *http.Request) bool
}

ServerInfo is server information.

type SubscriptionRequest

type SubscriptionRequest struct {
	Category       string `url:"category"`
	TimeoutSeconds int    `url:"timeout"`
	SinceTime      int64  `url:"since_time,omitempty"`
	LastID         string `url:"last_id,omitempty"`
}

SubscriptionRequest is subscription request information.

type SubscriptionResponse

type SubscriptionResponse struct {
	Header     net_http.Header
	StatusCode int
	Events     []struct {
		Timestamp int64  `json:"timestamp"`
		Category  string `json:"category"`
		ID        string `json:"id"`
		Data      string `json:"data"`
	} `json:"events"`
}

SubscriptionResponse is subscription response information.

func Subscription

func Subscription(rawURL string, header map[string][]string, request SubscriptionRequest, username, password string, transport *net_http.Transport) (SubscriptionResponse, error)

Subscription subscribes to server events using HTTP long polling.

This function sends a long polling request to the server and waits for events in the specified category. The request blocks until an event occurs or the timeout expires.

Parameters

  • rawURL: Server subscription endpoint URL (e.g., "http://localhost:8080/events")
  • header: Optional HTTP headers (e.g., custom headers or authorization)
  • request: Subscription parameters (category, timeout, since_time, last_id)
  • username: Optional HTTP basic authentication username
  • password: Optional HTTP basic authentication password
  • transport: Optional custom HTTP transport for connection pooling or proxies

Returns

  • SubscriptionResponse: Response containing events and metadata
  • error: Error if request fails or response parsing fails, nil on success

Behavior

The subscription request includes:

  • category: Event category to subscribe to
  • timeout: Maximum seconds to wait for events
  • since_time: Optional timestamp to retrieve events since (Unix milliseconds)
  • last_id: Optional last event ID to retrieve events after

The response includes:

  • Header: HTTP response headers
  • StatusCode: HTTP status code (200 for events, 204 for timeout)
  • Events: Array of events (empty if timeout)

Examples

Basic subscription:

response, err := long_polling.Subscription(
    "http://localhost:8080/events",
    nil,
    long_polling.SubscriptionRequest{
        Category: "notifications",
        TimeoutSeconds: 60,
    },
    "", "", nil,
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL