-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRedisService.js
More file actions
195 lines (178 loc) · 5.1 KB
/
Copy pathRedisService.js
File metadata and controls
195 lines (178 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
const redis = require('redis');
const ENV = require('../config/environment');
const Logger = require('../utils/Logger');
class RedisService {
constructor() {
this.client = null;
this.connected = false;
this.prefix = 'player:'; // キー: player:{roomId}:{playerId}
this.ttlSeconds = 600;
}
async connect() {
if (this.connected && this.client) return;
const hasUrl = !!ENV.REDIS_URL;
const host = ENV.REDIS_HOST || '127.0.0.1';
const port = Number(ENV.REDIS_PORT || 6379);
const useTls = String(ENV.REDIS_TLS || '').toLowerCase() === 'true';
const scheme = useTls ? 'rediss' : 'redis';
const url = hasUrl ? ENV.REDIS_URL : `${scheme}://${host}:${port}`;
// v4: url か socket.host/socket.port のどちらかを使用
const base = hasUrl
? { url }
: {
socket: {
host,
port,
reconnectStrategy: (retries) => Math.min(100 + retries * 50, 1000),
},
};
this.client = require('redis').createClient({
...base,
password: ENV.REDIS_PASSWORD || undefined,
// url 指定の場合も socket オプションは使えるので再接続戦略は付けておく
socket: {
...(base.socket || {}),
reconnectStrategy: (retries) => Math.min(100 + retries * 50, 1000),
},
});
this.client.on('error', (err) => {
this.connected = false;
Logger.error('Redis error', { message: err.message });
});
this.client.on('ready', () => {
this.connected = true;
Logger.info('Redis ready');
});
await this.client.connect();
this.connected = true;
}
isAvailable() {
return this.connected && !!this.client;
}
// ルームスコープキー
_key(roomId, playerId) {
const r = roomId || 'global';
return `${this.prefix}${r}:${playerId}`;
}
async setPlayerPosition(playerId, x, z, direction, state, roomId) {
if (!this.isAvailable()) return;
try {
const key = this._key(roomId, playerId);
const data = JSON.stringify({ x, z, direction, state });
await this.client.setEx(key, this.ttlSeconds, data);
} catch (err) {
Logger.error('Redis setPlayerPosition error', {
playerId,
roomId,
message: err.message,
});
}
}
async getPlayerPosition(playerId, roomId) {
if (!this.isAvailable()) return null;
try {
const key = this._key(roomId, playerId);
const data = await this.client.get(key);
return data ? JSON.parse(data) : null;
} catch (err) {
Logger.error('Redis getPlayerPosition error', {
playerId,
roomId,
message: err.message,
});
return null;
}
}
async deletePlayerPosition(playerId, roomId) {
if (!this.isAvailable()) return;
try {
const key = this._key(roomId, playerId);
await this.client.del(key);
} catch (err) {
Logger.error('Redis deletePlayerPosition error', {
playerId,
roomId,
message: err.message,
});
}
}
// ルーム単位クリア(SCAN + バッチDEL)
async clearRoom(roomId) {
if (!this.isAvailable()) return;
const pattern = `${this.prefix}${roomId || 'global'}:*`;
let batch = [];
let total = 0;
try {
for await (const key of this.client.scanIterator({
MATCH: pattern,
COUNT: 200,
})) {
batch.push(key);
if (batch.length >= 500) {
await this.client.del(batch);
total += batch.length;
batch = [];
}
}
if (batch.length) {
await this.client.del(batch);
total += batch.length;
}
if (total) {
Logger.info('Redis room cleared', { roomId, keysCleared: total });
}
} catch (err) {
Logger.error('Redis clearRoom error', { roomId, message: err.message });
}
}
// 全体クリア(本番では原則非推奨)
async clearAll() {
if (!this.isAvailable()) return;
let batch = [];
let total = 0;
try {
for await (const key of this.client.scanIterator({
MATCH: `${this.prefix}*`,
COUNT: 500,
})) {
batch.push(key);
if (batch.length >= 500) {
await this.client.del(batch);
total += batch.length;
batch = [];
}
}
if (batch.length) {
await this.client.del(batch);
total += batch.length;
}
Logger.info('Redis cleared', { keysCleared: total });
} catch (err) {
Logger.error('Redis clearAll error', { message: err.message });
}
}
async disconnect() {
if (!this.client) return;
try {
await this.client.quit();
} catch (err) {
Logger.error('Redis disconnect error', { message: err.message });
} finally {
this.connected = false;
this.client = null;
}
}
}
let instance = null;
async function getRedisService() {
if (!instance) {
instance = new RedisService();
try {
await instance.connect();
} catch (err) {
Logger.error('Redis connection failed', { message: err.message });
}
}
return instance;
}
module.exports = { getRedisService, RedisService };