27 Commits
v0.1.0 ... main

Author SHA1 Message Date
80808310ce Make sure the library compiles for windows
systemd sockets are not applicable for windows. However compilation
should not fail on windows
2025-07-17 20:53:10 -04:00
97b73fb442 remove binary from git 2025-02-03 11:35:06 -05:00
42b8afc9da Remove log from test 2025-01-24 22:10:58 -05:00
74ade60009 Refactor: Add GetListener and TLS versions 2025-01-24 19:01:12 -05:00
27d7f247c3 return ctx wrapping all values 2025-01-24 17:01:39 -05:00
f5fdf698ef Fix bad example for boolean options 2024-12-21 16:34:12 -05:00
94c737a02b Merge pull request 'Change address syntax to be a URL' (#3) from addr-url into main
Reviewed-on: #3
2024-12-19 10:06:02 -05:00
74efe7c808 update README 2024-12-19 09:46:36 -05:00
06e51af628 do dupe keys check only once 2024-12-18 15:45:36 -05:00
b14072106a Fix golint issues and add test 2024-12-18 15:38:52 -05:00
7cab173c7d fix ParseAddress 2024-12-17 19:36:31 -05:00
47ee5fb4f4 remove channel close 2024-12-17 18:16:38 -05:00
79256b0440 update to new syntax 2024-12-17 13:21:37 -05:00
e789d40258 Add helper function to parse address 2024-12-17 12:07:42 -05:00
917f340870 remove trailing space 2024-05-09 12:10:33 -04:00
0693a56697 add doc for idle server auto shutdown 2023-09-11 21:01:20 -04:00
e95d2d0a96 Merge pull request 'Add global idler and other helpers' (#2) from globalIdler into main
Reviewed-on: #2

Fixes #1
2023-09-11 20:45:51 -04:00
d4304636a2 add basic test 2023-09-11 20:44:52 -04:00
5c43a1a4fc un http Server async and return error via channel 2023-09-11 20:28:04 -04:00
ee28a78eda Add WrapHandler to automatically call Tick on every request 2023-09-11 20:24:10 -04:00
8ca82581fe WIP: Enter/Exit won't work :( 2023-09-08 19:35:58 -04:00
b5588989de Add function to return address type 2023-09-08 19:10:36 -04:00
ea04fe735b Add example for idle 2023-05-09 22:45:52 -04:00
710e8b66e6 Add idle package 2023-05-08 22:37:44 -04:00
bbef4be4a7 Refactor environment parsing
Systemd environment variables LISTEN* are unset by default and saved for
future calls
2023-04-30 23:37:46 -04:00
bbd21d78cf Improve error messsages 2023-04-28 15:31:05 -04:00
d983f84093 Add links and documentation 2023-04-25 09:14:16 -04:00
9 changed files with 741 additions and 78 deletions

View File

@@ -1,7 +1,9 @@
Create http server listening on unix sockets or systemd socket activated fds Create http server listening on unix sockets and systemd socket activated fds
## Quick Usage ## Quick Usage
go get go.balki.me/anyhttp
Just replace `http.ListenAndServe` with `anyhttp.ListenAndServe`. Just replace `http.ListenAndServe` with `anyhttp.ListenAndServe`.
```diff ```diff
@@ -15,37 +17,64 @@ Just replace `http.ListenAndServe` with `anyhttp.ListenAndServe`.
Syntax Syntax
unix/<path to socket> unix?path=<socket_path>&mode=<socket file mode>&remove_existing=<true|false>
Examples Examples
unix/relative/path.sock unix?path=relative/path.sock
unix//var/run/app/absolutepath.sock unix?path=/var/run/app/absolutepath.sock
unix?path=/run/app.sock&mode=600&remove_existing=false
| option | description | default |
|-----------------|------------------------------------------------|----------|
| path | path to unix socket | Required |
| mode | socket file mode | 666 |
| remove_existing | Whether to remove existing socket file or fail | true |
### Systemd Socket activated fd: ### Systemd Socket activated fd:
Syntax Syntax
sysd/fdidx/<fd index starting at 0> sysd?idx=<fd index>&name=<fd name>&check_pid=<true|false>&unset_env=<true|false>&idle_timeout=<duration>
sysd/fdname/<fd name set using FileDescriptorName socket setting >
Only one of `idx` or `name` has to be set
Examples: Examples:
# First (or only) socket fd passed to app # First (or only) socket fd passed to app
sysd/fdidx/0 sysd?idx=0
# Socket with FileDescriptorName # Socket with FileDescriptorName
sysd/fdname/myapp sysd?name=myapp
# Using default name # Using default name and auto shutdown if no requests received in last 30 minutes
sysd/fdname/myapp.socket sysd?name=myapp.socket&idle_timeout=30m
### TCP port | option | description | default |
|--------------|--------------------------------------------------------------------------------------------|------------------|
| name | Name configured via FileDescriptorName or socket file name | Required |
| idx | FD Index. Actual fd num will be 3 + idx | Required |
| idle_timeout | time to wait before shutdown. [syntax][0] | no auto shutdown |
| check_pid | Check process PID matches LISTEN_PID | true |
| unset_env | Unsets the LISTEN\* environment variables, so they don't get passed to any child processes | true |
If the address is a number less than 65536, it is assumed as a port and passed as `http.ListenAndServe(":<port>",...)` ### TCP
Anything else is directly passed to `http.ListenAndServe` as well. Below examples should work If the address is not one of above, it is assumed to be tcp and passed to `http.ListenAndServe`.
Examples:
:http :http
:8888 :8888
127.0.0.1:8080 127.0.0.1:8080
## Documentation
https://pkg.go.dev/go.balki.me/anyhttp
### Related links
* https://gist.github.com/teknoraver/5ffacb8757330715bcbcc90e6d46ac74#file-unixhttpd-go
* https://github.com/coreos/go-systemd/tree/main/activation
[0]: https://pkg.go.dev/time#ParseDuration

View File

@@ -2,15 +2,34 @@
package anyhttp package anyhttp
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"syscall" "sync"
"time"
"go.balki.me/anyhttp/idle"
)
// AddressType of the address passed
type AddressType string
var (
// UnixSocket - address is a unix socket, e.g. unix?path=/run/foo.sock
UnixSocket AddressType = "UnixSocket"
// SystemdFD - address is a systemd fd, e.g. sysd?name=myapp.socket
SystemdFD AddressType = "SystemdFD"
// TCP - address is a TCP address, e.g. :1234
TCP AddressType = "TCP"
// Unknown - address is not recognized
Unknown AddressType = "Unknown"
) )
// UnixSocketConfig has the configuration for Unix socket // UnixSocketConfig has the configuration for Unix socket
@@ -39,6 +58,39 @@ func NewUnixSocketConfig(socketPath string) UnixSocketConfig {
return usc return usc
} }
type sysdEnvData struct {
pid int
fdNames []string
fdNamesStr string
numFds int
}
var sysdEnvParser = struct {
sysdOnce sync.Once
data sysdEnvData
err error
}{}
func parse() (sysdEnvData, error) {
p := &sysdEnvParser
p.sysdOnce.Do(func() {
p.data.pid, p.err = strconv.Atoi(os.Getenv("LISTEN_PID"))
if p.err != nil {
p.err = fmt.Errorf("invalid LISTEN_PID, err: %w", p.err)
return
}
p.data.numFds, p.err = strconv.Atoi(os.Getenv("LISTEN_FDS"))
if p.err != nil {
p.err = fmt.Errorf("invalid LISTEN_FDS, err: %w", p.err)
return
}
p.data.fdNamesStr = os.Getenv("LISTEN_FDNAMES")
p.data.fdNames = strings.Split(p.data.fdNamesStr, ":")
})
return p.data, p.err
}
// SysdConfig has the configuration for the socket activated fd // SysdConfig has the configuration for the socket activated fd
type SysdConfig struct { type SysdConfig struct {
// Integer value starting at 0. Either index or name is required // Integer value starting at 0. Either index or name is required
@@ -49,12 +101,14 @@ type SysdConfig struct {
CheckPID bool CheckPID bool
// Unsets the LISTEN* environment variables, so they don't get passed to any child processes // Unsets the LISTEN* environment variables, so they don't get passed to any child processes
UnsetEnv bool UnsetEnv bool
// Shutdown http server if no requests received for below timeout
IdleTimeout *time.Duration
} }
// DefaultSysdConfig has the default values for SysdConfig // DefaultSysdConfig has the default values for SysdConfig
var DefaultSysdConfig = SysdConfig{ var DefaultSysdConfig = SysdConfig{
CheckPID: true, CheckPID: true,
UnsetEnv: false, UnsetEnv: true,
} }
// NewSysDConfigWithFDIdx creates SysdConfig with defaults and fdIdx // NewSysDConfigWithFDIdx creates SysdConfig with defaults and fdIdx
@@ -95,16 +149,6 @@ func (u *UnixSocketConfig) GetListener() (net.Listener, error) {
// StartFD is the starting file descriptor number // StartFD is the starting file descriptor number
const StartFD = 3 const StartFD = 3
func makeFdListener(fd int, name string) (net.Listener, error) {
fdFile := os.NewFile(uintptr(fd), name)
l, err := net.FileListener(fdFile)
if err != nil {
return nil, err
}
syscall.CloseOnExec(fd)
return l, nil
}
// GetListener returns the FileListener created with socketed activated fd // GetListener returns the FileListener created with socketed activated fd
func (s *SysdConfig) GetListener() (net.Listener, error) { func (s *SysdConfig) GetListener() (net.Listener, error) {
@@ -112,99 +156,274 @@ func (s *SysdConfig) GetListener() (net.Listener, error) {
defer UnsetSystemdListenVars() defer UnsetSystemdListenVars()
} }
envData, err := parse()
if err != nil {
return nil, err
}
if s.CheckPID { if s.CheckPID {
pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) if envData.pid != os.Getpid() {
if err != nil { return nil, fmt.Errorf("unexpected PID, current:%v, LISTEN_PID: %v", os.Getpid(), envData.pid)
return nil, err
}
if pid != os.Getpid() {
return nil, fmt.Errorf("fd not for you")
} }
} }
numFds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))
if err != nil {
return nil, err
}
fdNames := strings.Split(os.Getenv("LISTEN_FDNAMES"), ":")
if s.FDIndex != nil { if s.FDIndex != nil {
idx := *s.FDIndex idx := *s.FDIndex
if idx < 0 || idx >= numFds { if idx < 0 || idx >= envData.numFds {
return nil, fmt.Errorf("invalid fd") return nil, fmt.Errorf("invalid fd index, expected between 0 and %v, got: %v", envData.numFds, idx)
} }
fd := StartFD + idx fd := StartFD + idx
if idx < len(fdNames) { if idx < len(envData.fdNames) {
return makeFdListener(fd, fdNames[idx]) return makeFdListener(fd, envData.fdNames[idx])
} }
return makeFdListener(fd, fmt.Sprintf("sysdfd_%d", fd)) return makeFdListener(fd, fmt.Sprintf("sysdfd_%d", fd))
} }
if s.FDName != nil { if s.FDName != nil {
for idx, name := range fdNames { for idx, name := range envData.fdNames {
if name == *s.FDName { if name == *s.FDName {
fd := StartFD + idx fd := StartFD + idx
return makeFdListener(fd, name) return makeFdListener(fd, name)
} }
} }
return nil, fmt.Errorf("fdName not found: %q", *s.FDName) return nil, fmt.Errorf("fdName not found: %q, LISTEN_FDNAMES:%q", *s.FDName, envData.fdNamesStr)
} }
return nil, fmt.Errorf("neither FDIndex nor FDName set") return nil, errors.New("neither FDIndex nor FDName set")
} }
// GetListener gets a unix or systemd socket listener // GetListener is low level function for use with non-http servers. e.g. tcp, smtp
func GetListener(addr string) (net.Listener, error) { // Caller should handle idle timeout if needed
if strings.HasPrefix(addr, "unix/") { func GetListener(addr string) (net.Listener, AddressType, any /* cfg */, error) {
usc := NewUnixSocketConfig(strings.TrimPrefix(addr, "unix/"))
return usc.GetListener()
}
if strings.HasPrefix(addr, "sysd/fdidx/") { addrType, unixSocketConfig, sysdConfig, perr := parseAddress(addr)
idx, err := strconv.Atoi(strings.TrimPrefix(addr, "sysd/fdidx/")) if perr != nil {
return nil, Unknown, nil, perr
}
if unixSocketConfig != nil {
listener, err := unixSocketConfig.GetListener()
if err != nil { if err != nil {
return nil, err return nil, Unknown, nil, err
} }
sysdc := NewSysDConfigWithFDIdx(idx) return listener, addrType, unixSocketConfig, nil
return sysdc.GetListener() } else if sysdConfig != nil {
listener, err := sysdConfig.GetListener()
if err != nil {
return nil, Unknown, nil, err
} }
return listener, addrType, sysdConfig, nil
}
if addr == "" {
addr = ":http"
}
listener, err := net.Listen("tcp", addr)
return listener, TCP, nil, err
}
if strings.HasPrefix(addr, "sysd/fdname/") { type ServerCtx struct {
sysdc := NewSysDConfigWithFDName(strings.TrimPrefix(addr, "sysd/fdname/")) AddressType AddressType
return sysdc.GetListener() Listener net.Listener
} Server *http.Server
Idler idle.Idler
Done <-chan error
UnixSocketConfig *UnixSocketConfig
SysdConfig *SysdConfig
}
return nil, nil func (s *ServerCtx) Wait() error {
return <-s.Done
}
func (s *ServerCtx) Addr() net.Addr {
return s.Listener.Addr()
}
func (s *ServerCtx) Shutdown(ctx context.Context) error {
err := s.Server.Shutdown(ctx)
if err != nil {
return err
}
return <-s.Done
}
// ServeTLS creates and serves a HTTPS server.
func ServeTLS(addr string, h http.Handler, certFile string, keyFile string) (*ServerCtx, error) {
return serve(addr, h, certFile, keyFile)
}
// Serve creates and serves a HTTP server.
func Serve(addr string, h http.Handler) (*ServerCtx, error) {
return serve(addr, h, "", "")
} }
// ListenAndServe is the drop-in replacement for `http.ListenAndServe`. // ListenAndServe is the drop-in replacement for `http.ListenAndServe`.
// Supports unix and systemd sockets in addition // Supports unix and systemd sockets in addition
func ListenAndServe(addr string, h http.Handler) error { func ListenAndServe(addr string, h http.Handler) error {
ctx, err := Serve(addr, h)
listener, err := GetListener(addr)
if err != nil { if err != nil {
return err return err
} }
return ctx.Wait()
}
if listener != nil { func ListenAndServeTLS(addr string, certFile string, keyFile string, h http.Handler) error {
return http.Serve(listener, h) ctx, err := ServeTLS(addr, h, certFile, keyFile)
if err != nil {
return err
} }
return ctx.Wait()
if port, err := strconv.Atoi(addr); err == nil {
if port > 0 && port < 65536 {
return http.ListenAndServe(fmt.Sprintf(":%v", port), h)
}
return fmt.Errorf("invalid port: %v", port)
}
return http.ListenAndServe(addr, h)
} }
// UnsetSystemdListenVars unsets the LISTEN* environment variables so they are not passed to any child processes // UnsetSystemdListenVars unsets the LISTEN* environment variables so they are not passed to any child processes
func UnsetSystemdListenVars() { func UnsetSystemdListenVars() {
os.Unsetenv("LISTEN_PID") _ = os.Unsetenv("LISTEN_PID")
os.Unsetenv("LISTEN_FDS") _ = os.Unsetenv("LISTEN_FDS")
os.Unsetenv("LISTEN_FDNAMES") _ = os.Unsetenv("LISTEN_FDNAMES")
}
func parseAddress(addr string) (addrType AddressType, usc *UnixSocketConfig, sysc *SysdConfig, err error) {
usc = nil
sysc = nil
err = nil
u, err := url.Parse(addr)
if err != nil {
return TCP, nil, nil, nil
}
if u.Path == "unix" {
duc := DefaultUnixSocketConfig
usc = &duc
addrType = UnixSocket
for key, val := range u.Query() {
if len(val) != 1 {
err = fmt.Errorf("unix socket address error. Multiple %v found: %v", key, val)
return
}
if key == "path" {
usc.SocketPath = val[0]
} else if key == "mode" {
if _, serr := fmt.Sscanf(val[0], "%o", &usc.SocketMode); serr != nil {
err = fmt.Errorf("unix socket address error. Bad mode: %v, err: %w", val, serr)
return
}
} else if key == "remove_existing" {
if removeExisting, berr := strconv.ParseBool(val[0]); berr == nil {
usc.RemoveExisting = removeExisting
} else {
err = fmt.Errorf("unix socket address error. Bad remove_existing: %v, err: %w", val, berr)
return
}
} else {
err = fmt.Errorf("unix socket address error. Bad option; key: %v, val: %v", key, val)
return
}
}
if usc.SocketPath == "" {
err = fmt.Errorf("unix socket address error. Missing path; addr: %v", addr)
return
}
} else if u.Path == "sysd" {
dsc := DefaultSysdConfig
sysc = &dsc
addrType = SystemdFD
for key, val := range u.Query() {
if len(val) != 1 {
err = fmt.Errorf("systemd socket fd address error. Multiple %v found: %v", key, val)
return
}
if key == "name" {
sysc.FDName = &val[0]
} else if key == "idx" {
if idx, ierr := strconv.Atoi(val[0]); ierr == nil {
sysc.FDIndex = &idx
} else {
err = fmt.Errorf("systemd socket fd address error. Bad idx: %v, err: %w", val, ierr)
return
}
} else if key == "check_pid" {
if checkPID, berr := strconv.ParseBool(val[0]); berr == nil {
sysc.CheckPID = checkPID
} else {
err = fmt.Errorf("systemd socket fd address error. Bad check_pid: %v, err: %w", val, berr)
return
}
} else if key == "unset_env" {
if unsetEnv, berr := strconv.ParseBool(val[0]); berr == nil {
sysc.UnsetEnv = unsetEnv
} else {
err = fmt.Errorf("systemd socket fd address error. Bad unset_env: %v, err: %w", val, berr)
return
}
} else if key == "idle_timeout" {
if timeout, terr := time.ParseDuration(val[0]); terr == nil {
sysc.IdleTimeout = &timeout
} else {
err = fmt.Errorf("systemd socket fd address error. Bad idle_timeout: %v, err: %w", val, terr)
return
}
} else {
err = fmt.Errorf("systemd socket fd address error. Bad option; key: %v, val: %v", key, val)
return
}
}
if (sysc.FDIndex == nil) == (sysc.FDName == nil) {
err = fmt.Errorf("systemd socket fd address error. Exactly only one of name and idx has to be set. name: %v, idx: %v", sysc.FDName, sysc.FDIndex)
return
}
} else {
// Just assume as TCP address
return TCP, nil, nil, nil
}
return
}
func serve(addr string, h http.Handler, certFile string, keyFile string) (*ServerCtx, error) {
serveFn := func() func(ctx *ServerCtx) error {
if certFile != "" {
return func(ctx *ServerCtx) error {
return ctx.Server.ServeTLS(ctx.Listener, certFile, keyFile)
}
}
return func(ctx *ServerCtx) error {
return ctx.Server.Serve(ctx.Listener)
}
}()
var ctx ServerCtx
var err error
var cfg any
ctx.Listener, ctx.AddressType, cfg, err = GetListener(addr)
if err != nil {
return nil, err
}
switch ctx.AddressType {
case UnixSocket:
ctx.UnixSocketConfig = cfg.(*UnixSocketConfig)
case SystemdFD:
ctx.SysdConfig = cfg.(*SysdConfig)
}
errChan := make(chan error)
ctx.Done = errChan
if ctx.AddressType == SystemdFD && ctx.SysdConfig.IdleTimeout != nil {
ctx.Idler = idle.CreateIdler(*ctx.SysdConfig.IdleTimeout)
ctx.Server = &http.Server{Handler: idle.WrapIdlerHandler(ctx.Idler, h)}
waitErrChan := make(chan error)
go func() {
waitErrChan <- serveFn(&ctx)
}()
go func() {
select {
case err := <-waitErrChan:
errChan <- err
case <-ctx.Idler.Chan():
errChan <- ctx.Server.Shutdown(context.TODO())
}
}()
} else {
ctx.Server = &http.Server{Handler: h}
go func() {
errChan <- serveFn(&ctx)
}()
}
return &ctx, nil
} }

