From 46ccf5f962dfbf8203c324f8583c9eb3b9c21f6d Mon Sep 17 00:00:00 2001 From: mohanson Date: Tue, 9 Sep 2025 15:08:18 +0800 Subject: [PATCH 1/2] 2025-09-09 15:08:18 --- cmd/daze/main.go | 6 +- daze.go | 8 +- daze_test.go | 2 +- go.mod | 2 - lib/doa/README.md | 7 ++ lib/doa/doa.go | 38 ++++++ lib/gracefulexit/README.md | 3 + lib/gracefulexit/cmd/http/main.go | 27 ++++ lib/gracefulexit/gracefulexit.go | 22 ++++ lib/lru/README.md | 3 + lib/lru/lru.go | 146 ++++++++++++++++++++++ lib/lru/lru_test.go | 74 +++++++++++ lib/pretty/README.md | 44 +++++++ lib/pretty/cmd/progress/main.go | 17 +++ lib/pretty/cmd/table/main.go | 21 ++++ lib/pretty/cmd/tree/main.go | 37 ++++++ lib/pretty/pretty.go | 196 ++++++++++++++++++++++++++++++ lib/priority/README.md | 3 + lib/priority/cmd/race/main.go | 43 +++++++ lib/priority/priority.go | 30 +++++ lib/priority/priority_test.go | 27 ++++ lib/rate/README.md | 3 + lib/rate/cmd/race/main.go | 37 ++++++ lib/rate/rate.go | 91 ++++++++++++++ protocol/ashe/engine.go | 4 +- protocol/ashe/engine_test.go | 2 +- protocol/baboon/engine.go | 4 +- protocol/baboon/engine_test.go | 2 +- protocol/czar/engine.go | 2 +- protocol/czar/engine_test.go | 2 +- protocol/czar/err_test.go | 2 +- protocol/czar/mux.go | 4 +- protocol/czar/mux_test.go | 2 +- protocol/czar/sip.go | 2 +- protocol/czar/sip_test.go | 2 +- protocol/dahlia/engine.go | 2 +- protocol/dahlia/engine_test.go | 2 +- 37 files changed, 893 insertions(+), 26 deletions(-) create mode 100644 lib/doa/README.md create mode 100644 lib/doa/doa.go create mode 100644 lib/gracefulexit/README.md create mode 100644 lib/gracefulexit/cmd/http/main.go create mode 100644 lib/gracefulexit/gracefulexit.go create mode 100644 lib/lru/README.md create mode 100644 lib/lru/lru.go create mode 100644 lib/lru/lru_test.go create mode 100644 lib/pretty/README.md create mode 100644 lib/pretty/cmd/progress/main.go create mode 100644 lib/pretty/cmd/table/main.go create mode 100644 lib/pretty/cmd/tree/main.go create mode 100644 lib/pretty/pretty.go create mode 100644 lib/priority/README.md create mode 100644 lib/priority/cmd/race/main.go create mode 100644 lib/priority/priority.go create mode 100644 lib/priority/priority_test.go create mode 100644 lib/rate/README.md create mode 100644 lib/rate/cmd/race/main.go create mode 100644 lib/rate/rate.go diff --git a/cmd/daze/main.go b/cmd/daze/main.go index 1e981c9..84c9a36 100644 --- a/cmd/daze/main.go +++ b/cmd/daze/main.go @@ -15,13 +15,13 @@ import ( "time" "github.com/libraries/daze" + "github.com/libraries/daze/lib/doa" + "github.com/libraries/daze/lib/gracefulexit" + "github.com/libraries/daze/lib/rate" "github.com/libraries/daze/protocol/ashe" "github.com/libraries/daze/protocol/baboon" "github.com/libraries/daze/protocol/czar" "github.com/libraries/daze/protocol/dahlia" - "github.com/libraries/go/doa" - "github.com/libraries/go/gracefulexit" - "github.com/libraries/go/rate" ) // Conf is acting as package level configuration. diff --git a/daze.go b/daze.go index 3b075df..618d911 100644 --- a/daze.go +++ b/daze.go @@ -27,10 +27,10 @@ import ( "sync" "time" - "github.com/libraries/go/doa" - "github.com/libraries/go/lru" - "github.com/libraries/go/pretty" - "github.com/libraries/go/rate" + "github.com/libraries/daze/lib/doa" + "github.com/libraries/daze/lib/lru" + "github.com/libraries/daze/lib/pretty" + "github.com/libraries/daze/lib/rate" ) // ============================================================================ diff --git a/daze_test.go b/daze_test.go index 608c3db..4438d44 100644 --- a/daze_test.go +++ b/daze_test.go @@ -6,7 +6,7 @@ import ( "os/exec" "testing" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) const ( diff --git a/go.mod b/go.mod index 3fb7e76..bb5a3c0 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/libraries/daze go 1.25.0 - -require github.com/libraries/go v1.0.5 diff --git a/lib/doa/README.md b/lib/doa/README.md new file mode 100644 index 0000000..fa45664 --- /dev/null +++ b/lib/doa/README.md @@ -0,0 +1,7 @@ +# Doa + +Package doa is the abbreviation of the "Dead or alive". It provides some easy ways to make you panic down the program. + +> One of the benefits of detecting problems as soon as you can is that you can crash earlier, and crashing is often the bet thing you can do. The alternative may be to continue, writing corrupted data to some vital database or commanding the washing machine into its twentieth consecutive spin cycle. + +Crash is not poison, it's "Quit gracefully". diff --git a/lib/doa/doa.go b/lib/doa/doa.go new file mode 100644 index 0000000..8e1bef5 --- /dev/null +++ b/lib/doa/doa.go @@ -0,0 +1,38 @@ +// Package doa stands for "dead or alive". It provides simple utilities to intentionally crash the program with a panic. +package doa + +import ( + "log" +) + +// Doa checks a boolean condition and triggers a panic if it’s false. +func Doa(b bool) { + if !b { + log.Panicln("doa: unreachable") + } +} + +// Err returns the error passed to it, ignoring the first argument. +func Err(a any, err error) error { + return err +} + +// Nil checks if an error is non-nil and panics if it is. +func Nil(err error) { + if err != nil { + log.Panicln("doa:", err) + } +} + +// Try returns a value if there’s no error, otherwise it panics. +func Try[T any](a T, err error) T { + if err != nil { + log.Panicln("doa:", err) + } + return a +} + +// Val returns the first argument, ignoring the error. +func Val[T any](a T, err error) T { + return a +} diff --git a/lib/gracefulexit/README.md b/lib/gracefulexit/README.md new file mode 100644 index 0000000..e0a8e00 --- /dev/null +++ b/lib/gracefulexit/README.md @@ -0,0 +1,3 @@ +# Gracefulexit + +Package gracefulexit provides a method to exit the program gracefully. A graceful exit (or graceful handling) is a simple programming idiom[citation needed] wherein a program detects a serious error condition and "exits gracefully" in a controlled manner as a result. Often the program prints a descriptive error message to a terminal or log as part of the graceful exit. diff --git a/lib/gracefulexit/cmd/http/main.go b/lib/gracefulexit/cmd/http/main.go new file mode 100644 index 0000000..3fcb3b6 --- /dev/null +++ b/lib/gracefulexit/cmd/http/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "net" + "net/http" + + "github.com/libraries/daze/lib/gracefulexit" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) + w.Write([]byte("\n")) + }) + log.Println("main: listen and server on 127.0.0.1:8080") + l, err := net.Listen("tcp", "127.0.0.1:8080") + if err != nil { + log.Panicln("main:", err) + } + server := http.Server{} + go server.Serve(l) + gracefulexit.Wait() + log.Println("main: server close") + server.Close() + log.Println("main: done") +} diff --git a/lib/gracefulexit/gracefulexit.go b/lib/gracefulexit/gracefulexit.go new file mode 100644 index 0000000..64e6cf3 --- /dev/null +++ b/lib/gracefulexit/gracefulexit.go @@ -0,0 +1,22 @@ +// Package gracefulexit provides a method to exit the program gracefully. A graceful exit (or graceful handling) is a +// simple programming idiom[citation needed] wherein a program detects a serious error condition and "exits gracefully" +// in a controlled manner as a result. Often the program prints a descriptive error message to a terminal or log as part +// of the graceful exit. +package gracefulexit + +import ( + "os" + "os/signal" +) + +// Chan create a channel for os.Signal. +func Chan() chan os.Signal { + buffer := make(chan os.Signal, 1) + signal.Notify(buffer, os.Interrupt) + return buffer +} + +// Wait for a signal. +func Wait() { + <-Chan() +} diff --git a/lib/lru/README.md b/lib/lru/README.md new file mode 100644 index 0000000..ec400c1 --- /dev/null +++ b/lib/lru/README.md @@ -0,0 +1,3 @@ +# Lru + +Package lru implements an LRU cache. diff --git a/lib/lru/lru.go b/lib/lru/lru.go new file mode 100644 index 0000000..7a28efc --- /dev/null +++ b/lib/lru/lru.go @@ -0,0 +1,146 @@ +// Package lru implements an LRU cache. +package lru + +import ( + "sync" +) + +// Elem is an element of a linked list. +type Elem[K comparable, V any] struct { + Next, Prev *Elem[K, V] + K K + V V +} + +// List represents a doubly linked list. +type List[K comparable, V any] struct { + Root *Elem[K, V] + Size int +} + +// Init initializes or clears list l. +func (l *List[K, V]) Init() *List[K, V] { + root := Elem[K, V]{} + root.Prev = &root + root.Next = &root + l.Root = &root + l.Size = 0 + return l +} + +// Insert inserts e after root, increments l's size, and returns e. +func (l *List[K, V]) Insert(e *Elem[K, V]) *Elem[K, V] { + e.Prev = l.Root + e.Next = l.Root.Next + e.Prev.Next = e + e.Next.Prev = e + l.Size++ + return e +} + +// Remove removes e from its list, decrements l's size. +func (l *List[K, V]) Remove(e *Elem[K, V]) { + e.Prev.Next = e.Next + e.Next.Prev = e.Prev + e.Prev = nil // Avoid memory leaks + e.Next = nil // Avoid memory leaks + l.Size-- +} + +// Update e to next to root. +func (l *List[K, V]) Update(e *Elem[K, V]) { + if l.Root.Next == e { + return + } + e.Prev.Next = e.Next + e.Next.Prev = e.Prev + e.Prev = l.Root + e.Next = l.Root.Next + e.Prev.Next = e + e.Next.Prev = e +} + +// Lru cache. It is safe for concurrent access. +type Lru[K comparable, V any] struct { + // Drop is called automatically when an elem is deleted. + Drop func(k K, v V) + // Size is the maximum number of cache entries before an item is evicted. Zero means no limit. + Size int + List *List[K, V] + C map[K]*Elem[K, V] + M *sync.Mutex +} + +// Del removes the provided key from the cache. +func (l *Lru[K, V]) Del(k K) { + l.M.Lock() + defer l.M.Unlock() + if e, ok := l.C[k]; ok { + l.Drop(k, e.V) + delete(l.C, k) + l.List.Remove(e) + } +} + +// Get looks up a key's value from the cache. +func (l *Lru[K, V]) GetExists(k K) (v V, ok bool) { + l.M.Lock() + defer l.M.Unlock() + var e *Elem[K, V] + e, ok = l.C[k] + if ok { + l.List.Update(e) + v = e.V + } + return +} + +// Get looks up a key's value from the cache. +func (l *Lru[K, V]) Get(k K) (v V) { + v, _ = l.GetExists(k) + return +} + +// Has returns true if a key exists. +func (l *Lru[K, V]) Has(k K) bool { + l.M.Lock() + defer l.M.Unlock() + _, b := l.C[k] + return b +} + +// Len returns the number of items in the cache. +func (l *Lru[K, V]) Len() int { + l.M.Lock() + defer l.M.Unlock() + return l.List.Size +} + +// Set adds a value to the cache. +func (l *Lru[K, V]) Set(k K, v V) { + l.M.Lock() + defer l.M.Unlock() + if e, ok := l.C[k]; ok { + l.List.Update(e) + e.K = k + e.V = v + return + } + if l.List.Size == l.Size { + l.Drop(l.List.Root.Prev.K, l.List.Root.Prev.V) + delete(l.C, l.List.Root.Prev.K) + l.List.Remove(l.List.Root.Prev) + } + l.C[k] = l.List.Insert(&Elem[K, V]{K: k, V: v}) +} + +// New returns a new LRU cache. If size is zero, the cache has no limit. +func New[K comparable, V any](size int) *Lru[K, V] { + return &Lru[K, V]{ + Drop: func(k K, v V) {}, + Size: size, + List: new(List[K, V]).Init(), + C: map[K]*Elem[K, V]{}, + M: &sync.Mutex{}, + } +} diff --git a/lib/lru/lru_test.go b/lib/lru/lru_test.go new file mode 100644 index 0000000..24e6ad1 --- /dev/null +++ b/lib/lru/lru_test.go @@ -0,0 +1,74 @@ +package lru + +import ( + "testing" +) + +func TestLruAppend(t *testing.T) { + c := New[int, int](4) + c.Set(1, 1) + c.Set(2, 2) + c.Set(3, 3) + c.Set(4, 4) + c.Set(5, 5) + if c.Get(1) != 0 { + t.FailNow() + } + if c.Get(5) != 5 { + t.FailNow() + } +} + +func TestLruChange(t *testing.T) { + c := New[int, int](4) + c.Set(1, 1) + c.Set(2, 2) + c.Set(3, 3) + c.Set(4, 4) + c.Set(1, 5) + if c.Get(1) != 5 { + t.FailNow() + } +} + +func TestLruDel(t *testing.T) { + c := New[int, int](4) + c.Set(1, 1) + c.Set(2, 2) + c.Set(3, 3) + c.Set(4, 4) + c.Del(2) + if c.List.Size != c.Len() || c.Len() != 3 { + t.FailNow() + } + if c.Get(2) != 0 { + t.FailNow() + } +} + +func TestLruSize(t *testing.T) { + c := New[int, int](4) + if c.List.Size != c.Len() || c.Len() != 0 { + t.FailNow() + } + c.Set(1, 1) + if c.List.Size != c.Len() || c.Len() != 1 { + t.FailNow() + } + c.Set(2, 2) + if c.List.Size != c.Len() || c.Len() != 2 { + t.FailNow() + } + c.Set(3, 3) + if c.List.Size != c.Len() || c.Len() != 3 { + t.FailNow() + } + c.Set(4, 4) + if c.List.Size != c.Len() || c.Len() != 4 { + t.FailNow() + } + c.Set(5, 5) + if c.List.Size != c.Len() || c.Len() != 4 { + t.FailNow() + } +} diff --git a/lib/pretty/README.md b/lib/pretty/README.md new file mode 100644 index 0000000..bf94654 --- /dev/null +++ b/lib/pretty/README.md @@ -0,0 +1,44 @@ +# Pretty + +Package pretty provides utilities for beautifying console output. + +**Progress** + +```sh +$ go run cmd/progress/main.go + +2025/03/12 09:53:42 pretty: [=========================> ] 59% +``` + +**Table** + +```sh +$ go run cmd/table/main.go + +2025/09/09 10:34:44 pretty: City name Area Population Annual Rainfall +2025/09/09 10:34:44 pretty: ----------------------------------------- +2025/09/09 10:34:44 pretty: Adelaide 1295 1158259 600.5 +2025/09/09 10:34:44 pretty: Brisbane 5905 1857594 1146.4 +2025/09/09 10:34:44 pretty: Darwin 112 120900 1714.7 +2025/09/09 10:34:44 pretty: Hobart 1357 205556 619.5 +2025/09/09 10:34:44 pretty: Melbourne 1566 3806092 646.9 +2025/09/09 10:34:44 pretty: Perth 5386 1554769 869.4 +2025/09/09 10:34:44 pretty: Sydney 2058 4336374 1214.8 +``` + +**Tree** + +```sh +$ go run cmd/tree/main.go + +2025/09/08 16:39:26 pretty: . +2025/09/08 16:39:26 pretty: ├── README.md +2025/09/08 16:39:26 pretty: ├── cmd +2025/09/08 16:39:26 pretty: │ ├── progress +2025/09/08 16:39:26 pretty: │ │ └── main.go +2025/09/08 16:39:26 pretty: │ ├── table +2025/09/08 16:39:26 pretty: │ │ └── main.go +2025/09/08 16:39:26 pretty: │ └── tree +2025/09/08 16:39:26 pretty: │ └── main.go +2025/09/08 16:39:26 pretty: └── pretty.go +``` diff --git a/lib/pretty/cmd/progress/main.go b/lib/pretty/cmd/progress/main.go new file mode 100644 index 0000000..bb4988e --- /dev/null +++ b/lib/pretty/cmd/progress/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "time" + + "github.com/libraries/daze/lib/pretty" +) + +func main() { + progress := pretty.NewProgress() + progress.Print(0) + for i := range 1024 { + time.Sleep(time.Millisecond * 4) + progress.Print(float64(i+1) / 1024) + } + progress.Print(1) +} diff --git a/lib/pretty/cmd/table/main.go b/lib/pretty/cmd/table/main.go new file mode 100644 index 0000000..7bfc234 --- /dev/null +++ b/lib/pretty/cmd/table/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/libraries/daze/lib/pretty" +) + +func main() { + table := pretty.NewTable() + table.Head = []string{"City name", "Area", "Population", "Annual Rainfall"} + table.Conf = []string{"<", ">", ">", ">"} + table.Body = [][]string{ + {"Adelaide", "1295", "1158259", "600.5"}, + {"Brisbane", "5905", "1857594", "1146.4"}, + {"Darwin", "112", "120900", "1714.7"}, + {"Hobart", "1357", "205556", "619.5"}, + {"Melbourne", "1566", "3806092", "646.9"}, + {"Perth", "5386", "1554769", "869.4"}, + {"Sydney", "2058", "4336374", "1214.8"}, + } + table.Print() +} diff --git a/lib/pretty/cmd/tree/main.go b/lib/pretty/cmd/tree/main.go new file mode 100644 index 0000000..ff84c6d --- /dev/null +++ b/lib/pretty/cmd/tree/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "log" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/libraries/daze/lib/pretty" +) + +func walk(path string) *pretty.Tree { + info, err := os.Stat(path) + if err != nil { + log.Panicln("main:", err) + } + node := pretty.NewTree(info.Name()) + if info.IsDir() { + l, err := os.ReadDir(path) + if err != nil { + log.Panicln("main:", err) + } + for _, e := range l { + node.Leaf = append(node.Leaf, walk(filepath.Join(path, e.Name()))) + } + // Sort the elements alphabetically for consistent output. + slices.SortFunc(node.Leaf, func(a, b *pretty.Tree) int { + return strings.Compare(a.Name, b.Name) + }) + } + return node +} + +func main() { + walk(".").Print() +} diff --git a/lib/pretty/pretty.go b/lib/pretty/pretty.go new file mode 100644 index 0000000..a4685f4 --- /dev/null +++ b/lib/pretty/pretty.go @@ -0,0 +1,196 @@ +// Package pretty provides utilities for beautifying console output. +package pretty + +import ( + "fmt" + "log" + "os" + "slices" + "strings" +) + +// Progress represents a progress bar in the terminal. +type Progress struct { + chardev bool + current float64 +} + +// Update updates the progress bar to the specified percent (0 to 1). +func (p *Progress) Print(percent float64) { + if percent > 1 { + log.Panicln("pretty: the percent cannot be greater than 1") + } + if percent < p.current { + log.Panicln("pretty: the percent cannot be decreased") + } + if percent != 0 && percent != 1 && percent-p.current < 0.01 { + // Only update if the change is significant to avoid flickering. + return + } + if percent == 1 && percent == p.current { + // No need to update if already at 100%. + return + } + if percent == 0 && p.chardev { + // Save cursor position. + log.Writer().Write([]byte{0x1b, 0x37}) + } + if percent != 0 && p.chardev { + // Load cursor position. + log.Writer().Write([]byte{0x1b, 0x38}) + } + p.current = percent + cap := int(percent * 44) + buf := []byte("[ ] 000%") + for i := 1; i < cap+1; i++ { + buf[i] = '=' + } + buf[1+cap] = '>' + num := fmt.Sprintf("%3d", int(percent*100)) + buf[48] = num[0] + buf[49] = num[1] + buf[50] = num[2] + log.Println("pretty:", string(buf)) +} + +// NewProgress creates a new Progress instance. +func NewProgress() *Progress { + s, err := os.Stdout.Stat() + if err != nil { + log.Panicln("pretty: cannot stat stdout:", err) + } + return &Progress{ + // Identify if we are displaying to a terminal or through a pipe or redirect. + chardev: s.Mode()&os.ModeCharDevice == os.ModeCharDevice, + current: 0, + } +} + +// ProgressWriter is an io.Writer that updates a progress bar as data is written. +type ProgressWriter struct { + p *Progress + m int64 + n int64 +} + +// Write writes data to the ProgressWriter and updates the progress bar. +func (p *ProgressWriter) Write(b []byte) (int, error) { + l := len(b) + p.m += int64(l) + p.p.Print(float64(p.m) / float64(p.n)) + return l, nil +} + +// NewProgressWriter creates a new ProgressWriter for a task of the given size. +// +// For example, to display progress while reading from a reader: +// +// reader := io.TeeReader(io.LimitReader(os.Stdin, 1024), NewProgressWriter(1024)) +// +// Or to display progress while writing to a writer: +// +// writer := io.MultiWriter(os.Stdout, NewProgressWriter(1024)) +func NewProgressWriter(n int64) *ProgressWriter { + p := NewProgress() + p.Print(0) + return &ProgressWriter{ + p: p, + m: 0, + n: n, + } +} + +// Table represents a table structure with a head and body. +type Table struct { + // Conf specifies the alignment for each column: "<" for left, ">" for right. + // If conf has fewer entries than head, the remaining columns default to left alignment ("<"). + Conf []string + // Head represents the head of the table. + Head []string + // Body represents the body of the table. + Body [][]string +} + +// Print prints the table to the console with proper alignment. +func (t *Table) Print() { + conf := slices.Clone(t.Conf) + for range len(t.Head) - len(conf) { + conf = append(conf, "<") + } + size := make([]int, len(t.Head)) + for i, c := range t.Head { + size[i] = len(c) + } + for _, r := range t.Body { + for i, c := range r { + size[i] = max(size[i], len(c)) + } + } + line := make([]string, len(t.Head)) + for i, c := range t.Head { + l := size[i] + switch conf[i] { + case "<": + line[i] = c + strings.Repeat(" ", l-len(c)) + case ">": + line[i] = strings.Repeat(" ", l-len(c)) + c + } + } + log.Println("pretty:", strings.Join(line, " ")) + for i, n := range size { + line[i] = strings.Repeat("-", n) + } + log.Println("pretty:", strings.Join(line, "-")) + for _, r := range t.Body { + for i, c := range r { + l := size[i] + switch conf[i] { + case "<": + line[i] = c + strings.Repeat(" ", l-len(c)) + case ">": + line[i] = strings.Repeat(" ", l-len(c)) + c + } + } + log.Println("pretty:", strings.Join(line, " ")) + } +} + +// NewTable creates a new Table instance. +func NewTable() *Table { + return &Table{} +} + +// Tree represents a node in a tree structure. +type Tree struct { + Name string + Leaf []*Tree +} + +func (t *Tree) print(prefix string) { + for i, elem := range t.Leaf { + isLast := i == len(t.Leaf)-1 + branch := "├── " + if isLast { + branch = "└── " + } + log.Println("pretty:", prefix+branch+elem.Name) + if len(elem.Leaf) > 0 { + middle := "│ " + if isLast { + middle = " " + } + elem.print(prefix + middle) + } + } +} + +// Print prints the tree structure starting from the root node. +func (t *Tree) Print() { + log.Println("pretty:", t.Name) + t.print("") +} + +// NewTree creates a new Tree node with the given name. +func NewTree(name string) *Tree { + return &Tree{Name: name} +} diff --git a/lib/priority/README.md b/lib/priority/README.md new file mode 100644 index 0000000..2895c1e --- /dev/null +++ b/lib/priority/README.md @@ -0,0 +1,3 @@ +# Priority + +Package priority implements a priority mutex. diff --git a/lib/priority/cmd/race/main.go b/lib/priority/cmd/race/main.go new file mode 100644 index 0000000..5daf660 --- /dev/null +++ b/lib/priority/cmd/race/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "log" + "time" + + "github.com/libraries/daze/lib/priority" +) + +const ( + cGo = 4 + cPriorityLevels = 3 + cTick = time.Millisecond +) + +func main() { + pri := priority.NewPriority(cPriorityLevels) + ret := make([]uint64, cPriorityLevels) + ctx, fin := context.WithTimeout(context.Background(), time.Second*4) + for i := range cPriorityLevels * cGo { + p := i % cPriorityLevels + go func() { + for { + select { + case <-ctx.Done(): + return + default: + pri.Pri(p, func() error { + ret[p] += 1 + time.Sleep(cTick) + return nil + }) + } + } + }() + } + <-ctx.Done() + fin() + for i := range cPriorityLevels { + log.Printf("main: report level=%d count=%d", i, ret[i]) + } +} diff --git a/lib/priority/priority.go b/lib/priority/priority.go new file mode 100644 index 0000000..34db656 --- /dev/null +++ b/lib/priority/priority.go @@ -0,0 +1,30 @@ +// Package priority implements a priority mutex. +package priority + +import ( + "sync" +) + +// Priority implement a lock with priorities. +type Priority struct { + l []sync.Mutex +} + +// Call the function f with priority. +func (p *Priority) Pri(n int, f func() error) error { + for i := n; i >= 0; i-- { + p.l[i].Lock() + } + err := f() + for i := 0; i <= n; i++ { + p.l[i].Unlock() + } + return err +} + +// NewPriority returns a new Priority with n priority levels. +func NewPriority(n int) *Priority { + return &Priority{ + l: make([]sync.Mutex, n), + } +} diff --git a/lib/priority/priority_test.go b/lib/priority/priority_test.go new file mode 100644 index 0000000..0d83f36 --- /dev/null +++ b/lib/priority/priority_test.go @@ -0,0 +1,27 @@ +package priority + +import ( + "testing" +) + +func BenchmarkPriority(b *testing.B) { + pri := NewPriority(3) + for b.Loop() { + pri.Pri(2, func() error { + return nil + }) + } +} + +func TestPriority(t *testing.T) { + pri := NewPriority(3) + pri.Pri(0, func() error { + return nil + }) + pri.Pri(1, func() error { + return nil + }) + pri.Pri(2, func() error { + return nil + }) +} diff --git a/lib/rate/README.md b/lib/rate/README.md new file mode 100644 index 0000000..a2157b1 --- /dev/null +++ b/lib/rate/README.md @@ -0,0 +1,3 @@ +# Rate + +Package rate provides a rate limiter. It implements a classic token bucket algorithm, which can achieve functions such as http api speed limit and network bandwidth speed limit. diff --git a/lib/rate/cmd/race/main.go b/lib/rate/cmd/race/main.go new file mode 100644 index 0000000..1615f84 --- /dev/null +++ b/lib/rate/cmd/race/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "math/rand/v2" + "time" + + "github.com/libraries/daze/lib/rate" +) + +const ( + cGo = 4 + cLimNum = 128 + cLimPer = time.Millisecond + cTx = time.Second * 4 +) + +func main() { + lim := rate.NewLimits(cLimNum, cLimPer) + ctx, fin := context.WithTimeout(context.Background(), cTx) + for range cGo { + go func() { + for { + select { + case <-ctx.Done(): + return + default: + maxn := uint64(cLimNum) * 2 + step := rand.Uint64() % maxn + lim.Wait(step) + } + } + }() + } + <-ctx.Done() + fin() +} diff --git a/lib/rate/rate.go b/lib/rate/rate.go new file mode 100644 index 0000000..19f5e80 --- /dev/null +++ b/lib/rate/rate.go @@ -0,0 +1,91 @@ +// Package rate provides a rate limiter. It implements a classic token bucket algorithm, which can achieve functions +// such as http api speed limit and network bandwidth speed limit. +package rate + +import ( + "log" + "math" + "sync" + "time" +) + +// Limits represents a rate limiter that controls resource allocation over time. +type Limits struct { + addition uint64 + capacity uint64 + last time.Time + mu sync.Mutex + size uint64 + step time.Duration +} + +// Peek glances there are enough resources (n) available. +func (l *Limits) Peek(n uint64) bool { + l.mu.Lock() + defer l.mu.Unlock() + cycles := uint64(time.Since(l.last) / l.step) + if cycles > 0 { + l.last = l.last.Add(l.step * time.Duration(cycles)) + if cycles > math.MaxUint64/l.addition { + log.Panicln("rate: overflow") + } + if l.size > math.MaxUint64-l.addition*cycles { + log.Panicln("rate: overflow") + } + l.size = l.size + l.addition*cycles + l.size = min(l.size, l.capacity) + } + return l.size >= n +} + +// Wait ensures there are enough resources (n) available, blocking if necessary. +func (l *Limits) Wait(n uint64) { + l.mu.Lock() + defer l.mu.Unlock() + cycles := uint64(time.Since(l.last) / l.step) + if cycles > 0 { + l.last = l.last.Add(l.step * time.Duration(cycles)) + if cycles > math.MaxUint64/l.addition { + log.Panicln("rate: overflow") + } + if l.size > math.MaxUint64-l.addition*cycles { + log.Panicln("rate: overflow") + } + l.size = l.size + l.addition*cycles + l.size = min(l.size, l.capacity) + } + if l.size < n { + cycles = (n - l.size + l.addition - 1) / l.addition + time.Sleep(l.step * time.Duration(cycles)) + l.last = l.last.Add(l.step * time.Duration(cycles)) + l.size = l.size + l.addition*cycles + } + l.size -= n +} + +// NewLimits creates a new rate limiter with rate r over period p. +// +// Overflow warning: +// If the rate r is set to a very large value (e.g., 1G = 1024 * 1024 * 1024) and the period (p) is one second, the +// internal counters may overflow after approximately 544 years. However, this overflow only occurs if the limiter +// remains completely idle (i.e., neither peek nor wait is called) for the entire duration. Consider this when designing +// long-running systems with very high rates. +func NewLimits(r uint64, p time.Duration) *Limits { + gcd := func(a, b uint64) uint64 { + t := uint64(0) + for b != 0 { + t = b + b = a % b + a = t + } + return a + }(r, uint64(p)) + return &Limits{ + addition: r / gcd, + capacity: r, + last: time.Now(), + mu: sync.Mutex{}, + size: r, + step: p / time.Duration(gcd), + } +} diff --git a/protocol/ashe/engine.go b/protocol/ashe/engine.go index d5d393d..d504724 100644 --- a/protocol/ashe/engine.go +++ b/protocol/ashe/engine.go @@ -11,8 +11,8 @@ import ( "time" "github.com/libraries/daze" - "github.com/libraries/go/doa" - "github.com/libraries/go/rate" + "github.com/libraries/daze/lib/doa" + "github.com/libraries/daze/lib/rate" ) // This document describes a tcp-based cryptographic proxy protocol. The main purpose of this protocol is to bypass diff --git a/protocol/ashe/engine_test.go b/protocol/ashe/engine_test.go index 2911b73..8b17238 100644 --- a/protocol/ashe/engine_test.go +++ b/protocol/ashe/engine_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/libraries/daze" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) const ( diff --git a/protocol/baboon/engine.go b/protocol/baboon/engine.go index f10ff4c..72d0fd0 100644 --- a/protocol/baboon/engine.go +++ b/protocol/baboon/engine.go @@ -13,9 +13,9 @@ import ( "time" "github.com/libraries/daze" + "github.com/libraries/daze/lib/doa" + "github.com/libraries/daze/lib/rate" "github.com/libraries/daze/protocol/ashe" - "github.com/libraries/go/doa" - "github.com/libraries/go/rate" ) // Protocol baboon is the ashe protocol based on http. diff --git a/protocol/baboon/engine_test.go b/protocol/baboon/engine_test.go index 7002624..6d7cf45 100644 --- a/protocol/baboon/engine_test.go +++ b/protocol/baboon/engine_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/libraries/daze" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) const ( diff --git a/protocol/czar/engine.go b/protocol/czar/engine.go index 73e7d1d..557a52b 100644 --- a/protocol/czar/engine.go +++ b/protocol/czar/engine.go @@ -10,8 +10,8 @@ import ( "time" "github.com/libraries/daze" + "github.com/libraries/daze/lib/rate" "github.com/libraries/daze/protocol/ashe" - "github.com/libraries/go/rate" ) // The czar protocol is a proxy protocol built on tcp multiplexing technology. By establishing multiple tcp connections diff --git a/protocol/czar/engine_test.go b/protocol/czar/engine_test.go index 5535dc9..f8f66b0 100644 --- a/protocol/czar/engine_test.go +++ b/protocol/czar/engine_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/libraries/daze" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) const ( diff --git a/protocol/czar/err_test.go b/protocol/czar/err_test.go index 7529731..3ecff31 100644 --- a/protocol/czar/err_test.go +++ b/protocol/czar/err_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) func TestProtocolCzarErr(t *testing.T) { diff --git a/protocol/czar/mux.go b/protocol/czar/mux.go index 3d5ce5c..54d09de 100644 --- a/protocol/czar/mux.go +++ b/protocol/czar/mux.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/libraries/go/doa" - "github.com/libraries/go/priority" + "github.com/libraries/daze/lib/doa" + "github.com/libraries/daze/lib/priority" ) // A Stream managed by the multiplexer. diff --git a/protocol/czar/mux_test.go b/protocol/czar/mux_test.go index e19d6d3..e14d9b3 100644 --- a/protocol/czar/mux_test.go +++ b/protocol/czar/mux_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/libraries/daze" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) func TestProtocolCzarMux(t *testing.T) { diff --git a/protocol/czar/sip.go b/protocol/czar/sip.go index 0acc598..3796d87 100644 --- a/protocol/czar/sip.go +++ b/protocol/czar/sip.go @@ -5,7 +5,7 @@ import ( "math/big" "sync" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) // A stream id generator. Stream id can be reused, and the smallest available stream id is guaranteed to be generated diff --git a/protocol/czar/sip_test.go b/protocol/czar/sip_test.go index fe92494..e14bf04 100644 --- a/protocol/czar/sip_test.go +++ b/protocol/czar/sip_test.go @@ -3,7 +3,7 @@ package czar import ( "testing" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) func TestProtocolCzarSip(t *testing.T) { diff --git a/protocol/dahlia/engine.go b/protocol/dahlia/engine.go index 902964a..defa571 100644 --- a/protocol/dahlia/engine.go +++ b/protocol/dahlia/engine.go @@ -9,8 +9,8 @@ import ( "time" "github.com/libraries/daze" + "github.com/libraries/daze/lib/rate" "github.com/libraries/daze/protocol/ashe" - "github.com/libraries/go/rate" ) // Dahlia is an encrypted port forwarding protocol. Unlike common port forwarding tools, it needs to configure a server diff --git a/protocol/dahlia/engine_test.go b/protocol/dahlia/engine_test.go index cf5d62c..ce6590d 100644 --- a/protocol/dahlia/engine_test.go +++ b/protocol/dahlia/engine_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/libraries/daze" - "github.com/libraries/go/doa" + "github.com/libraries/daze/lib/doa" ) const ( From f1ad8315fa7657dfa5ab2acc3c2589d1d6f8f359 Mon Sep 17 00:00:00 2001 From: mohanson Date: Tue, 9 Sep 2025 15:40:44 +0800 Subject: [PATCH 2/2] 2025-09-09 15:40:44 --- daze.go | 2 +- lib/pretty/pretty.go | 8 ++++---- lib/rate/rate.go | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/daze.go b/daze.go index 618d911..518e4bf 100644 --- a/daze.go +++ b/daze.go @@ -1209,7 +1209,7 @@ func LoadApnic() map[string][]*net.IPNet { log.Println("main: load apnic data from", url) rep := doa.Try(http.Get(url)) defer rep.Body.Close() - f := io.TeeReader(rep.Body, pretty.NewProgressWriter(rep.ContentLength)) + f := io.TeeReader(rep.Body, pretty.NewProgressWriter(uint64(rep.ContentLength))) r := map[string][]*net.IPNet{} s := bufio.NewScanner(f) for s.Scan() { diff --git a/lib/pretty/pretty.go b/lib/pretty/pretty.go index a4685f4..ae5d2c2 100644 --- a/lib/pretty/pretty.go +++ b/lib/pretty/pretty.go @@ -69,14 +69,14 @@ func NewProgress() *Progress { // ProgressWriter is an io.Writer that updates a progress bar as data is written. type ProgressWriter struct { p *Progress - m int64 - n int64 + m uint64 + n uint64 } // Write writes data to the ProgressWriter and updates the progress bar. func (p *ProgressWriter) Write(b []byte) (int, error) { l := len(b) - p.m += int64(l) + p.m += uint64(l) p.p.Print(float64(p.m) / float64(p.n)) return l, nil } @@ -90,7 +90,7 @@ func (p *ProgressWriter) Write(b []byte) (int, error) { // Or to display progress while writing to a writer: // // writer := io.MultiWriter(os.Stdout, NewProgressWriter(1024)) -func NewProgressWriter(n int64) *ProgressWriter { +func NewProgressWriter(n uint64) *ProgressWriter { p := NewProgress() p.Print(0) return &ProgressWriter{ diff --git a/lib/rate/rate.go b/lib/rate/rate.go index 19f5e80..8ef035a 100644 --- a/lib/rate/rate.go +++ b/lib/rate/rate.go @@ -89,3 +89,27 @@ func NewLimits(r uint64, p time.Duration) *Limits { step: p / time.Duration(gcd), } } + +// LimitsWriter is an io.Writer that applies rate limiting to write operations. +// +// For example, to limit a reader's read speed to 1MB/s: +// +// reader := io.TeeReader(os.Stdin, NewLimitsWriter(1024*1024, time.Second)) +// +// Or, to limit a writer's write speed to 1MB/s: +// +// writer := io.MultiWriter(os.Stdout, NewLimitsWriter(1024*1024, time.Second)) +type LimitsWriter struct { + li *Limits +} + +// Write writes data to the underlying writer, applying rate limiting based on the configured limits. +func (l *LimitsWriter) Write(p []byte) (int, error) { + l.li.Wait(uint64(len(p))) + return len(p), nil +} + +// NewLimitsWriter creates a new LimitsWriter that limits write operations to r bytes per period p. +func NewLimitsWriter(limits *Limits) *LimitsWriter { + return &LimitsWriter{li: limits} +}