Skip to content

Commit a0d1dfe

Browse files
committed
docs: RFC: Enhancement of Internal System Session Management
1 parent 70999a6 commit a0d1dfe

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Enhancement of Internal System Session Management
2+
3+
- Author: [Wang Chao](https://github.com/lcwangchao)
4+
- Discussion PR: <https://github.com/pingcap/tidb/pull/59875>
5+
- Tracking Issue: <https://github.com/pingcap/tidb/issues/60115>
6+
7+
## Motivation and Background
8+
9+
The internal system sessions are widely used in TiDB across multiple modules. To minimize the overhead of creating and destroying sessions, we use a session pool for management. However, we have recently encountered several issues with the current session pool implementation—most of which stem from its improper usage.
10+
11+
For example, in issue [#56934](https://github.com/pingcap/tidb/issues/56934), the TTL module retrieves a session from the pool but fails to return it after use, leading to a memory leak. Another issue arose after PR [#32726](https://github.com/pingcap/tidb/pull/32726), where each session retrieved from the pool is stored as a reference in a map. This reference is only removed when the session is returned to the pool. As a result, if a session is closed using the `Close` method, it may still cause a memory leak because it remains referenced in the map.
12+
13+
Some PR such as [#59546](https://github.com/pingcap/tidb/pull/59546) attempt to address these issues by introducing a `Destroy` method for the session pool. However, this approach does not fully resolve the problems and still makes it difficult to ensure that developers call the correct method.
14+
15+
This document proposes a more effective solution for managing internal system sessions to achieve the following goals:
16+
17+
- Prevent concurrent usage across modules: Once a session is returned to the pool, it should no longer be used to avoid potential conflicts from concurrent access.
18+
- Simplify session lifecycle management: Ensure that sessions are always either returned to the pool or properly closed after use, eliminating the risk of memory leaks.
19+
- Enforce session state reset: When a session is returned to the pool, its internal state should be forcibly reset to prevent unexpected behaviors in subsequent usage.
20+
21+
## Design
22+
23+
### Pool
24+
25+
We need to introduce a new enhanced session pool to manage internal system sessions. The new session pool will provide the following methods:
26+
27+
```go
28+
type Pool struct {
29+
...
30+
}
31+
32+
func (p *Pool) get() (*Session, error) {
33+
...
34+
}
35+
36+
func (p *Pool) put(s *Session) {
37+
...
38+
}
39+
40+
func (p *Pool) WithSession(func(*Session) error) error {
41+
...
42+
}
43+
```
44+
45+
The most important method of `Pool` is `WithSession` which is introduced to replace the `Get` and `Put` methods of the current session pool. The `WithSession` method accepts a function as an argument, which will be executed with a session retrieved from the pool. After the function completes, the session will be automatically returned to the pool. This design ensures that the session is always returned to the pool after use, preventing memory leaks.
46+
47+
Let's see how `WithSession` works:
48+
49+
```go
50+
func (p *Pool) WithSession(func(*Session) error) (err error) {
51+
se, err := p.get()
52+
if err != nil {
53+
return err
54+
}
55+
56+
defer func() {
57+
if r := recover(); r != nil {
58+
// Always destroy the session when panic to avoid undetermined state.
59+
se.Destroy()
60+
panic(r)
61+
}
62+
p.put(se)
63+
}()
64+
return f(se)
65+
}
66+
```
67+
68+
We can see that the `WithSession`n method always calls `p.put(se)` within a defer function to ensure the session is returned to the pool. One exception to this occurs when a panic happens. In such cases, the session is destroyed instead of being returned to the pool, as it may be in an undefined state, making it safer to discard rather than reuse.
69+
70+
The `Pool` should be responsible for managing the internal sessions' registration and unregistering, and the callers should not care about it. These operations are handled by `Pool.get` and `Pool.put` methods:
71+
72+
```go
73+
func (p *Pool) get() (*Session, error) {
74+
// get a session from the pool
75+
se, err := getFromPool()
76+
if err != nil {
77+
return nil, err
78+
}
79+
// register the internal session to the map
80+
infosync.StoreInternalSession(se.GetSessionContext())
81+
return se, nil
82+
}
83+
84+
func (p *Pool) put(s *Session) {
85+
// rollback the session first to reset the internal state.
86+
s.Rollback()
87+
// unregister the internal session from the map
88+
infosync.DeleteInternalSession(s.GetSessionContext())
89+
// put the session back to the pool
90+
p.putToPool(s)
91+
}
92+
```
93+
94+
The above code has been simplified for demonstration purposes. The session pool stores the internal session in a map when `Pool.get` is called and removes it from the map in `Pool.put`. Additionally, before being returned to the pool, the session is automatically rolled back to ensure its internal state is reset.
95+
96+
With the newly implemented session pool that manages the session lifecycle, developers no longer need to worry about the session's state or lifecycle.
97+
98+
### Session
99+
100+
The type `Session` is not a "real" one but a wrapper of the internal session, see:
101+
102+
```go
103+
// Session is public to callers.
104+
type Session struct {
105+
internal *session
106+
}
107+
```
108+
109+
It provides a set of methods that proxy access to the internal session. When the caller invokes `WithSession`, a new `Session` instance is created and passed to the function. Once the function completes, the `Session` instance is destroyed, and only the internal session is returned to the pool.
110+
111+
We have several reasons for proposing the above design:
112+
113+
- The wrapped methods allow for additional checks to prevent misuse of the internal session. For example, if a caller attempts to invoke methods after the session has been returned to the pool, we can detect this and return an error.
114+
- The `Session` type can be extended with new methods, providing more convenient functionality for internal use.
115+
- The original session contains numerous methods, many of which may not be necessary for the caller. By filtering out unnecessary methods, we can expose only the required ones, simplifying the interface and improving usability.
116+
117+
The internal session is not exposed to the caller and can only be accessed by the `Session` or `Pool` types. It has several fields, see:
118+
119+
```go
120+
// session is private and can only be accessed by the `Pool` and `Session`.
121+
type session struct {
122+
mu sync.Mutex
123+
owner any
124+
sctx sessionctx.Context
125+
...
126+
}
127+
```
128+
129+
The `mu` protects the internal session from concurrent access, and the field `sctx` is the real session context that holds the session's state.
130+
131+
Let's provide further clarification on the `owner` field. The "owner" refers to the entity that holds the internal session, and only the owner is permitted to use it. When an internal session is in the pool, the pool itself is considered the owner. When a caller retrieves the session from the pool, the wrapped `Session` becomes the new owner, which is then passed to the caller.
132+
133+
When the caller returns the session to the pool, ownership is first transferred back to the pool, which then places the session back into the pool. Since the public `Session` will not be reused, once it loses ownership, it can never regain it. This ensures that the `Session` becomes invalid once it has been used, preventing any further use after its lifecycle is completed.
134+
135+
```go
136+
func (s *session) checkOwner(owner any) error {
137+
s.mu.Lock()
138+
defer s.mu.Unlock()
139+
if s.owner == nil || s.owner != owner {
140+
return errors.New("invalid owner")
141+
}
142+
return nil
143+
}
144+
145+
func (s *Session) ExecuteInternal(ctx context.Context, sql string, args ...any) (sqlexec.RecordSet, error) {
146+
if err := s.internal.checkOwner(s); err != nil {
147+
return nil, err
148+
}
149+
return s.internal.sctx.ExecuteInternal(ctx, sql, args...)
150+
}
151+
```
152+
153+
## Tests
154+
155+
We should do the below tests to make sure the new introduced session pool works as expected:
156+
157+
- Unit tests.
158+
- Integration tests for specified scenarios:
159+
- Test 1000+ TTL tables with small `tidb_ttl_job_internal` settings to test the frequent session creation and destruction in the TTL scene.
160+
- More tests for other scenarios.
161+
162+
## More Features to Support
163+
164+
There are also some features we can support in the future:
165+
166+
- Support to detect concurrent usage of the un-thread-safe methods.
167+
- Support to detect more session state changes such as system variables and reset them when returning to the pool.

0 commit comments

Comments
 (0)