158
anyhttp_test.go Normal file
View File

@@ -0,0 +1,158 @@
package anyhttp
import (
"context"
"encoding/json"
"testing"
"time"
)
func Test_parseAddress(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
addr string
wantAddrType AddressType
wantUsc *UnixSocketConfig
wantSysc *SysdConfig
wantErr bool
}{
{
name: "tcp port",
addr: ":8080",
wantAddrType: TCP,
wantUsc: nil,
wantSysc: nil,
wantErr: false,
},
{
name: "unix address",
addr: "unix?path=/run/foo.sock&mode=660",
wantAddrType: UnixSocket,
wantUsc: &UnixSocketConfig{
SocketPath: "/run/foo.sock",
SocketMode: 0660,
RemoveExisting: true,
},
wantSysc: nil,
wantErr: false,
},
{
name: "systemd address",
addr: "sysd?name=foo.socket",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: &SysdConfig{
FDIndex: nil,
FDName: ptr("foo.socket"),
CheckPID: true,
UnsetEnv: true,
IdleTimeout: nil,
},
wantErr: false,
},
{
name: "systemd address with index",
addr: "sysd?idx=0&idle_timeout=30m",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: &SysdConfig{
FDIndex: ptr(0),
FDName: nil,
CheckPID: true,
UnsetEnv: true,
IdleTimeout: ptr(30 * time.Minute),
},
wantErr: false,
},
{
name: "systemd address. Bad example",
addr: "sysd?idx=0&idle_timeout=30m&name=foo",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: nil,
wantErr: true,
},
{
name: "systemd address with check_pid and unset_env",
addr: "sysd?idx=0&check_pid=false&unset_env=f",
wantAddrType: SystemdFD,
wantUsc: nil,
wantSysc: &SysdConfig{
FDIndex: ptr(0),
FDName: nil,
CheckPID: false,
UnsetEnv: false,
IdleTimeout: nil,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotAddrType, gotUsc, gotSysc, gotErr := parseAddress(tt.addr)
if gotErr != nil {
if !tt.wantErr {
t.Errorf("parseAddress() failed: %v", gotErr)
}
return
}
if tt.wantErr {
t.Fatal("parseAddress() succeeded unexpectedly")
}
if gotAddrType != tt.wantAddrType {
t.Errorf("parseAddress() addrType = %v, want %v", gotAddrType, tt.wantAddrType)
}
if !check(gotUsc, tt.wantUsc) {
t.Errorf("parseAddress() Usc = %v, want %v", gotUsc, tt.wantUsc)
}
if !check(gotSysc, tt.wantSysc) {
if (gotSysc == nil || tt.wantSysc == nil) ||
!(check(gotSysc.FDIndex, tt.wantSysc.FDIndex) &&
check(gotSysc.FDName, tt.wantSysc.FDName) &&
check(gotSysc.IdleTimeout, tt.wantSysc.IdleTimeout)) {
t.Errorf("parseAddress() Sysc = %v, want %v", asJSON(gotSysc), asJSON(tt.wantSysc))
}
}
})
}
}
func TestServe(t *testing.T) {
ctx, err := Serve("unix?path=/tmp/foo.sock", nil)
if err != nil {
t.Fatal()
}
if ctx.AddressType != UnixSocket {
t.Errorf("Serve() ServerCtx = %v, want %v", ctx.AddressType, UnixSocket)
}
ctx.Shutdown(context.TODO())
}
// Helpers
// print value instead of pointer
func asJSON[T any](val T) string {
op, err := json.Marshal(val)
if err != nil {
return err.Error()
}
return string(op)
}
func ptr[T any](val T) *T {
return &val
}
// nil safe equal check
func check[T comparable](got, want *T) bool {
if (got == nil) != (want == nil) {
return false
}
if got == nil {
return true
}
return *got == *want
}

