Skip to content

Commit a282367

Browse files
authored
Merge pull request #91 from ayamzh/mazheng
Fix the links to the examples project in the Chinese version of the README; add a close callback mechanism for sessions.
2 parents f8ccfbb + c0ee5b7 commit a282367

File tree

7 files changed

+842
-2
lines changed

7 files changed

+842
-2
lines changed

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,73 @@ Additionally, you can manage heartbeat logic within the (Codec)OnCron method in
1818

1919
If you're using WebSocket, you don't need to worry about heartbeat request/response, as Getty handles this task within session.go's (Session)handleLoop method by sending and receiving WebSocket ping/pong frames. Your responsibility is to check whether the WebSocket session has timed out or not within codec.go's (Codec)OnCron method using session.go's (Session)GetActive method.
2020

21-
For code examples, you can refer to https://github.com/AlexStocks/getty-examples.
21+
For code examples, you can refer to [getty-examples](https://github.com/AlexStocks/getty-examples).
22+
23+
## Callback System
24+
25+
Getty provides a robust callback system that allows you to register and manage callback functions for session lifecycle events. This is particularly useful for cleanup operations, resource management, and custom event handling.
26+
27+
### Key Features
28+
29+
- **Thread-safe operations**: All callback operations are protected by mutex locks
30+
- **Replace semantics**: Adding with the same (handler, key) replaces the existing callback in place (position preserved)
31+
- **Panic safety**: During session close, callbacks run in a dedicated goroutine with defer/recover; panics are logged with stack traces and do not escape the close path
32+
- **Ordered execution**: Callbacks are executed in the order they were added
33+
34+
### Usage Example
35+
36+
```go
37+
// Add a close callback
38+
session.AddCloseCallback("cleanup", "resources", func() {
39+
// Cleanup resources when session closes
40+
cleanupResources()
41+
})
42+
43+
// Remove a specific callback
44+
// Safe to call even if the pair was never added (no-op)
45+
session.RemoveCloseCallback("cleanup", "resources")
46+
47+
// Callbacks are automatically executed when the session closes
48+
```
49+
50+
**Note**: During session shutdown, callbacks are executed sequentially in a dedicated goroutine to preserve add-order, with defer/recover to log panics without letting them escape the close path.
51+
52+
### Callback Management
53+
54+
- **AddCloseCallback**: Register a callback to be executed when the session closes
55+
- **RemoveCloseCallback**: Remove a previously registered callback (no-op if not found; safe to call multiple times)
56+
- **Thread Safety**: All operations are thread-safe and can be called concurrently
57+
58+
### Type Requirements
59+
60+
The `handler` and `key` parameters must be **comparable types** that support the `==` operator:
61+
62+
**✅ Supported types:**
63+
- **Basic types**: `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `uintptr`, `float32`, `float64`, `bool`, `complex64`, `complex128`
64+
- ⚠️ Avoid `float*`/`complex*` as keys due to NaN and precision semantics; prefer strings/ints
65+
- **Pointer types**: Pointers to any type (e.g., `*int`, `*string`, `*MyStruct`)
66+
- **Interface types**: Interface types are comparable only when their dynamic values are comparable types; using "==" with non-comparable dynamic values will be safely ignored with error log
67+
- **Channel types**: Channel types (compared by channel identity)
68+
- **Array types**: Arrays of comparable elements (e.g., `[3]int`, `[2]string`)
69+
- **Struct types**: Structs where all fields are comparable types
70+
71+
**⚠️ Non-comparable types (will be safely ignored with error log):**
72+
- `map` types (e.g., `map[string]int`)
73+
- `slice` types (e.g., `[]int`, `[]string`)
74+
- `func` types (e.g., `func()`, `func(int) string`)
75+
- Structs containing non-comparable fields (maps, slices, functions)
76+
77+
**Examples:**
78+
```go
79+
// ✅ Valid usage
80+
session.AddCloseCallback("user", "cleanup", callback)
81+
session.AddCloseCallback(123, "cleanup", callback)
82+
session.AddCloseCallback(true, false, callback)
83+
84+
// ⚠️ Non-comparable types (safely ignored with error log)
85+
session.AddCloseCallback(map[string]int{"a": 1}, "key", callback) // Logged and ignored
86+
session.AddCloseCallback([]int{1, 2, 3}, "key", callback) // Logged and ignored
87+
```
2288

2389
## About network transmission in getty
2490

README_CN.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,73 @@ Getty 是一个使用 Golang 开发的异步网络 I/O 库。它适用于 TCP、
1818

1919
如果您使用 WebSocket,您无需担心心跳请求/响应,因为 Getty 在 session.go 的 (Session)handleLoop 方法内通过发送和接收 WebSocket ping/pong 帧来处理此任务。您只需在 codec.go 的 (Codec)OnCron 方法内使用 session.go 的 (Session)GetActive 方法检查 WebSocket 会话是否已超时。
2020

21-
有关代码示例,请参阅 https://github.com/AlexStocks/getty-examples。
21+
有关代码示例,请参阅 [AlexStocks/getty-examples](https://github.com/AlexStocks/getty-examples)
22+
23+
## 回调系统
24+
25+
Getty 提供了一个强大的回调系统,允许您为会话生命周期事件注册和管理回调函数。这对于清理操作、资源管理和自定义事件处理特别有用。
26+
27+
### 主要特性
28+
29+
- **线程安全操作**:所有回调操作都受到互斥锁保护
30+
- **替换语义**:使用相同的 (handler, key) 添加会替换现有回调并保持位置不变
31+
- **Panic 安全性**:在会话关闭期间,回调在专用 goroutine 中运行,带有 defer/recover;panic 会被记录堆栈跟踪且不会逃逸出关闭路径
32+
- **有序执行**:回调按照添加的顺序执行
33+
34+
### 使用示例
35+
36+
```go
37+
// 添加关闭回调
38+
session.AddCloseCallback("cleanup", "resources", func() {
39+
// 当会话关闭时清理资源
40+
cleanupResources()
41+
})
42+
43+
// 移除特定回调
44+
// 即使从未添加过该对也可以安全调用(无操作)
45+
session.RemoveCloseCallback("cleanup", "resources")
46+
47+
// 当会话关闭时,回调会自动执行
48+
```
49+
50+
**注意**:在会话关闭期间,回调在专用 goroutine 中顺序执行以保持添加顺序,带有 defer/recover 来记录 panic 而不让它们逃逸出关闭路径。
51+
52+
### 回调管理
53+
54+
- **AddCloseCallback**:注册一个在会话关闭时执行的回调
55+
- **RemoveCloseCallback**:移除之前注册的回调(未找到时无操作;可安全多次调用)
56+
- **线程安全**:所有操作都是线程安全的,可以并发调用
57+
58+
### 类型要求
59+
60+
`handler``key` 参数必须是**可比较的类型**,支持 `==` 操作符:
61+
62+
**✅ 支持的类型:**
63+
- **基本类型**`string``int``int8``int16``int32``int64``uint``uint8``uint16``uint32``uint64``uintptr``float32``float64``bool``complex64``complex128`
64+
- ⚠️ 避免使用 `float*`/`complex*` 作为键,因为 NaN 和精度语义问题;建议使用字符串/整数
65+
- **指针类型**:指向任何类型的指针(如 `*int``*string``*MyStruct`
66+
- **接口类型**:仅当其动态值为可比较类型时可比较;若动态值不可比较,使用"=="将被安全忽略并记录错误日志
67+
- **通道类型**:通道类型(按通道标识比较)
68+
- **数组类型**:可比较元素的数组(如 `[3]int``[2]string`
69+
- **结构体类型**:所有字段都是可比较类型的结构体
70+
71+
**⚠️ 不可比较类型(将被安全忽略并记录错误日志):**
72+
- `map` 类型(如 `map[string]int`
73+
- `slice` 类型(如 `[]int``[]string`
74+
- `func` 类型(如 `func()``func(int) string`
75+
- 包含不可比较字段的结构体(maps、slices、functions)
76+
77+
**示例:**
78+
```go
79+
// ✅ 有效用法
80+
session.AddCloseCallback("user", "cleanup", callback)
81+
session.AddCloseCallback(123, "cleanup", callback)
82+
session.AddCloseCallback(true, false, callback)
83+
84+
// ⚠️ 不可比较类型(安全忽略并记录错误日志)
85+
session.AddCloseCallback(map[string]int{"a": 1}, "key", callback) // 记录日志并忽略
86+
session.AddCloseCallback([]int{1, 2, 3}, "key", callback) // 记录日志并忽略
87+
```
2288

2389
## 关于 Getty 中的网络传输
2490

transport/callback.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package getty
19+
20+
import (
21+
"fmt"
22+
"reflect"
23+
)
24+
25+
import (
26+
perrors "github.com/pkg/errors"
27+
)
28+
29+
import (
30+
log "github.com/AlexStocks/getty/util"
31+
)
32+
33+
// callbackNode represents a node in the callback linked list
34+
// Each node contains handler identifier, key, callback function and pointer to next node
35+
type callbackNode struct {
36+
handler any // Handler identifier, used to identify the source or type of callback
37+
key any // Unique identifier key for callback, used in combination with handler
38+
call func() // Actual callback function to be executed
39+
next *callbackNode // Pointer to next node, forming linked list structure
40+
}
41+
42+
// callbacks is a singly linked list structure for managing multiple callback functions
43+
// Supports dynamic addition, removal and execution of callbacks
44+
type callbacks struct {
45+
first *callbackNode // Pointer to the first node of the linked list
46+
last *callbackNode // Pointer to the last node of the linked list, used for quick addition of new nodes
47+
cbNum int // Number of callback functions in the linked list
48+
}
49+
50+
// isComparable checks if a value is comparable using Go's == operator
51+
// Returns true if the value can be safely compared, false otherwise
52+
func isComparable(v any) bool {
53+
if v == nil {
54+
return true
55+
}
56+
return reflect.TypeOf(v).Comparable()
57+
}
58+
59+
// Add adds a new callback function to the callback linked list
60+
// Parameters:
61+
// - handler: Handler identifier, can be any type
62+
// - key: Unique identifier key for callback, used in combination with handler
63+
// - callback: Callback function to be executed, ignored if nil
64+
//
65+
// Note: If a callback with the same handler and key already exists, it will be replaced
66+
func (t *callbacks) Add(handler, key any, callback func()) {
67+
// Prevent adding empty callback function
68+
if callback == nil {
69+
return
70+
}
71+
72+
// Guard: avoid runtime panic on non-comparable types
73+
if !isComparable(handler) || !isComparable(key) {
74+
log.Error(perrors.New(fmt.Sprintf("callbacks.Add: non-comparable handler/key: %T, %T; ignored", handler, key)))
75+
return
76+
}
77+
78+
// Check if a callback with the same handler and key already exists
79+
for cb := t.first; cb != nil; cb = cb.next {
80+
if cb.handler == handler && cb.key == key {
81+
// Replace existing callback
82+
cb.call = callback
83+
return
84+
}
85+
}
86+
87+
// Create new callback node
88+
newItem := &callbackNode{handler, key, callback, nil}
89+
90+
if t.first == nil {
91+
// If linked list is empty, new node becomes the first node
92+
t.first = newItem
93+
} else {
94+
// Otherwise add new node to the end of linked list
95+
t.last.next = newItem
96+
}
97+
// Update pointer to last node
98+
t.last = newItem
99+
// Increment callback count
100+
t.cbNum++
101+
}
102+
103+
// Remove removes the specified callback function from the callback linked list
104+
// Parameters:
105+
// - handler: Handler identifier of the callback to be removed
106+
// - key: Unique identifier key of the callback to be removed
107+
//
108+
// Note: If no matching callback is found, this method has no effect
109+
func (t *callbacks) Remove(handler, key any) {
110+
// Guard: avoid runtime panic on non-comparable types
111+
if !isComparable(handler) || !isComparable(key) {
112+
log.Error(perrors.New(fmt.Sprintf("callbacks.Remove: non-comparable handler/key: %T, %T; ignored", handler, key)))
113+
return
114+
}
115+
116+
var prev *callbackNode
117+
118+
// Traverse linked list to find the node to be removed
119+
for callback := t.first; callback != nil; prev, callback = callback, callback.next {
120+
// Found matching node
121+
if callback.handler == handler && callback.key == key {
122+
if t.first == callback {
123+
// If it's the first node, update first pointer
124+
t.first = callback.next
125+
} else if prev != nil {
126+
// If it's a middle node, update the next pointer of the previous node
127+
prev.next = callback.next
128+
}
129+
130+
if t.last == callback {
131+
// If it's the last node, update last pointer
132+
t.last = prev
133+
}
134+
135+
// Decrement callback count
136+
t.cbNum--
137+
138+
// Return immediately after finding and removing
139+
return
140+
}
141+
}
142+
}
143+
144+
// Invoke executes all registered callback functions in the linked list
145+
// Executes each callback in the order they were added
146+
// Note: If a callback function is nil, it will be skipped
147+
// If a callback panics, it will be handled by the outer caller's panic recovery
148+
func (t *callbacks) Invoke() {
149+
// Traverse the entire linked list starting from the head node
150+
for callback := t.first; callback != nil; callback = callback.next {
151+
// Ensure callback function is not nil before executing
152+
if callback.call != nil {
153+
callback.call()
154+
}
155+
}
156+
}
157+
158+
// Len returns the number of callback functions in the linked list
159+
// Return value: Total number of currently registered callback functions
160+
func (t *callbacks) Len() int {
161+
return t.cbNum
162+
}

0 commit comments

Comments
 (0)