ZRY 1 month ago
parent
commit
3498823866
9 changed files with 428 additions and 0 deletions
  1. 8 0
      .idea/.gitignore
  2. 9 0
      .idea/go-int32-handle.iml
  3. 7 0
      .idea/misc.xml
  4. 8 0
      .idea/modules.xml
  5. 6 0
      .idea/vcs.xml
  6. 5 0
      go.mod
  7. 2 0
      go.sum
  8. 113 0
      handle.go
  9. 270 0
      handle_test.go

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 9 - 0
.idea/go-int32-handle.iml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="Go" enabled="true" />
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="XMakeProjectSettings">
+    <option name="currentArchitecture" value="x86" />
+    <option name="workingDirectory" value="$PROJECT_DIR$" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/go-int32-handle.iml" filepath="$PROJECT_DIR$/.idea/go-int32-handle.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module git.swzry.com/zry/go-int32-handle
+
+go 1.20
+
+require github.com/gammazero/deque v0.2.1

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
+github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=

+ 113 - 0
handle.go

@@ -0,0 +1,113 @@
+package go_int32_handle
+
+import (
+	"fmt"
+	"github.com/gammazero/deque"
+	"sync"
+)
+
+type Payload interface {
+}
+
+var ErrHandleExceedMax = fmt.Errorf("handle exceed max")
+
+type HandleManager[T Payload] struct {
+	handles         map[int32]T
+	lock            sync.RWMutex
+	counter         int32
+	releasedHandles *deque.Deque[int32]
+	max             int32
+}
+
+// New returns a new handle manager, same as NewWithRange(0, 2147483647)
+func New[T Payload]() *HandleManager[T] {
+	return &HandleManager[T]{
+		handles:         make(map[int32]T),
+		releasedHandles: deque.New[int32](),
+		counter:         0,
+		lock:            sync.RWMutex{},
+		max:             2147483647,
+	}
+}
+
+// NewWithRange returns a new handle manager.
+// `shouldAbove` is the first handle - 1. this should be greater than 0 or equal to 0.
+// `max` is the last handle. this should be greater than 0.
+func NewWithRange[T Payload](shouldAbove int32, max int32) *HandleManager[T] {
+	return &HandleManager[T]{
+		handles:         make(map[int32]T),
+		releasedHandles: deque.New[int32](),
+		counter:         shouldAbove,
+		lock:            sync.RWMutex{},
+		max:             max,
+	}
+}
+
+func (hm *HandleManager[T]) getNewHandle() int32 {
+	if hm.releasedHandles.Len() > 0 {
+		return hm.releasedHandles.PopFront()
+	} else {
+		hm.counter = hm.counter + 1
+		return hm.counter
+	}
+}
+
+func (hm *HandleManager[T]) AllocateAndPut(value T) (int32, error) {
+	hm.lock.Lock()
+	defer hm.lock.Unlock()
+	h := hm.getNewHandle()
+	if h >= hm.max {
+		return 0, ErrHandleExceedMax
+	}
+	_, ok := hm.handles[h]
+	if ok {
+		return 0, fmt.Errorf("internal error: failed to allocate handle: next val %d is already in use", h)
+	}
+	hm.handles[h] = value
+	return h, nil
+}
+
+func (hm *HandleManager[T]) Get(h int32) (T, bool) {
+	hm.lock.RLock()
+	defer hm.lock.RUnlock()
+	if h <= 0 {
+		var emptyT T
+		return emptyT, false
+	}
+	value, ok := hm.handles[h]
+	return value, ok
+}
+
+func (hm *HandleManager[T]) Release(h int32) (T, bool) {
+	hm.lock.Lock()
+	defer hm.lock.Unlock()
+	if h <= 0 {
+		var emptyT T
+		return emptyT, false
+	}
+	value, ok := hm.handles[h]
+	if ok {
+		delete(hm.handles, h)
+		hm.releasedHandles.PushBack(h)
+	}
+	return value, ok
+}
+
+func (hm *HandleManager[T]) ReleaseSilently(h int32) {
+	hm.lock.Lock()
+	defer hm.lock.Unlock()
+	if h <= 0 {
+		return
+	}
+	_, ok := hm.handles[h]
+	if ok {
+		delete(hm.handles, h)
+		hm.releasedHandles.PushBack(h)
+	}
+}
+
+func (hm *HandleManager[T]) Count() int {
+	hm.lock.RLock()
+	defer hm.lock.RUnlock()
+	return len(hm.handles)
+}

+ 270 - 0
handle_test.go