View File

@@ -0,0 +1,35 @@
package main
import (
"context"
"net/http"
"time"
"go.balki.me/anyhttp/idle"
)
func main() {
idler := idle.CreateIdler(10 * time.Second)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
idler.Tick()
w.Write([]byte("Hey there!\n"))
})
http.HandleFunc("/job", func(w http.ResponseWriter, r *http.Request) {
go func() {
idler.Enter()
defer idler.Exit()
time.Sleep(15 * time.Second)
}()
w.Write([]byte("Job scheduled\n"))
})
server := http.Server{
Addr: ":8888",
}
go server.ListenAndServe()
idler.Wait()
server.Shutdown(context.TODO())
}

32
examples/simple/main.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"context"
"log"
"net/http"
"os"
"time"
"go.balki.me/anyhttp"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello\n"))
})
//log.Println("Got error: ", anyhttp.ListenAndServe(os.Args[1], nil))
ctx, err := anyhttp.Serve(os.Args[1], nil)
log.Printf("Got ctx: %v\n, err: %v", ctx, err)
log.Println(ctx.Addr())
if err != nil {
return
}
select {
case doneErr := <-ctx.Done:
log.Println(doneErr)
case <-time.After(1 * time.Minute):
log.Println("Awake")
ctx.Shutdown(context.TODO())
}
}

