Building an Async HTTPS Client with Goblins and Fibers › Goblins Actor Implementation [#255]
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:
- We use
onhandlers instead of explicit channels—Goblins manages the async flow - The
<-operator sends messages to actors, which can be local or remote - We use
done?condition to wait for completion becauseonreturns immediately - 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.