Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,73 @@ Additionally, you can manage heartbeat logic within the (Codec)OnCron method in

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.

For code examples, you can refer to https://github.com/AlexStocks/getty-examples.
For code examples, you can refer to [getty-examples](https://github.com/AlexStocks/getty-examples).

## Callback System

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.

### Key Features

- **Thread-safe operations**: All callback operations are protected by mutex locks
- **Replace semantics**: Adding with the same (handler, key) replaces the existing callback in place (position preserved)
- **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
- **Ordered execution**: Callbacks are executed in the order they were added

### Usage Example

```go
// Add a close callback
session.AddCloseCallback("cleanup", "resources", func() {
// Cleanup resources when session closes
cleanupResources()
})

// Remove a specific callback
// Safe to call even if the pair was never added (no-op)
session.RemoveCloseCallback("cleanup", "resources")

// Callbacks are automatically executed when the session closes
```

**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.

### Callback Management

- **AddCloseCallback**: Register a callback to be executed when the session closes
- **RemoveCloseCallback**: Remove a previously registered callback (no-op if not found; safe to call multiple times)
- **Thread Safety**: All operations are thread-safe and can be called concurrently

### Type Requirements

The `handler` and `key` parameters must be **comparable types** that support the `==` operator:

**✅ Supported types:**
- **Basic types**: `string`, `int`, `int8`, `int16`, `int32`, `int64`, `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `uintptr`, `float32`, `float64`, `bool`, `complex64`, `complex128`
- ⚠️ Avoid `float*`/`complex*` as keys due to NaN and precision semantics; prefer strings/ints
- **Pointer types**: Pointers to any type (e.g., `*int`, `*string`, `*MyStruct`)
- **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
- **Channel types**: Channel types (compared by channel identity)
- **Array types**: Arrays of comparable elements (e.g., `[3]int`, `[2]string`)
- **Struct types**: Structs where all fields are comparable types

**⚠️ Non-comparable types (will be safely ignored with error log):**
- `map` types (e.g., `map[string]int`)
- `slice` types (e.g., `[]int`, `[]string`)
- `func` types (e.g., `func()`, `func(int) string`)
- Structs containing non-comparable fields (maps, slices, functions)

**Examples:**
```go
// ✅ Valid usage
session.AddCloseCallback("user", "cleanup", callback)
session.AddCloseCallback(123, "cleanup", callback)
session.AddCloseCallback(true, false, callback)

// ⚠️ Non-comparable types (safely ignored with error log)
session.AddCloseCallback(map[string]int{"a": 1}, "key", callback) // Logged and ignored
session.AddCloseCallback([]int{1, 2, 3}, "key", callback) // Logged and ignored
```

## About network transmission in getty

Expand Down
68 changes: 67 additions & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,73 @@ Getty 是一个使用 Golang 开发的异步网络 I/O 库。它适用于 TCP、

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

有关代码示例,请参阅 https://github.com/AlexStocks/getty-examples。
有关代码示例,请参阅 [AlexStocks/getty-examples](https://github.com/AlexStocks/getty-examples)。

## 回调系统

Getty 提供了一个强大的回调系统,允许您为会话生命周期事件注册和管理回调函数。这对于清理操作、资源管理和自定义事件处理特别有用。

### 主要特性

- **线程安全操作**:所有回调操作都受到互斥锁保护
- **替换语义**:使用相同的 (handler, key) 添加会替换现有回调并保持位置不变
- **Panic 安全性**:在会话关闭期间,回调在专用 goroutine 中运行,带有 defer/recover;panic 会被记录堆栈跟踪且不会逃逸出关闭路径
- **有序执行**:回调按照添加的顺序执行

### 使用示例

```go
// 添加关闭回调
session.AddCloseCallback("cleanup", "resources", func() {
// 当会话关闭时清理资源
cleanupResources()
})

// 移除特定回调
// 即使从未添加过该对也可以安全调用(无操作)
session.RemoveCloseCallback("cleanup", "resources")

// 当会话关闭时,回调会自动执行
```

**注意**:在会话关闭期间,回调在专用 goroutine 中顺序执行以保持添加顺序,带有 defer/recover 来记录 panic 而不让它们逃逸出关闭路径。

### 回调管理

- **AddCloseCallback**:注册一个在会话关闭时执行的回调
- **RemoveCloseCallback**:移除之前注册的回调(未找到时无操作;可安全多次调用)
- **线程安全**:所有操作都是线程安全的,可以并发调用

### 类型要求

`handler` 和 `key` 参数必须是**可比较的类型**,支持 `==` 操作符:

**✅ 支持的类型:**
- **基本类型**:`string`、`int`、`int8`、`int16`、`int32`、`int64`、`uint`、`uint8`、`uint16`、`uint32`、`uint64`、`uintptr`、`float32`、`float64`、`bool`、`complex64`、`complex128`
- ⚠️ 避免使用 `float*`/`complex*` 作为键,因为 NaN 和精度语义问题;建议使用字符串/整数
- **指针类型**:指向任何类型的指针(如 `*int`、`*string`、`*MyStruct`)
- **接口类型**:仅当其动态值为可比较类型时可比较;若动态值不可比较,使用"=="将被安全忽略并记录错误日志
- **通道类型**:通道类型(按通道标识比较)
- **数组类型**:可比较元素的数组(如 `[3]int`、`[2]string`)
- **结构体类型**:所有字段都是可比较类型的结构体

**⚠️ 不可比较类型(将被安全忽略并记录错误日志):**
- `map` 类型(如 `map[string]int`)
- `slice` 类型(如 `[]int`、`[]string`)
- `func` 类型(如 `func()`、`func(int) string`)
- 包含不可比较字段的结构体(maps、slices、functions)

**示例:**
```go
// ✅ 有效用法
session.AddCloseCallback("user", "cleanup", callback)
session.AddCloseCallback(123, "cleanup", callback)
session.AddCloseCallback(true, false, callback)

// ⚠️ 不可比较类型(安全忽略并记录错误日志)
session.AddCloseCallback(map[string]int{"a": 1}, "key", callback) // 记录日志并忽略
session.AddCloseCallback([]int{1, 2, 3}, "key", callback) // 记录日志并忽略
```

## 关于 Getty 中的网络传输

Expand Down
162 changes: 162 additions & 0 deletions transport/callback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package getty

import (
"fmt"
"reflect"
)

import (
perrors "github.com/pkg/errors"
)

import (
log "github.com/AlexStocks/getty/util"
)

// callbackNode represents a node in the callback linked list
// Each node contains handler identifier, key, callback function and pointer to next node
type callbackNode struct {
handler any // Handler identifier, used to identify the source or type of callback
key any // Unique identifier key for callback, used in combination with handler
call func() // Actual callback function to be executed
next *callbackNode // Pointer to next node, forming linked list structure
}

// callbacks is a singly linked list structure for managing multiple callback functions
// Supports dynamic addition, removal and execution of callbacks
type callbacks struct {
first *callbackNode // Pointer to the first node of the linked list
last *callbackNode // Pointer to the last node of the linked list, used for quick addition of new nodes
cbNum int // Number of callback functions in the linked list
}

// isComparable checks if a value is comparable using Go's == operator
// Returns true if the value can be safely compared, false otherwise
func isComparable(v any) bool {
if v == nil {
return true
}
return reflect.TypeOf(v).Comparable()
}

// Add adds a new callback function to the callback linked list
// Parameters:
// - handler: Handler identifier, can be any type
// - key: Unique identifier key for callback, used in combination with handler
// - callback: Callback function to be executed, ignored if nil
//
// Note: If a callback with the same handler and key already exists, it will be replaced
func (t *callbacks) Add(handler, key any, callback func()) {
// Prevent adding empty callback function
if callback == nil {
return
}

// Guard: avoid runtime panic on non-comparable types
if !isComparable(handler) || !isComparable(key) {
log.Error(perrors.New(fmt.Sprintf("callbacks.Add: non-comparable handler/key: %T, %T; ignored", handler, key)))
return
}

// Check if a callback with the same handler and key already exists
for cb := t.first; cb != nil; cb = cb.next {
if cb.handler == handler && cb.key == key {
// Replace existing callback
cb.call = callback
return
}
}

// Create new callback node
newItem := &callbackNode{handler, key, callback, nil}

if t.first == nil {
// If linked list is empty, new node becomes the first node
t.first = newItem
} else {
// Otherwise add new node to the end of linked list
t.last.next = newItem
}
// Update pointer to last node
t.last = newItem
// Increment callback count
t.cbNum++
}

// Remove removes the specified callback function from the callback linked list
// Parameters:
// - handler: Handler identifier of the callback to be removed
// - key: Unique identifier key of the callback to be removed
//
// Note: If no matching callback is found, this method has no effect
func (t *callbacks) Remove(handler, key any) {
// Guard: avoid runtime panic on non-comparable types
if !isComparable(handler) || !isComparable(key) {
log.Error(perrors.New(fmt.Sprintf("callbacks.Remove: non-comparable handler/key: %T, %T; ignored", handler, key)))
return
}

var prev *callbackNode

// Traverse linked list to find the node to be removed
for callback := t.first; callback != nil; prev, callback = callback, callback.next {
// Found matching node
if callback.handler == handler && callback.key == key {
if t.first == callback {
// If it's the first node, update first pointer
t.first = callback.next
} else if prev != nil {
// If it's a middle node, update the next pointer of the previous node
prev.next = callback.next
}

if t.last == callback {
// If it's the last node, update last pointer
t.last = prev
}

// Decrement callback count
t.cbNum--

// Return immediately after finding and removing
return
}
}
}

// Invoke executes all registered callback functions in the linked list
// Executes each callback in the order they were added
// Note: If a callback function is nil, it will be skipped
// If a callback panics, it will be handled by the outer caller's panic recovery
func (t *callbacks) Invoke() {
// Traverse the entire linked list starting from the head node
for callback := t.first; callback != nil; callback = callback.next {
// Ensure callback function is not nil before executing
if callback.call != nil {
callback.call()
}
}
}

// Len returns the number of callback functions in the linked list
// Return value: Total number of currently registered callback functions
func (t *callbacks) Len() int {
return t.cbNum
}
Loading
Loading