19
fd_unix.go Normal file
View File

@@ -0,0 +1,19 @@
//go:build unix
package anyhttp
import (
"net"
"os"
"syscall"
)
func makeFdListener(fd int, name string) (net.Listener, error) {
fdFile := os.NewFile(uintptr(fd), name)
l, err := net.FileListener(fdFile)
if err != nil {
return nil, err
}
syscall.CloseOnExec(fd)
return l, nil
}

10
fd_windows.go Normal file
View File

@@ -0,0 +1,10 @@
package anyhttp
import (
"errors"
"net"
)
func makeFdListener(fd int, name string) (net.Listener, error) {
return nil, errors.New("windows not supported")
}

127
idle/idle.go Normal file
View File

@@ -0,0 +1,127 @@
// Package idle helps to gracefully shutdown idle (typically http) servers
package idle
import (
"fmt"
"net/http"
"sync/atomic"
"time"
)
var (
// For simple servers without backgroud jobs, global singleton for simpler API
// Enter/Exit worn't work for global idler as Enter may be called before Wait, use CreateIdler in those cases
gIdler atomic.Pointer[idler]
)
// Wait waits till the server is idle and returns. i.e. no Ticks in last <timeout> duration
func Wait(timeout time.Duration) error {
i := CreateIdler(timeout).(*idler)
ok := gIdler.CompareAndSwap(nil, i)
if !ok {
return fmt.Errorf("idler already waiting")
}
i.Wait()
return nil
}
// Tick records the current time. This will make the server not idle until next Tick or timeout
func Tick() {
i := gIdler.Load()
if i != nil {
i.Tick()
}
}
// WrapHandler calls Tick() before processing passing request to http.Handler
func WrapHandler(h http.Handler) http.Handler {
if h == nil {
h = http.DefaultServeMux
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Tick()
h.ServeHTTP(w, r)
})
}
// WrapIdlerHandler calls idler.Tick() before processing passing request to http.Handler
func WrapIdlerHandler(i Idler, h http.Handler) http.Handler {
if h == nil {
h = http.DefaultServeMux
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
i.Tick()
h.ServeHTTP(w, r)
})
}
// Idler helps manage idle servers
type Idler interface {
// Tick records the current time. This will make the server not idle until next Tick or timeout
Tick()
// Wait waits till the server is idle and returns. i.e. no Ticks in last <timeout> duration
Wait()
// For long running background jobs, use Enter to record start time. Wait will not return while there are active jobs running
Enter()
// Exit records end of a background job
Exit()
// Get the channel to wait yourself
Chan() <-chan struct{}
}
type idler struct {
lastTick atomic.Pointer[time.Time]
c chan struct{}
active atomic.Int64
}
func (i *idler) Enter() {
i.active.Add(1)
}
func (i *idler) Exit() {
i.Tick()
i.active.Add(-1)
}
// CreateIdler creates an Idler with given timeout
func CreateIdler(timeout time.Duration) Idler {
i := &idler{}
i.c = make(chan struct{})
i.Tick()
go func() {
for {
if i.active.Load() != 0 {
time.Sleep(timeout)
continue
}
t := *i.lastTick.Load()
now := time.Now()
dur := t.Add(timeout).Sub(now)
if dur == dur.Abs() {
time.Sleep(dur)
continue
}
break
}
close(i.c)
}()
return i
}
func (i *idler) Tick() {
now := time.Now()
i.lastTick.Store(&now)
}
func (i *idler) Wait() {
<-i.c
}
func (i *idler) Chan() <-chan struct{} {
return i.c
}

34
idle/idle_test.go Normal file
View File

@@ -0,0 +1,34 @@
package idle
import (
"testing"
"time"
)
func TestIdlerChan(_ *testing.T) {
i := CreateIdler(10 * time.Millisecond)
<-i.Chan()
}
func TestGlobalIdler(t *testing.T) {
err := Wait(10 * time.Millisecond)
if err != nil {
t.Fatalf("idle.Wait failed, %v", err)
}
err = Wait(10 * time.Millisecond)
if err == nil {
t.Fatal("idle.Wait should fail when called second time")
}
}
func TestIdlerEnterExit(t *testing.T) {
i := CreateIdler(10 * time.Millisecond).(*idler)
i.Enter()
if i.active.Load() != 1 {
t.FailNow()
}
i.Exit()
if i.active.Load() != 0 {
t.FailNow()
}
}