@@ -0,0 +1,270 @@
+package go_int32_handle
+
+import (
+	"fmt"
+	"math/rand"
+	"runtime"
+	"sync"
+	"testing"
+)
+
+func TestFunc1(t *testing.T) {
+	hm := New[string]()
+	h1 := testUtilAlloc(hm, t, "hello")
+	h2 := testUtilAlloc(hm, t, "world")
+	testUtilGet(hm, t, h1, "hello")
+	testUtilGet(hm, t, h2, "world")
+	testUtilRelease(hm, t, h1, "hello")
+	h3 := testUtilAlloc(hm, t, "gensoukyo")
+	testUtilGet(hm, t, h3, "gensoukyo")
+	testUtilRelease(hm, t, h2, "world")
+	testUtilRelease(hm, t, h3, "gensoukyo")
+	h4 := testUtilAlloc(hm, t, "satori")
+	h5 := testUtilAlloc(hm, t, "koishi")
+	h6 := testUtilAlloc(hm, t, "cirno")
+	testUtilGet(hm, t, h4, "satori")
+	testUtilGet(hm, t, h5, "koishi")
+	testUtilGet(hm, t, h6, "cirno")
+}
+
+func TestFunc2(t *testing.T) {
+	hm := NewWithRange[string](0, 32)
+	var i int32
+	for i = 0; i < 31; i++ {
+		testUtilAlloc(hm, t, fmt.Sprintf("val-%d", i))
+	}
+	for i = 0; i < 31; i++ {
+		testUtilGet(hm, t, i+1, fmt.Sprintf("val-%d", i))
+	}
+	testUtilAllocOverflow(hm, t, "val-32")
+	testUtilRelease(hm, t, 15, "val-14")
+	hr := testUtilAlloc(hm, t, "val-xr-14")
+	if hr != 15 {
+		t.Fatalf("allocate behavior mismatch: expected 15, got %d", hr)
+	}
+	testUtilGet(hm, t, hr, "val-xr-14")
+}
+
+func TestFunc3(t *testing.T) {
+	tryCount := 65536
+	hm := New[string]()
+	tlist := make([]struct {
+		h int32
+		v string
+	}, 0, tryCount)
+	t.Log("==== Do Random Alloc and Release ====")
+	for i := 0; i < tryCount; i++ {
+		ri := rand.Int31()
+		rk := rand.Intn(10)
+		if rk == 0 && len(tlist) > 0 {
+			sel := rand.Intn(len(tlist))
+			testUtilReleaseSilent(hm, t, tlist[sel].h, tlist[sel].v)
+			tlist = append(tlist[:sel], tlist[sel+1:]...)
+		} else {
+			v := fmt.Sprintf("val-test-%d", ri)
+			h := testUtilAllocSilent(hm, t, v)
+			tlist = append(tlist, struct {
+				h int32
+				v string
+			}{h: h, v: v})
+		}
+	}
+	t.Log("==== Do Verification ====")
+	for i := 0; i < len(tlist); i++ {
+		testUtilGetSilent(hm, t, tlist[i].h, tlist[i].v)
+	}
+}
+
+func TestFunc4(t *testing.T) {
+	eachRoutineTryCount := 65536
+	routineCount := runtime.NumCPU()
+	hm := New[string]()
+	wg := sync.WaitGroup{}
+	wg.Add(routineCount)
+	t.Logf("routineCount: %d", routineCount)
+	t.Log("==== Do Parallel Random Alloc and Release Test ====")
+	for i := 0; i < routineCount; i++ {
+		go func() {
+			testUtilDoRandom(hm, eachRoutineTryCount)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	t.Log("==== Done ====")
+}
+
+func BenchmarkAlloc(b *testing.B) {
+	hm := New[string]()
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			_, _ = hm.AllocateAndPut("hello")
+		}
+	})
+}
+
+func BenchmarkGet(b *testing.B) {
+	maxCount := 65536
+	hm := New[string]()
+	for i := 0; i < maxCount; i++ {
+		_, _ = hm.AllocateAndPut(fmt.Sprintf("val-%d", i))
+	}
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			_, _ = hm.Get(int32(rand.Intn(maxCount)))
+		}
+	})
+}
+
+func BenchmarkComprehensive(b *testing.B) {
+	hm := New[string]()
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			op := rand.Intn(3)
+			switch op {
+			case 0:
+				_, _ = hm.AllocateAndPut("hello")
+			case 1:
+				if hm.Count() <= 0 {
+					_, _ = hm.AllocateAndPut("hello")
+				} else {
+					_, _ = hm.Get(int32(rand.Intn(hm.Count())))
+				}
+			case 2:
+				if hm.Count() <= 0 {
+					_, _ = hm.AllocateAndPut("hello")
+				} else {
+					hm.ReleaseSilently(int32(rand.Intn(hm.Count())))
+				}
+			}
+		}
+	})
+}
+
+func testUtilDoRandom(hm *HandleManager[string], tryCount int) {
+	tlist := make([]struct {
+		h int32
+		v string
+	}, 0, tryCount)
+	for i := 0; i < tryCount; i++ {
+		ri := rand.Int31()
+		rk := rand.Intn(10)
+		if rk == 0 && len(tlist) > 0 {
+			sel := rand.Intn(len(tlist))
+			h := tlist[sel].h
+			v := tlist[sel].v
+			releasedVal, ok := hm.Release(h)
+			if !ok {
+				panic(fmt.Errorf("failed to release handle %d: not found", h))
+			}
+			if releasedVal != v {
+				panic(fmt.Errorf("release handle %d result mismatched: expected %s, got %s", h, v, releasedVal))
+			}
+			tlist = append(tlist[:sel], tlist[sel+1:]...)
+		} else {
+			v := fmt.Sprintf("val-test-%d", ri)
+			h, err := hm.AllocateAndPut(v)
+			if err != nil {
+				panic(fmt.Errorf("failed to allocate handle: %v", err))
+			}
+			tlist = append(tlist, struct {
+				h int32
+				v string
+			}{h: h, v: v})
+		}
+	}
+	for i := 0; i < len(tlist); i++ {
+		h := tlist[i].h
+		v := tlist[i].v
+		vr, ok := hm.Get(h)
+		if !ok {
+			panic(fmt.Errorf("failed to get handle %d: not found", h))
+			return
+		}
+		if vr != v {
+			panic(fmt.Errorf("get handle %d result mismatched: expected %s, got %s", h, v, vr))
+			return
+		}
+	}
+}
+
+func testUtilAlloc(hm *HandleManager[string], t *testing.T, val string) int32 {
+	h, err := hm.AllocateAndPut(val)
+	if err != nil {
+		t.Fatalf("failed to allocate handle: %v", err)
+		return 0
+	}
+	t.Logf("allocated handle %d", h)
+	return h
+}
+
+func testUtilAllocSilent(hm *HandleManager[string], t *testing.T, val string) int32 {
+	h, err := hm.AllocateAndPut(val)
+	if err != nil {
+		t.Fatalf("failed to allocate handle: %v", err)
+		return 0
+	}
+	return h
+}
+
+func testUtilAllocOverflow(hm *HandleManager[string], t *testing.T, val string) {
+	_, err := hm.AllocateAndPut(val)
+	if err == ErrHandleExceedMax {
+		t.Log("allocated a overflowed handle got expected error")
+		return
+	} else {
+		t.Fatalf("allocated a overflowed handle got unexpected error: %v", err)
+		return
+	}
+}
+
+func testUtilGet(hm *HandleManager[string], t *testing.T, h int32, expected string) {
+	v, ok := hm.Get(h)
+	if !ok {
+		t.Fatalf("failed to get handle %d: not found", h)
+		return
+	}
+	if v != expected {
+		t.Fatalf("get handle %d result mismatched: expected %s, got %s", h, expected, v)
+		return
+	}
+	t.Logf("get handle %d: %v", h, v)
+}
+
+func testUtilGetSilent(hm *HandleManager[string], t *testing.T, h int32, expected string) {
+	v, ok := hm.Get(h)
+	if !ok {
+		t.Fatalf("failed to get handle %d: not found", h)
+		return
+	}
+	if v != expected {
+		t.Fatalf("get handle %d result mismatched: expected %s, got %s", h, expected, v)
+		return
+	}
+}
+
+func testUtilRelease(hm *HandleManager[string], t *testing.T, h int32, expected string) {
+	releasedVal, ok := hm.Release(h)
+	if !ok {
+		t.Fatalf("failed to release handle %d: not found", h)
+		return
+	}
+	if releasedVal != expected {
+		t.Fatalf("release handle %d result mismatched: expected %s, got %s", h, expected, releasedVal)
+		return
+	}
+	t.Logf("release handle %d ok", h)
+}
+func testUtilReleaseSilent(hm *HandleManager[string], t *testing.T, h int32, expected string) {
+	releasedVal, ok := hm.Release(h)
+	if !ok {
+		t.Fatalf("failed to release handle %d: not found", h)
+		return
+	}
+	if releasedVal != expected {
+		t.Fatalf("release handle %d result mismatched: expected %s, got %s", h, expected, releasedVal)
+		return
+	}
+}