Building an Async HTTPS Client with Goblins and Fibers › Goblins Actor Implementation [#255]

Goblins builds on top of Fibers, providing a distributed object environment. The fibrous macro combines spawn-fiber with Goblins' sophisticated promise system—called vows. Unlike simple Fibers channels, vows support distributed promise pipelining: you can compose async operations across network boundaries.

Here's the complete Goblins implementation with the same httpbin delay test:

(use-modules (goblins))
(use-modules (goblins vat))
(use-modules (goblins actor-lib methods))
(use-modules (fibers conditions))
(use-modules (web response))
(use-modules ((web client) #:hide (open-socket-for-uri)))

;;; Borrow open-socket-for-uri from guile-websocket
(define open-socket-for-uri (@@ (web socket client) open-socket-for-uri))

(define (use-nonblocking-i/o! port)
  (fcntl port F_SETFL
         (logior O_NONBLOCK (fcntl port F_GETFL))))

(define (wrap-http-nonblocking http-proc)
  (λ (uri args)
    (let ([port (open-socket-for-uri uri #:configure-socket use-nonblocking-i/o!)])
      (apply http-proc uri #:port port args))))

;;; Define the HTTP client actor
(define-actor (^http-client _bcom)
  #:self self
  (methods
    [(request uri . args)
     (fibrous (call-with-values (λ () ((wrap-http-nonblocking http-request) uri args))
                                cons))]
    [(get     uri . args) (apply $ self 'request uri #:method 'GET     args)]
    [(head    uri . args) (apply $ self 'request uri #:method 'HEAD    args)]
    [(post    uri . args) (apply $ self 'request uri #:method 'POST    args)]
    [(put     uri . args) (apply $ self 'request uri #:method 'PUT     args)]
    [(delete  uri . args) (apply $ self 'request uri #:method 'DELETE  args)]
    [(trace   uri . args) (apply $ self 'request uri #:method 'TRACE   args)]
    [(options uri . args) (apply $ self 'request uri #:method 'OPTIONS args)]))

;;; Demonstrate async behavior with concurrent HTTP requests
(define (run-demo)
  (define done? (make-condition))
  (define completed-count 0)
  (define total-requests 3)

  (define (check-complete!)
    (set! completed-count (1+ completed-count))
    (when (= completed-count total-requests)
      (signal-condition! done?)))

  (define vat (spawn-vat))

  (with-vat vat
    (define http-client (spawn ^http-client))

    (display "Starting 3 concurrent requests with delays of 5s, 2s, and 3s...")
    (newline)
    (display (format #f "[~a] Main vat starting" 
                     (strftime "%H:%M:%S" (localtime (current-time)))))
    (newline)
    (newline)

    ;; Request 1: 5 second delay
    (on (<- http-client 'get "https://httpbin.org/delay/5")
        (λ (result)
          (display (format #f "[~a] Completed 5s delay request, status: ~a"
                           (strftime "%H:%M:%S" (localtime (current-time)))
                           (response-code (car result))))
          (newline)
          (check-complete!)))

    ;; Request 2: 2 second delay (will complete first)
    (on (<- http-client 'get "https://httpbin.org/delay/2")
        (λ (result)
          (display (format #f "[~a] Completed 2s delay request, status: ~a"
                           (strftime "%H:%M:%S" (localtime (current-time)))
                           (response-code (car result))))
          (newline)
          (check-complete!)))

    ;; Request 3: 3 second delay (will complete second)
    (on (<- http-client 'get "https://httpbin.org/delay/3")
        (λ (result)
          (display (format #f "[~a] Completed 3s delay request, status: ~a"
                           (strftime "%H:%M:%S" (localtime (current-time)))
                           (response-code (car result))))
          (newline)
          (check-complete!))))

  ;; Wait for all requests to complete
  ;; Note: on returns immediately, so we need done? to prevent exit
  (wait done?)

  (vat-halt! vat)

  (display "")
  (display (format #f "[~a] All requests completed!" 
                   (strftime "%H:%M:%S" (localtime (current-time)))))
  (newline)
  'done)

;; Run the demo
(run-demo)

Key differences from the Fibers version:

  1. We use on handlers instead of explicit channels—Goblins manages the async flow
  2. The <- operator sends messages to actors, which can be local or remote
  3. We use done? condition to wait for completion because on returns immediately
  4. The actor can be transparently moved to another machine via OCapN without code changes

The request method uses fibrous to leverage Fibers for non-blocking I/O while returning a Goblins vow. This vow can be pipelined with other async operations, even across network boundaries. The call-with-values handles Guile's multiple return values, and cons packages them for distributed messaging.