Commit 629a330b82a5c6942942f24dceed6741690c3b7b
1 parent
0aff5f86
Exists in
master
first commit
Showing
46 changed files
with
8351 additions
and
0 deletions
Show diff stats
... | ... | @@ -0,0 +1,57 @@ |
1 | +package conf | |
2 | + | |
3 | +import ( | |
4 | + "encoding/xml" | |
5 | + "io/ioutil" | |
6 | + _ "strings" | |
7 | + | |
8 | + "common/logger" | |
9 | +) | |
10 | + | |
11 | +type RedisConf struct { | |
12 | + Host string `xml:",attr"` | |
13 | + Db uint32 `xml:",attr"` | |
14 | + Password string `xml:",attr"` | |
15 | +} | |
16 | + | |
17 | +type TexasConf struct { | |
18 | + ServerHttpAddr ServerHttpAddrConf `xml:"ServerHttpAddr"` | |
19 | + Redis RedisConf `xml:"Redis"` | |
20 | +} | |
21 | + | |
22 | +var ( | |
23 | + config = new(TexasConf) | |
24 | +) | |
25 | + | |
26 | +func GetRedisConf() RedisConf { | |
27 | + return config.Redis | |
28 | +} | |
29 | + | |
30 | +func LoadConf(filename string) error { | |
31 | + content, err := ioutil.ReadFile(filename) | |
32 | + if err != nil { | |
33 | + logger.Notic("read file:%v error:", filename, err) | |
34 | + return err | |
35 | + } | |
36 | + logger.Info("conf xml:%v", string(content)) | |
37 | + err = xml.Unmarshal(content, config) | |
38 | + if err != nil { | |
39 | + logger.Notic("decode xml error:%v", err) | |
40 | + return err | |
41 | + } | |
42 | + DumpConf() | |
43 | + return nil | |
44 | +} | |
45 | + | |
46 | +func DumpConf() { | |
47 | + logger.Info("--------------config dump----start--------") | |
48 | + logger.Info("--------------config dump----end--------") | |
49 | +} | |
50 | + | |
51 | +func GetServerHttpAddrConf() string { | |
52 | + return config.ServerHttpAddr.Host | |
53 | +} | |
54 | + | |
55 | +type ServerHttpAddrConf struct { | |
56 | + Host string `xml:",attr"` | |
57 | +} | ... | ... |
... | ... | @@ -0,0 +1,456 @@ |
1 | +package logic | |
2 | + | |
3 | +type UserLoginData struct { | |
4 | + //Uuid int `json:"uuid"` | |
5 | + Fromid int `json:"fromid"` | |
6 | + Sharetype int `json:"sharetype"` | |
7 | +} | |
8 | + | |
9 | +type UserLoginResp struct { | |
10 | + Code int `json:"code"` | |
11 | + Message string `json:"message"` | |
12 | + Gold int64 `json:"gold"` | |
13 | + Love int64 `json:"love"` | |
14 | + Goldrate int64 `json:"goldrate"` | |
15 | + Loverate int64 `json:"loverate"` | |
16 | + Highestlv int `json:"highestlv"` | |
17 | + Curboxlv int `json:"curboxlv"` | |
18 | + Isdoublegold int `json:"isdoublegold"` | |
19 | + Isauto int `json:"isauto"` | |
20 | + Offlinegold int64 `json:"offlinegold"` | |
21 | + Offlinelove int64 `json:"offlinelove"` | |
22 | + Data []DataDesc `json:"data"` | |
23 | +} | |
24 | + | |
25 | +type GetUserDataReq struct { | |
26 | + //Uuid int `json:"uuid"` | |
27 | +} | |
28 | + | |
29 | +type GetUserDataResp struct { | |
30 | + Code int `json:"code"` | |
31 | + Message string `json:"message"` | |
32 | + Gold int64 `json:"gold"` | |
33 | + Love int64 `json:"love"` | |
34 | + Dougoldlefttime int `json:"dougoldlefttime"` | |
35 | + Automlefttime int `json:"automlefttime"` | |
36 | + Acclcteboxlefttime int `json:"acclcteboxlefttime"` | |
37 | + Israndgift int `json:"israndgift"` | |
38 | +} | |
39 | + | |
40 | +type DataDesc struct { | |
41 | + Pos int `json:"pos"` | |
42 | + Catlv int `json:"cat_lv"` | |
43 | + Countdown int `json:"countdown"` | |
44 | +} | |
45 | + | |
46 | +type ExchangePosReq struct { | |
47 | + //Uuid int `json:"uuid"` | |
48 | + Pos_1 int `json:"pos_1"` | |
49 | + Pos_2 int `json:"pos_2"` | |
50 | +} | |
51 | + | |
52 | +type ExchangePosResp struct { | |
53 | + Code int `json:"code"` | |
54 | + Message string `json:"message"` | |
55 | + Pos1_lv int `json:"pos1_lv"` | |
56 | + Pos2_lv int `json:"pos2_lv"` | |
57 | + Highest_lv int `json:"highest_lv"` | |
58 | + Add_num int `json:"add_num"` | |
59 | +} | |
60 | + | |
61 | +type ClickBoxReq struct { | |
62 | + //Uuid int `json:"uuid"` | |
63 | + Pos int `json:"pos"` | |
64 | +} | |
65 | + | |
66 | +type ClickBoxResp struct { | |
67 | + Code int `json:"code"` | |
68 | + Message string `json:"message"` | |
69 | + Pos int `json:"pos"` | |
70 | + Lv int `json:"lv"` | |
71 | +} | |
72 | + | |
73 | +type UpgradeBoxReq struct { | |
74 | + //Uuid int `json:"uuid"` | |
75 | +} | |
76 | +type UpgradeBoxResp struct { | |
77 | + Code int `json:"code"` | |
78 | + Message string `json:"message"` | |
79 | + Boxlv int `json:"boxlv"` | |
80 | +} | |
81 | + | |
82 | +type GenerateBoxReq struct { | |
83 | + //Uuid int `json:"uuid"` | |
84 | + Pos int `json:"pos"` | |
85 | +} | |
86 | + | |
87 | +type GenerateBoxResp struct { | |
88 | + Code int `json:"code"` | |
89 | + Message string `json:"message"` | |
90 | +} | |
91 | + | |
92 | +type AcclecteReq struct { | |
93 | + //Uuid int `json:"uuid"` | |
94 | +} | |
95 | +type AcclecteResp struct { | |
96 | + Code int `json:"code"` | |
97 | + Message string `json:"message"` | |
98 | +} | |
99 | + | |
100 | +type AcclecteBoxResp struct { | |
101 | + Code int `json:"code"` | |
102 | + Message string `json:"message"` | |
103 | +} | |
104 | + | |
105 | +type AutomergeReq struct { | |
106 | + //Uuid int `json:"uuid"` | |
107 | +} | |
108 | +type AutomergeResp struct { | |
109 | + Code int `json:"code"` | |
110 | + Message string `json:"message"` | |
111 | +} | |
112 | + | |
113 | +type QueryBuyCatReq struct { | |
114 | + //Uuid int `json:"uuid"` | |
115 | +} | |
116 | +type QueryBuyCatResp struct { | |
117 | + Code int `json:"code"` | |
118 | + Message string `json:"message"` | |
119 | + Maxcatlv int `json:"maxcatlv"` | |
120 | + Data []BuyCatDesc `json:"data"` | |
121 | +} | |
122 | + | |
123 | +type BuyCatDesc struct { | |
124 | + Lv int `json:"lv"` | |
125 | + Goldnum int64 `json:"goldnum"` | |
126 | +} | |
127 | + | |
128 | +type ClickRandGiftReq struct { | |
129 | + //Uuid int `json:"uuid"` | |
130 | +} | |
131 | +type ClickRandGiftResp struct { | |
132 | + Code int `json:"code"` | |
133 | + Message string `json:"message"` | |
134 | + Gold int64 `json:"gold"` | |
135 | + Lefttime int `json:"lefttime"` | |
136 | +} | |
137 | + | |
138 | +type DoBuyCatReq struct { | |
139 | + Lv int `json:"lv"` | |
140 | +} | |
141 | +type DoBuyCatResp struct { | |
142 | + Code int `json:"code"` | |
143 | + Message string `json:"message"` | |
144 | + Pos int `json:"pos"` | |
145 | +} | |
146 | + | |
147 | +type QueryWareHouseResp struct { | |
148 | + Code int `json:"code"` | |
149 | + Message string `json:"message"` | |
150 | + Data []DataDesc `json:"data"` | |
151 | +} | |
152 | + | |
153 | +type QueryAutomergeResp struct { | |
154 | + Code int `json:"code"` | |
155 | + Message string `json:"message"` | |
156 | + Goldrate int64 `json:"goldrate"` | |
157 | + Higestlv int `json:"higestlv"` | |
158 | + Curcatjianum int `json:"curcatjianum"` | |
159 | + Data []DataDesc `json:"data"` | |
160 | +} | |
161 | + | |
162 | +type PutWareHouseReq struct { | |
163 | + Pos int `json:"pos"` | |
164 | +} | |
165 | +type PutWareHouseResp struct { | |
166 | + Code int `json:"code"` | |
167 | + Message string `json:"message"` | |
168 | + Warepos int `json:"warepos"` | |
169 | +} | |
170 | + | |
171 | +type TakeWareHouseReq struct { | |
172 | + Warepos int `json:"warepos"` | |
173 | +} | |
174 | +type TakeWareHouseResp struct { | |
175 | + Code int `json:"code"` | |
176 | + Message string `json:"message"` | |
177 | + Pos int `json:"pos"` | |
178 | +} | |
179 | + | |
180 | +type CatRoomDesc struct { | |
181 | + Pos int `json:"pos"` | |
182 | + Catlv int `json:"cat_lv"` | |
183 | +} | |
184 | + | |
185 | +type QueryCatRoomInfoResp struct { | |
186 | + Code int `json:"code"` | |
187 | + Message string `json:"message"` | |
188 | + Data []CatRoomDesc `json:"data"` | |
189 | +} | |
190 | + | |
191 | +type BuyCatRoomReq struct { | |
192 | + Lv int `json:"lv"` | |
193 | +} | |
194 | + | |
195 | +type BuyCatRoomResp struct { | |
196 | + Code int `json:"code"` | |
197 | + Message string `json:"message"` | |
198 | +} | |
199 | + | |
200 | +type UpCattoRoomReq struct { | |
201 | + Roompos int `json:"roompos"` | |
202 | + Callv int `json:"callv"` | |
203 | + Optype int `json:"optype"` | |
204 | +} | |
205 | + | |
206 | +type UpCattoRoommResp struct { | |
207 | + Code int `json:"code"` | |
208 | + Message string `json:"message"` | |
209 | +} | |
210 | + | |
211 | +type QueryCatShopInfoResp struct { | |
212 | + Code int `json:"code"` | |
213 | + Message string `json:"message"` | |
214 | + Chapter int `json:"chapter"` | |
215 | + Section int `json:"section"` | |
216 | + Lefttime int `json:"lefttime"` | |
217 | + Canwatch int `json:"canwatch"` | |
218 | + Storyhappen int `json:"storyhappen"` | |
219 | +} | |
220 | + | |
221 | +type CatShoPlayReq struct { | |
222 | + Catlv int `json:"catlv"` | |
223 | +} | |
224 | +type CatShoPlayResp struct { | |
225 | + Code int `json:"code"` | |
226 | + Message string `json:"message"` | |
227 | + Lefttime int `json:"lefttime"` | |
228 | + Canwatch int `json:"canwatch"` | |
229 | +} | |
230 | + | |
231 | +type GetCatShopRewardReq struct { | |
232 | + Optype int `json:"optype"` | |
233 | +} | |
234 | + | |
235 | +type GetCatShopRewardResp struct { | |
236 | + Code int `json:"code"` | |
237 | + Message string `json:"message"` | |
238 | + Love int64 `json:"love"` | |
239 | +} | |
240 | + | |
241 | +type AcclecteCatStoryResp struct { | |
242 | + Code int `json:"code"` | |
243 | + Message string `json:"message"` | |
244 | +} | |
245 | + | |
246 | +type UpdateUserInfoReq struct { | |
247 | + Headurl string `json:"headurl"` | |
248 | + Nickname string `json:"nickname"` | |
249 | + Realname string `json:"realname"` | |
250 | +} | |
251 | + | |
252 | +type UpdateUserInfoResp struct { | |
253 | + Code int `json:"code"` | |
254 | + Message string `json:"message"` | |
255 | +} | |
256 | + | |
257 | +type RankInfoDesc struct { | |
258 | + Rank int `json:"rank"` | |
259 | + Headurl string `json:"headurl"` | |
260 | + Nickname string `json:"nickname"` | |
261 | + Catlv int `json:"catlv"` | |
262 | + Goldnum int64 `json:"goldnum"` | |
263 | +} | |
264 | + | |
265 | +type QueryPlayerRankResp struct { | |
266 | + Code int `json:"code"` | |
267 | + Message string `json:"message"` | |
268 | + Data []RankInfoDesc `json:"data"` | |
269 | +} | |
270 | + | |
271 | +type QueryCompleteTaskDesc struct { | |
272 | + Taskid int `json:"taskid"` | |
273 | +} | |
274 | + | |
275 | +type QueryOnlienTaskDesc struct { | |
276 | + Taskid int `json:"taskid"` | |
277 | + Lefttime int64 `json:"lefttime"` | |
278 | +} | |
279 | + | |
280 | +type QueryCompleteTaskResp struct { | |
281 | + Code int `json:"code"` | |
282 | + Message string `json:"message"` | |
283 | + Data []QueryCompleteTaskDesc `json:"data"` | |
284 | + Online []QueryOnlienTaskDesc `json:"online"` | |
285 | +} | |
286 | + | |
287 | +type QueryCompleteAchievementDesc struct { | |
288 | + Achieveid int `json:"achieveid"` | |
289 | +} | |
290 | + | |
291 | +type QueryCompleteAchievementResp struct { | |
292 | + Code int `json:"code"` | |
293 | + Message string `json:"message"` | |
294 | + Data []QueryCompleteAchievementDesc `json:"data"` | |
295 | +} | |
296 | + | |
297 | +type GetTaskRewardReq struct { | |
298 | + Taskid int `json:"taskid"` | |
299 | +} | |
300 | + | |
301 | +type GetTaskRewardResp struct { | |
302 | + Code int `json:"code"` | |
303 | + Message string `json:"message"` | |
304 | +} | |
305 | + | |
306 | +type GetAchieveRewardReq struct { | |
307 | + Achieveid int `json:"achieveid"` | |
308 | +} | |
309 | + | |
310 | +type GetAchieveRewardResp struct { | |
311 | + Code int `json:"code"` | |
312 | + Message string `json:"message"` | |
313 | +} | |
314 | + | |
315 | +type StartOnlineTaskReq struct { | |
316 | + Taskid int `json:"taskid"` | |
317 | +} | |
318 | + | |
319 | +type StartOnlineTaskResp struct { | |
320 | + Code int `json:"code"` | |
321 | + Message string `json:"message"` | |
322 | + Lefttime int `json:"lefttime"` | |
323 | +} | |
324 | + | |
325 | +type GetOfflineRewardReq struct { | |
326 | + Optype int `json:"optype"` | |
327 | +} | |
328 | + | |
329 | +type GetOfflineRewardResp struct { | |
330 | + Code int `json:"code"` | |
331 | + Message string `json:"message"` | |
332 | + Gold int64 `json:"gold"` | |
333 | + Love int64 `json:"love"` | |
334 | +} | |
335 | + | |
336 | +//********************************************************************************************************** | |
337 | + | |
338 | +type PosData struct { | |
339 | + Pos int | |
340 | + Catlv int | |
341 | + Countdown int //倒计时 为0表示没有 | |
342 | + //UpPos int //上阵的位置 0表示未上阵 | |
343 | +} | |
344 | + | |
345 | +//玩家购买猫详情 | |
346 | +type BuyCatInfoData struct { | |
347 | + Buytime int //购买次数 | |
348 | + IsMaxBuytime int //是否达到涨价上线 1是0否 | |
349 | + CurPrice int64 //当前价格 | |
350 | +} | |
351 | + | |
352 | +//猫咖店家具数据 | |
353 | +type CatRoomData struct { | |
354 | + LvCatlv int //入住猫的等级 | |
355 | +} | |
356 | + | |
357 | +//猫咖门店数据 | |
358 | +type CatShopData struct { | |
359 | + Chapter int //当前所处进度 对应shopcaofig的id | |
360 | + Section int //当前小节进度 ,即当前大章节第几次故事 | |
361 | + IsPlaying int //是否处于探险模式 1是0否 | |
362 | + LeftTime int //探险剩余时间 | |
363 | + PlayTimes int //当前为第几次探险 | |
364 | + DayNum int //当天日期,用于判断跨天 | |
365 | + TotalWatchNumLeft int //当天剩余看视频次数 | |
366 | + ThisIsWatch int //本次探险是否已经看过视频 | |
367 | + IsMax int //是否已经通关 1是0否 | |
368 | + CurCatLv int //参加探险的猫等级 | |
369 | + IsStoryHappen int //此次是否触发故事 | |
370 | +} | |
371 | + | |
372 | +//玩家排行榜数据 | |
373 | +type UserRankInfo struct { | |
374 | + Gold int64 //金币 | |
375 | + Head string //头像地址 | |
376 | + NickName string //昵称 | |
377 | + Highestlv int //当前最高猫等级 | |
378 | + Uuid int //uuid | |
379 | +} | |
380 | + | |
381 | +//玩家任务数据 每日清零 | |
382 | +type TaskData struct { | |
383 | + //StartOnline int //开始计算在线时间 | |
384 | + StartOnline map[int]int64 //记录对应takid 和开始计时的时间 | |
385 | + BuyCatTime int //商店购买猫次数 | |
386 | + MergeTime int //合成猫次数 | |
387 | + PlayWithTime int //陪玩次数 | |
388 | + WatchAddTime int //看广告次数 | |
389 | + CompleteId map[int]int //已经完成的任务的id,对应task表的id value无用 | |
390 | + HaveComplete map[int]int //记录当日已完成的任务,已完成则不再完成 | |
391 | +} | |
392 | + | |
393 | +//玩家成就数据 | |
394 | +type AchieveMentData struct { | |
395 | + GetNewCatTime int //累计解锁新猫次数 | |
396 | + GetAllJia int //累计获得猫爬架次数 | |
397 | + GetRoomJu int //累计解锁新家具次数 | |
398 | + StoryTime int //累计解锁故事 | |
399 | + ShopTime int //累计解锁店铺 | |
400 | + CompleteId map[int]int //已经完成的任务的id,对应achievement表id value为无用 | |
401 | + HaveComplete map[int]int //记录已完成成就已完成则不再触发 | |
402 | +} | |
403 | + | |
404 | +//玩家数据 | |
405 | +type UserData struct { | |
406 | + Gold int64 //金币 | |
407 | + Love int64 //爱心值 | |
408 | + Goldrate int64 //金币生成速率 | |
409 | + Loverate int64 //爱心生产速率 | |
410 | + Highestlv int //当前最高猫等级 | |
411 | + InviteId int //邀请者uid | |
412 | + CurBoxLv int //当前猫箱子等级 | |
413 | + IsDouble int //当前加速标签 1表示双倍收益 0表示正常 | |
414 | + IsAuto int //当前是否自动合成 | |
415 | + IsBoxAcc int //是否处于加速生成箱子状态 | |
416 | + RandGiftNum int //当前剩余空投猫粮次数 | |
417 | + RandGiftDay int //记录当前猫粮日期,当日期变化则重置RandGiftNum | |
418 | + RandGiftTime int //记录上一次空投猫粮时间 | |
419 | + Redbag int //红包值 单位为分 | |
420 | + Head string //头像地址 | |
421 | + NickName string //昵称 | |
422 | + RealName string //实名 | |
423 | + IsFirstRedBgCat int //是否合成过红包猫 0表示否1表示是 | |
424 | + OfflineGold int64 //离线金币 | |
425 | + OfflineLove int64 //离线爱心 | |
426 | + CatShopInfo CatShopData //猫咖门店数据 | |
427 | + Taskinfo TaskData //任务数据 | |
428 | + AchieveMent AchieveMentData //成就数据 | |
429 | + PosInfo []PosData //位置信息 从0开始 | |
430 | + BuyCatInfo []BuyCatInfoData //商店购买猫数据 第一个元素为1级猫 第二个为2级猫以此类推 | |
431 | + CatRoomInfo []CatRoomData //猫咖店数据 | |
432 | + | |
433 | +} | |
434 | + | |
435 | +//仓库数据详情 | |
436 | +type WareHouseDesc struct { | |
437 | + Warelv int //红包猫等级 对应表id | |
438 | +} | |
439 | + | |
440 | +//玩家仓库数据 | |
441 | +type UserWareHouseData struct { | |
442 | + Info []WareHouseDesc //下标表示位置 | |
443 | +} | |
444 | + | |
445 | +const ( | |
446 | + TASK_TYPE_ONLINE = 1 //在线 | |
447 | + TASK_TYPE_BUYCAT = 2 //商店购买猫 | |
448 | + TASK_TYPE_MERGE = 3 //合成猫 | |
449 | + TASK_TYPE_PLAYWITHCAT = 4 //猫咪陪玩 | |
450 | + TASK_TYPE_WATCHADD = 5 //观看广告 | |
451 | + ACH_TYPE_GETCAT = 6 //累计解锁猫 | |
452 | + ACH_TYPE_GETCATJIA = 7 //累计获得猫爬架 | |
453 | + ACH_TYPE_GETCATROOMJIA = 8 //累计解锁新家具 | |
454 | + ACH_TYPE_GETSTORY = 9 //累计解锁新故事 | |
455 | + ACH_TYPE_GBESHOP = 10 //累计用于新店铺 | |
456 | +) | ... | ... |
... | ... | @@ -0,0 +1,106 @@ |
1 | +package logic | |
2 | + | |
3 | +import ( | |
4 | + "HttpServer/conf" | |
5 | + "bytes" | |
6 | + "common/logger" | |
7 | + "encoding/json" | |
8 | + "strconv" | |
9 | + | |
10 | + "io/ioutil" | |
11 | + //"log" | |
12 | + "net/http" | |
13 | +) | |
14 | + | |
15 | +//定时处理倒计时 | |
16 | +func StartHttpTicker() { | |
17 | + | |
18 | +} | |
19 | + | |
20 | +func StartHttpServe() { | |
21 | + startServerHttpServe() | |
22 | +} | |
23 | + | |
24 | +//just for test | |
25 | +func Testsendhttp() { | |
26 | + var test UserLoginData | |
27 | + //test.Uuid = 100 | |
28 | + test.Fromid = 200 | |
29 | + test.Sharetype = 1 | |
30 | + | |
31 | + client := &http.Client{} | |
32 | + | |
33 | + bys, err := json.Marshal(&test) | |
34 | + if err != nil { | |
35 | + logger.Error("testsendhttp failed=%v", err) | |
36 | + return | |
37 | + } | |
38 | + body := bytes.NewBuffer(bys) | |
39 | + url := "http://127.0.0.1:50056/cat/login" | |
40 | + reqest, err := http.NewRequest("POST", url, body) | |
41 | + if err != nil { | |
42 | + logger.Error("http.NewRequest failed") | |
43 | + } | |
44 | + reqest.Header.Add("Uuid", "101") | |
45 | + //发送 | |
46 | + //res, err := http.Post(url, "application/json;charset=utf-8", body) | |
47 | + res, err := client.Do(reqest) | |
48 | + if err != nil { | |
49 | + logger.Error(" post failed to %v err:%v data:%v", url, err, string(bys)) | |
50 | + return | |
51 | + } | |
52 | + | |
53 | + result, _ := ioutil.ReadAll(res.Body) | |
54 | + res.Body.Close() | |
55 | + | |
56 | + s := string(result) | |
57 | + var resp GetUserDataResp | |
58 | + resp.Code = 0 | |
59 | + var rdata UserLoginResp | |
60 | + _ = json.Unmarshal([]byte(s), &rdata) | |
61 | + logger.Info("testsendhttp , body:%v", rdata) | |
62 | +} | |
63 | + | |
64 | +func CheckErr(err error) { | |
65 | + if err != nil { | |
66 | + panic(err) | |
67 | + } | |
68 | +} | |
69 | + | |
70 | +func startServerHttpServe() { | |
71 | + http.HandleFunc("/cat/login", UserLogin) //登录 | |
72 | + | |
73 | + | |
74 | + err := http.ListenAndServe(conf.GetServerHttpAddrConf(), nil) | |
75 | + CheckErr(err) | |
76 | +} | |
77 | + | |
78 | + | |
79 | + | |
80 | +func UserLogin(w http.ResponseWriter, r *http.Request) { | |
81 | + //logger.Info("%%%%%%%%%%%%%%%%path=%v", *r.URL) | |
82 | + //for k, v := range r.Header { | |
83 | + // logger.Info("*********************key=%v,value=%v", k, v) | |
84 | + //} | |
85 | + | |
86 | + //w.Header().Add("Access-Control-Allow-Headers", "") | |
87 | + //w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma") | |
88 | + | |
89 | + Uuid := 0 | |
90 | + if len(r.Header) > 0 { | |
91 | + Uuid, _ = strconv.Atoi(r.Header.Get("Uuid")) | |
92 | + } | |
93 | + | |
94 | + if Uuid == 0 { | |
95 | + return | |
96 | + } | |
97 | + result, _ := ioutil.ReadAll(r.Body) | |
98 | + r.Body.Close() | |
99 | + | |
100 | + s := string(result) | |
101 | + logger.Info("UserLogin , body:%v,uuid=%v", s, Uuid) | |
102 | + | |
103 | + //HandlerLogin(w, s, Uuid) | |
104 | +} | |
105 | + | |
106 | + | ... | ... |
... | ... | @@ -0,0 +1,76 @@ |
1 | +package main | |
2 | + | |
3 | +import ( | |
4 | + "HttpServer/conf" | |
5 | + | |
6 | + "HttpServer/logic" | |
7 | + "HttpServer/redishandler" | |
8 | + "common/logger" | |
9 | + "flag" | |
10 | + "os" | |
11 | + "runtime/pprof" | |
12 | + "time" | |
13 | +) | |
14 | + | |
15 | +func InitLogger(file string, lvl int) { | |
16 | + logger.New(file, lvl, logger.Rotate{Size: logger.GB, Expired: time.Hour * 24 * 7, Interval: time.Hour * 24}) | |
17 | +} | |
18 | + | |
19 | +func main() { | |
20 | + //defer utils.PrintPanicStack() | |
21 | + filename := flag.String("conf_path", "./conf/world.xml", "config file path") | |
22 | + logpath := flag.String("logpath", "./log", "log file path") | |
23 | + cpuprofile := flag.String("cpuprofile", "", "write cpu profile to file") | |
24 | + secs := flag.Int("exitsecs", 0, "how many secs to exit, defaut 0 to never exit") | |
25 | + lvl := flag.Int("lvl", 6, "log level default -debug") | |
26 | + flag.Parse() | |
27 | + InitLogger(*logpath+"/world.log", *lvl) | |
28 | + logger.Info("config file path:%v", *filename) | |
29 | + | |
30 | + if *cpuprofile != "" { | |
31 | + f, err := os.Create(*cpuprofile) | |
32 | + if err != nil { | |
33 | + logger.Info("open cpu profile error %v:", err) | |
34 | + } else { | |
35 | + pprof.StartCPUProfile(f) | |
36 | + defer pprof.StopCPUProfile() | |
37 | + } | |
38 | + } | |
39 | + var ch chan int = make(chan int) | |
40 | + go waitFor(ch, *secs) | |
41 | + | |
42 | + err := conf.LoadConf(*filename) | |
43 | + if err != nil { | |
44 | + logger.Info("error load conf fail %v:", err) | |
45 | + return | |
46 | + } | |
47 | + | |
48 | + | |
49 | + | |
50 | + err = redishandler.Init() | |
51 | + if err != nil { | |
52 | + logger.Info("err init redis err=%v", err) | |
53 | + return | |
54 | + } | |
55 | + | |
56 | + go logic.StartHttpServe() | |
57 | + go logic.StartHttpTicker() | |
58 | + time.Sleep(time.Duration(2) * time.Second) | |
59 | + logic.Testsendhttp() | |
60 | + | |
61 | + select { | |
62 | + case _ = <-ch: | |
63 | + logger.Info("---I'm done----") | |
64 | + break | |
65 | + } | |
66 | +} | |
67 | + | |
68 | +func waitFor(ch chan int, secs int) { | |
69 | + if secs <= 0 { | |
70 | + return | |
71 | + } | |
72 | + time.Sleep(time.Duration(secs) * time.Second) | |
73 | + | |
74 | + logger.Info("-------------ready to exit--------------------") | |
75 | + ch <- 1 | |
76 | +} | ... | ... |
... | ... | @@ -0,0 +1,125 @@ |
1 | +package redishandler | |
2 | + | |
3 | +import ( | |
4 | + "HttpServer/conf" | |
5 | + "common/logger" | |
6 | + "common/redis" | |
7 | + "encoding/json" | |
8 | + "errors" | |
9 | + "strconv" | |
10 | +) | |
11 | + | |
12 | +const USER_TOKEN_KEY = "login_token" | |
13 | + | |
14 | +var gRedis *redis.RedisClient = nil | |
15 | + | |
16 | +func GetRedisClient() *redis.RedisClient { | |
17 | + return gRedis | |
18 | +} | |
19 | + | |
20 | +type TestInfo struct { | |
21 | + Gold int64 //金币 | |
22 | + Head string //头像地址 | |
23 | + NickName string //昵称 | |
24 | + Highestlv int //当前最高猫等级 | |
25 | + Uuid int //uuid | |
26 | +} | |
27 | + | |
28 | +type Mytest struct { | |
29 | + Test1 int | |
30 | + M_userInfo map[int]int | |
31 | +} | |
32 | + | |
33 | +func TestMyredis() { | |
34 | + //testmap | |
35 | + var test Mytest | |
36 | + test.Test1 = 1 | |
37 | + test.M_userInfo = make(map[int]int) | |
38 | + test.M_userInfo[1] = 1 | |
39 | + test.M_userInfo[1] = 2 | |
40 | + test.M_userInfo[2] = 2 | |
41 | + ddt, _ := json.Marshal(&test) | |
42 | + gRedis.SetString("testddt", string(ddt)) | |
43 | + logger.Info("testddt test=%v.ddt=%v", test, string(ddt)) | |
44 | + | |
45 | + rest, err := gRedis.GetString("testddt") | |
46 | + if err != nil { | |
47 | + logger.Error("TestMyredis ddtfailed err=%v", err) | |
48 | + } else { | |
49 | + var restest Mytest | |
50 | + err = json.Unmarshal([]byte(rest), &restest) | |
51 | + logger.Info("testddt rest=%v,restest=%v", rest, restest) | |
52 | + | |
53 | + } | |
54 | + //gRedis.HSet("luheng", "luhengsb", "22") | |
55 | + //gRedis.HSet("luheng", "luhengbb", "33") | |
56 | + | |
57 | + var rinfo TestInfo | |
58 | + rinfo.Gold = 100 | |
59 | + rinfo.Highestlv = 99 | |
60 | + rinfo.Head = "sb" | |
61 | + rinfo.NickName = "sd" | |
62 | + rinfo.Uuid = 1004 | |
63 | + rinfostr, _ := json.Marshal(&rinfo) | |
64 | + err = gRedis.Zadd("luhengtest", float64(rinfo.Gold), string(rinfostr)) | |
65 | + if err != nil { | |
66 | + logger.Info("TestMyredis failed err=%v", err) | |
67 | + } else { | |
68 | + logger.Info("TestMyredis success rinfostr=%v", string(rinfostr)) | |
69 | + } | |
70 | + rinfo.Gold = 1000 | |
71 | + rinfo.Highestlv = 99 | |
72 | + rinfo.Head = "sb1" | |
73 | + rinfo.NickName = "sd1" | |
74 | + rinfo.Uuid = 1005 | |
75 | + rinfostr, _ = json.Marshal(&rinfo) | |
76 | + err = gRedis.Zadd("luhengtest", float64(rinfo.Gold), string(rinfostr)) | |
77 | + rinfo.Gold = 10000 | |
78 | + rinfo.Highestlv = 99 | |
79 | + rinfo.Head = "sb2" | |
80 | + rinfo.NickName = "sd2" | |
81 | + rinfo.Uuid = 1006 | |
82 | + rinfostr, _ = json.Marshal(&rinfo) | |
83 | + err = gRedis.Zadd("luhengtest", float64(rinfo.Gold), string(rinfostr)) | |
84 | + | |
85 | + vv, err := gRedis.ZRevRangewithIndex("luhengtest", 0, -1) | |
86 | + if err == nil { | |
87 | + for k, v := range vv { | |
88 | + rinfobyte, _ := v.([]byte) | |
89 | + var rinfo TestInfo | |
90 | + //logger.Info("TestMyredis ori=%v", string(v.([]byte))) | |
91 | + err := json.Unmarshal(rinfobyte, &rinfo) | |
92 | + if err == nil { | |
93 | + logger.Info("TestMyredis k=%v,v=%v", k, rinfo) | |
94 | + } else { | |
95 | + logger.Info("TestMyredis isbigsb err=%v", err) | |
96 | + } | |
97 | + | |
98 | + } | |
99 | + } else { | |
100 | + logger.Info("test is err=%v", err) | |
101 | + } | |
102 | +} | |
103 | + | |
104 | +func Init() error { | |
105 | + redis_cfg := conf.GetRedisConf() | |
106 | + if gRedis = redis.NewRedisClient(redis_cfg.Host, redis_cfg.Password, redis_cfg.Db); gRedis == nil { | |
107 | + return errors.New("initRedis error") | |
108 | + } | |
109 | + | |
110 | + logger.Info("init redis success!") | |
111 | + TestMyredis() | |
112 | + return nil | |
113 | +} | |
114 | + | |
115 | +/* | |
116 | + 加载token | |
117 | +*/ | |
118 | +func LoadPlayerTokenFromRedis(uid int) (string, error) { | |
119 | + token, err := gRedis.HGet(USER_TOKEN_KEY, strconv.Itoa(uid)) | |
120 | + if err != nil { | |
121 | + logger.Notic("LoadPlayerTokenFromRedis failed uid:%v err%v", uid, err) | |
122 | + return "", err | |
123 | + } | |
124 | + return token, err | |
125 | +} | ... | ... |
... | ... | @@ -0,0 +1,125 @@ |
1 | +// Copyright 2014 beego Author. All Rights Reserved. | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | +// you may not use this file except in compliance with the License. | |
5 | +// You may obtain a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | +// See the License for the specific language governing permissions and | |
13 | +// limitations under the License. | |
14 | + | |
15 | +// source code is here : https://github.com/astaxie/beego/blob/master/utils/safemap.go | |
16 | + | |
17 | +//package utils | |
18 | +package beegomap | |
19 | + | |
20 | +import ( | |
21 | + "sync" | |
22 | +) | |
23 | + | |
24 | +// BeeMap is a map with lock | |
25 | +type BeeMap struct { | |
26 | + lock *sync.RWMutex | |
27 | + Bm map[uint32]interface{} | |
28 | +} | |
29 | + | |
30 | +// NewBeeMap return new safemap | |
31 | +func NewBeeMap() *BeeMap { | |
32 | + return &BeeMap{ | |
33 | + lock: new(sync.RWMutex), | |
34 | + Bm: make(map[uint32]interface{}), | |
35 | + } | |
36 | +} | |
37 | + | |
38 | +// Get from maps return the k's value | |
39 | +func (m *BeeMap) Get(k uint32) interface{} { | |
40 | + m.lock.RLock() | |
41 | + defer m.lock.RUnlock() | |
42 | + if val, ok := m.Bm[k]; ok { | |
43 | + return val | |
44 | + } | |
45 | + return nil | |
46 | +} | |
47 | + | |
48 | +// Set Maps the given key and value. Returns false | |
49 | +// if the key is already in the map and changes nothing. | |
50 | +func (m *BeeMap) Set(k uint32, v interface{}) bool { | |
51 | + m.lock.Lock() | |
52 | + defer m.lock.Unlock() | |
53 | + if val, ok := m.Bm[k]; !ok { | |
54 | + m.Bm[k] = v | |
55 | + } else if val != v { | |
56 | + m.Bm[k] = v | |
57 | + } else { | |
58 | + return false | |
59 | + } | |
60 | + return true | |
61 | +} | |
62 | + | |
63 | +//force set | |
64 | +func (m *BeeMap) Replace(k uint32, v interface{}) bool { | |
65 | + m.lock.Lock() | |
66 | + defer m.lock.Unlock() | |
67 | + delete(m.Bm, k) | |
68 | + if val, ok := m.Bm[k]; !ok { | |
69 | + m.Bm[k] = v | |
70 | + } else if val != v { | |
71 | + m.Bm[k] = v | |
72 | + } else { | |
73 | + return false | |
74 | + } | |
75 | + return true | |
76 | +} | |
77 | + | |
78 | +// Check Returns true if k is exist in the map. | |
79 | +func (m *BeeMap) Check(k uint32) bool { | |
80 | + m.lock.RLock() | |
81 | + defer m.lock.RUnlock() | |
82 | + if _, ok := m.Bm[k]; !ok { | |
83 | + return false | |
84 | + } | |
85 | + return true | |
86 | +} | |
87 | + | |
88 | +// Delete the given key and value. | |
89 | +func (m *BeeMap) Delete(k uint32) { | |
90 | + m.lock.Lock() | |
91 | + defer m.lock.Unlock() | |
92 | + delete(m.Bm, k) | |
93 | +} | |
94 | + | |
95 | +// Items returns all items in safemap. | |
96 | +func (m *BeeMap) Items() map[uint32]interface{} { | |
97 | + m.lock.RLock() | |
98 | + defer m.lock.RUnlock() | |
99 | + r := make(map[uint32]interface{}) | |
100 | + for k, v := range m.Bm { | |
101 | + r[k] = v | |
102 | + } | |
103 | + return r | |
104 | +} | |
105 | + | |
106 | +func (m *BeeMap) Len() uint32 { | |
107 | + m.lock.RLock() | |
108 | + defer m.lock.RUnlock() | |
109 | + return uint32(len(m.Bm)) | |
110 | +} | |
111 | + | |
112 | +func (m *BeeMap) Clear() { | |
113 | + items := m.Items() | |
114 | + for k := range items { | |
115 | + m.Delete(k) | |
116 | + } | |
117 | +} | |
118 | +func (m *BeeMap) GetRandomKey() uint32 { | |
119 | + m.lock.RLock() | |
120 | + defer m.lock.RUnlock() | |
121 | + for k := range m.Items() { | |
122 | + return k | |
123 | + } | |
124 | + return 0 | |
125 | +} | ... | ... |
... | ... | @@ -0,0 +1,280 @@ |
1 | +package logger | |
2 | + | |
3 | +import ( | |
4 | + "fmt" | |
5 | + "log" | |
6 | + "os" | |
7 | + "path/filepath" | |
8 | + "strconv" | |
9 | + "strings" | |
10 | + "sync" | |
11 | + "sync/atomic" | |
12 | + "time" | |
13 | +) | |
14 | + | |
15 | +type ByteSize float64 | |
16 | + | |
17 | +const ( | |
18 | + _ = iota | |
19 | + KB ByteSize = 1 << (10 * iota) | |
20 | + MB | |
21 | + GB | |
22 | + TB | |
23 | + PB | |
24 | + EB | |
25 | + ZB | |
26 | + YB | |
27 | +) | |
28 | + | |
29 | +const ( | |
30 | + checkSize = 2 * time.Minute | |
31 | + checkExpired = 2 * time.Hour | |
32 | +) | |
33 | + | |
34 | +var ( | |
35 | + stdFatal = log.New(os.Stderr, "\033[0;33mFATAL:\033[0m ", log.LstdFlags|log.Lshortfile) | |
36 | + stdError = log.New(os.Stderr, "\033[0;31mERROR:\033[0m ", log.LstdFlags|log.Lshortfile) | |
37 | + stdWarn = log.New(os.Stderr, "\033[0;35mWARN:\033[0m ", log.LstdFlags|log.Lshortfile) | |
38 | + ll *Logger | |
39 | +) | |
40 | + | |
41 | +type Logger struct { | |
42 | + errCount int32 | |
43 | + rotate Rotate | |
44 | + level chan int | |
45 | + | |
46 | + rwm sync.RWMutex | |
47 | + file *os.File | |
48 | + debug, info, notic, warn, err, fatal *log.Logger | |
49 | +} | |
50 | + | |
51 | +type Rotate struct { | |
52 | + Size ByteSize | |
53 | + Expired, Interval time.Duration | |
54 | +} | |
55 | + | |
56 | +func New(fp string, lvl int, rotate Rotate) *Logger { | |
57 | + f, err := os.OpenFile(fp, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) | |
58 | + if err != nil { | |
59 | + stdFatal.Fatalln(err.Error()) | |
60 | + } | |
61 | + ll = new(Logger) | |
62 | + ll.rotate = rotate | |
63 | + ll.file = f | |
64 | + ll.level = make(chan int) | |
65 | + go ll.loop() | |
66 | + ll.SetLevel(lvl) | |
67 | + return ll | |
68 | +} | |
69 | + | |
70 | +func (l *Logger) SetLevel(lvl int) { | |
71 | + l.level <- lvl | |
72 | +} | |
73 | + | |
74 | +func (l *Logger) setLevel(f *os.File, lvl int) { | |
75 | + switch { | |
76 | + case lvl > 5: | |
77 | + l.debug = log.New(f, "\033[0;36mDEBUG:\033[0m ", log.LstdFlags|log.Lshortfile) | |
78 | + fallthrough | |
79 | + case lvl > 4: | |
80 | + l.info = log.New(f, "INFO: ", log.LstdFlags|log.Lshortfile) | |
81 | + fallthrough | |
82 | + case lvl > 3: | |
83 | + l.notic = log.New(f, "\033[0;32mNOTIC:\033[0m ", log.LstdFlags|log.Lshortfile) | |
84 | + fallthrough | |
85 | + case lvl > 2: | |
86 | + l.warn = log.New(f, "\033[0;35mWARN:\033[0m ", log.LstdFlags|log.Lshortfile) | |
87 | + fallthrough | |
88 | + case lvl > 1: | |
89 | + l.err = log.New(f, "\033[0;31mERROR:\033[0m ", log.LstdFlags|log.Lshortfile) | |
90 | + fallthrough | |
91 | + case lvl > 0: | |
92 | + l.fatal = log.New(f, "\033[0;33mFATAL:\033[0m ", log.LstdFlags|log.Lshortfile) | |
93 | + } | |
94 | + switch { | |
95 | + case lvl < 1: | |
96 | + l.fatal = nil | |
97 | + fallthrough | |
98 | + case lvl < 2: | |
99 | + l.err = nil | |
100 | + fallthrough | |
101 | + case lvl < 3: | |
102 | + l.warn = nil | |
103 | + fallthrough | |
104 | + case lvl < 4: | |
105 | + l.notic = nil | |
106 | + fallthrough | |
107 | + case lvl < 5: | |
108 | + l.info = nil | |
109 | + fallthrough | |
110 | + case lvl < 6: | |
111 | + l.debug = nil | |
112 | + } | |
113 | +} | |
114 | + | |
115 | +func (l *Logger) getFileSize() ByteSize { | |
116 | + l.rwm.RLock() | |
117 | + defer l.rwm.RUnlock() | |
118 | + fi, err := l.file.Stat() | |
119 | + if err != nil { | |
120 | + Warn("get log file size failed, no trunc %s", err.Error()) | |
121 | + return 0.0 | |
122 | + } | |
123 | + return ByteSize(fi.Size()) | |
124 | +} | |
125 | + | |
126 | +func (l *Logger) trunc(fp, ext string, lvl int) { | |
127 | + l.rwm.Lock() | |
128 | + defer l.rwm.Unlock() | |
129 | + err := l.file.Close() | |
130 | + if err != nil { | |
131 | + stdWarn.Println("fail to close log file", err.Error()) | |
132 | + return | |
133 | + } | |
134 | + err = os.Rename(fp, fp+ext) | |
135 | + if err != nil { | |
136 | + stdWarn.Println("fail to rename log file, no trunc", err.Error()) | |
137 | + } | |
138 | + f, err := os.OpenFile(fp, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) | |
139 | + if err != nil { | |
140 | + Error("create log file failed %s", err.Error()) | |
141 | + return | |
142 | + } | |
143 | + l.setLevel(f, lvl) | |
144 | + l.file = f | |
145 | +} | |
146 | + | |
147 | +func suffix(t time.Time) string { | |
148 | + y, m, d := t.Date() | |
149 | + return "-" + fmt.Sprintf("%04d%02d%02d%02d", y, m, d, t.Hour()) | |
150 | +} | |
151 | + | |
152 | +func toNextBound(d time.Duration) time.Duration { | |
153 | + return time.Now().Truncate(d).Add(d).Sub(time.Now()) | |
154 | +} | |
155 | + | |
156 | +func (l *Logger) loop() error { | |
157 | + interval := time.After(toNextBound(l.rotate.Interval)) | |
158 | + expired := time.After(checkExpired) | |
159 | + var sizeExt, lvl int = 1, 4 | |
160 | + fp, err := filepath.Abs(l.file.Name()) | |
161 | + if err != nil { | |
162 | + stdFatal.Fatalln("get log filepath failed %s", err.Error()) | |
163 | + } | |
164 | + for { | |
165 | + var size <-chan time.Time | |
166 | + if toNextBound(l.rotate.Interval) != checkSize { | |
167 | + size = time.After(checkSize) | |
168 | + } | |
169 | + select { | |
170 | + case lvl = <-l.level: | |
171 | + l.rwm.Lock() | |
172 | + l.setLevel(l.file, lvl) | |
173 | + l.rwm.Unlock() | |
174 | + Notic("log level change to %d", lvl) | |
175 | + case t := <-interval: | |
176 | + interval = time.After(l.rotate.Interval) | |
177 | + l.trunc(fp, suffix(t), lvl) | |
178 | + sizeExt = 1 | |
179 | + Notic("log truncated by time interval") | |
180 | + case <-expired: | |
181 | + expired = time.After(checkExpired) | |
182 | + err := filepath.Walk(filepath.Dir(fp), | |
183 | + func(path string, info os.FileInfo, err error) error { | |
184 | + if err != nil { | |
185 | + return nil | |
186 | + } | |
187 | + isLog := strings.Contains(info.Name(), ".log") | |
188 | + | |
189 | + //log.Println("strings.Contains(", info.Name(), " log') isLog = ", isLog) | |
190 | + if time.Since(info.ModTime()) > l.rotate.Expired && isLog && info.IsDir() == false { | |
191 | + if err := os.Remove(path); err != nil { | |
192 | + return err | |
193 | + } | |
194 | + Notic("remove expired log files %s", filepath.Base(path)) | |
195 | + } | |
196 | + return nil | |
197 | + }) | |
198 | + if err != nil { | |
199 | + Warn("remove expired logs failed %s", err.Error()) | |
200 | + } | |
201 | + case t := <-size: | |
202 | + if l.getFileSize() < l.rotate.Size { | |
203 | + break | |
204 | + } | |
205 | + l.trunc(fp, suffix(t)+"."+strconv.Itoa(sizeExt), lvl) | |
206 | + sizeExt++ | |
207 | + Notic("log over size, truncated") | |
208 | + } | |
209 | + } | |
210 | +} | |
211 | + | |
212 | +// Debug log debug message with cyan color. | |
213 | +func Debug(format string, v ...interface{}) { | |
214 | + ll.rwm.RLock() | |
215 | + if ll.debug != nil { | |
216 | + ll.debug.Output(2, fmt.Sprintf(format, v...)) | |
217 | + } | |
218 | + ll.rwm.RUnlock() | |
219 | +} | |
220 | + | |
221 | +// Info log normal message. | |
222 | +func Info(format string, v ...interface{}) { | |
223 | + ll.rwm.RLock() | |
224 | + if ll.info != nil { | |
225 | + ll.info.Output(2, fmt.Sprintf(format, v...)) | |
226 | + } | |
227 | + ll.rwm.RUnlock() | |
228 | +} | |
229 | + | |
230 | +// Notice log notice message with blue color. | |
231 | +func Notic(format string, v ...interface{}) { | |
232 | + ll.rwm.RLock() | |
233 | + if ll.notic != nil { | |
234 | + ll.notic.Output(2, fmt.Sprintf(format, v...)) | |
235 | + } | |
236 | + ll.rwm.RUnlock() | |
237 | +} | |
238 | + | |
239 | +// Error log error message with red color. | |
240 | +func Error(format string, v ...interface{}) { | |
241 | + atomic.AddInt32(&ll.errCount, 1) | |
242 | + stdError.Output(2, fmt.Sprintf(format, v...)) | |
243 | + ll.rwm.RLock() | |
244 | + if ll.err != nil { | |
245 | + ll.err.Output(2, fmt.Sprintf(format, v...)) | |
246 | + } | |
247 | + ll.rwm.RUnlock() | |
248 | +} | |
249 | + | |
250 | +func ErrCount() int32 { | |
251 | + ec := atomic.LoadInt32(&ll.errCount) | |
252 | + if ec < 0 { | |
253 | + Warn("error count overflow") | |
254 | + return -1 | |
255 | + } | |
256 | + return ec | |
257 | +} | |
258 | + | |
259 | +func Fatal(format string, v ...interface{}) { | |
260 | + stdFatal.Output(2, fmt.Sprintf(format, v...)) | |
261 | + ll.rwm.RLock() | |
262 | + if ll.fatal != nil { | |
263 | + ll.fatal.Output(2, fmt.Sprintf(format, v...)) | |
264 | + } | |
265 | + ll.rwm.RUnlock() | |
266 | + os.Exit(1) | |
267 | +} | |
268 | + | |
269 | +func Warn(format string, v ...interface{}) { | |
270 | + stdWarn.Output(2, fmt.Sprintf(format, v...)) | |
271 | + ll.rwm.RLock() | |
272 | + if ll.warn != nil { | |
273 | + ll.warn.Output(2, fmt.Sprintf(format, v...)) | |
274 | + } | |
275 | + ll.rwm.RUnlock() | |
276 | +} | |
277 | + | |
278 | +func GetLogger() *Logger { | |
279 | + return ll | |
280 | +} | ... | ... |
... | ... | @@ -0,0 +1,13 @@ |
1 | +package redis | |
2 | + | |
3 | +const ( | |
4 | + USER_LAST_LOGIN_TIME = "CATSERVER_USER_LAST_LOGIN_TIME" //玩家上次登陆时间 | |
5 | + USER_INFO = "CATSERVER_USER_USER_INFO" //玩家数据 | |
6 | + USER_LAST_CALC_TIME = "CATSERVER_USER_LAST_CALC_TIME" //玩家上一次数据计算时间 | |
7 | + USER_INVITE_ID = "CATSERVER_USER_INVITE_ID" //玩家邀请者ID | |
8 | + USER_STARTDOUBLE_TIME = "CATSERVER_USER_STARTDOUBLE_TIME" //开启双倍时间 | |
9 | + USER_STARTAUTO_TIME = "CATSERVER_USER_STARTAUTO_TIME" //开启自动合成时间 | |
10 | + USER_STARTACC_BOX_TIME = "CATSERVER_USER_STARTACC_BOX_TIME" //开启加速成产箱子 | |
11 | + USER_WAREHOUSE_INFO = "CATSERVER_USER_WAREHOUSE_INFO" //玩家仓库信息 | |
12 | + USER_GOLD_RANK = "CATSERVER_USER_GOLD_RANK" //玩家排行榜,根据金币排 | |
13 | +) | ... | ... |
... | ... | @@ -0,0 +1,212 @@ |
1 | +package redis | |
2 | + | |
3 | +import ( | |
4 | + "sync" | |
5 | + | |
6 | + "github.com/garyburd/redigo/redis" | |
7 | + //"math/rand" | |
8 | + "errors" | |
9 | + //"sync" | |
10 | + "fmt" | |
11 | + "time" | |
12 | +) | |
13 | + | |
14 | +type subCallBack interface { | |
15 | + OnMessage(channnel string, msg []byte) | |
16 | + OnPMessage(pattern string, channel string, msg []byte) | |
17 | + OnSubscription(kind string, channel string, count int) | |
18 | + OnError(err error) | |
19 | +} | |
20 | + | |
21 | +type PubSubClient struct { | |
22 | + m_host string | |
23 | + db_index uint32 | |
24 | + password string | |
25 | + m_pool *redis.Pool | |
26 | + m_conn redis.PubSubConn | |
27 | + m_lock sync.Mutex | |
28 | + m_subCallback subCallBack | |
29 | + m_subList map[string]bool | |
30 | + m_psubList map[string]bool | |
31 | +} | |
32 | + | |
33 | +func NewPubSubClient(host string, password string, db uint32) *PubSubClient { | |
34 | + client := new(PubSubClient) | |
35 | + client.m_host = host | |
36 | + client.db_index = db | |
37 | + client.password = password | |
38 | + client.m_subList = make(map[string]bool) | |
39 | + client.m_psubList = make(map[string]bool) | |
40 | + client.m_pool = &redis.Pool{ | |
41 | + MaxIdle: 64, | |
42 | + //MaxActive: 1, | |
43 | + IdleTimeout: 60 * time.Second, | |
44 | + TestOnBorrow: func(c redis.Conn, t time.Time) error { | |
45 | + _, err := c.Do("PING") | |
46 | + | |
47 | + return err | |
48 | + }, | |
49 | + Dial: func() (redis.Conn, error) { | |
50 | + c, err := redis.Dial("tcp", host, redis.DialDatabase(int(db)), redis.DialPassword(password)) | |
51 | + if err != nil { | |
52 | + return nil, err | |
53 | + } | |
54 | + return c, err | |
55 | + }, | |
56 | + } | |
57 | + | |
58 | + conn, err := redis.Dial("tcp", host, redis.DialDatabase(int(db)), redis.DialPassword(password)) | |
59 | + if err != nil { | |
60 | + fmt.Printf("error dial:%v", err) | |
61 | + return nil | |
62 | + } | |
63 | + client.m_conn = redis.PubSubConn{Conn: conn} | |
64 | + return client | |
65 | +} | |
66 | + | |
67 | +func (client *PubSubClient) Publish(channel, value interface{}) error { | |
68 | + conn := client.m_pool.Get() | |
69 | + defer conn.Close() | |
70 | + if _, err := conn.Do("PUBLISH", channel, value); err != nil { | |
71 | + return err | |
72 | + } | |
73 | + return nil | |
74 | + | |
75 | +} | |
76 | + | |
77 | +func (client *PubSubClient) Subscribe(channel string) error { | |
78 | + client.m_lock.Lock() | |
79 | + defer client.m_lock.Unlock() | |
80 | + if err := client.m_conn.Subscribe(channel); err != nil { | |
81 | + return err | |
82 | + } | |
83 | + client.m_subList[channel] = true | |
84 | + return nil | |
85 | +} | |
86 | + | |
87 | +func (client *PubSubClient) Unsubscribe(channel string) error { | |
88 | + //logger.DEBUG("Unsubscribe, channName:%s\n", channel) | |
89 | + client.m_lock.Lock() | |
90 | + defer client.m_lock.Unlock() | |
91 | + if channel == "" { | |
92 | + if err := client.m_conn.Unsubscribe(); err != nil { | |
93 | + return err | |
94 | + } | |
95 | + // clear all element in this map | |
96 | + for k := range client.m_subList { | |
97 | + delete(client.m_subList, k) | |
98 | + } | |
99 | + } else { | |
100 | + if err := client.m_conn.Unsubscribe(channel); err != nil { | |
101 | + return err | |
102 | + } | |
103 | + delete(client.m_subList, channel) | |
104 | + } | |
105 | + return nil | |
106 | +} | |
107 | + | |
108 | +func (client *PubSubClient) PSubscribe(channel string) error { | |
109 | + client.m_lock.Lock() | |
110 | + defer client.m_lock.Unlock() | |
111 | + if err := client.m_conn.PSubscribe(channel); err != nil { | |
112 | + return err | |
113 | + } | |
114 | + client.m_psubList[channel] = true | |
115 | + return nil | |
116 | +} | |
117 | + | |
118 | +func (client *PubSubClient) PUnsubscribe(channel string) error { | |
119 | + client.m_lock.Lock() | |
120 | + defer client.m_lock.Unlock() | |
121 | + if channel == "" { | |
122 | + if err := client.m_conn.PUnsubscribe(); err != nil { | |
123 | + return err | |
124 | + } | |
125 | + // clear all element in this map | |
126 | + for k := range client.m_subList { | |
127 | + delete(client.m_psubList, k) | |
128 | + } | |
129 | + } else { | |
130 | + if err := client.m_conn.PUnsubscribe(channel); err != nil { | |
131 | + return err | |
132 | + } | |
133 | + delete(client.m_subList, channel) | |
134 | + } | |
135 | + return nil | |
136 | +} | |
137 | + | |
138 | +func (client *PubSubClient) SetSubCallback(cb subCallBack) { | |
139 | + client.m_subCallback = cb | |
140 | +} | |
141 | + | |
142 | +func (client *PubSubClient) reConnect() { | |
143 | + | |
144 | + //1. first close old connection | |
145 | + client.m_conn.Close() | |
146 | + | |
147 | + //2. create a new connection | |
148 | + for { | |
149 | + // conn, err := redis.Dial("tcp", client.m_host) | |
150 | + conn, err := redis.Dial("tcp", client.m_host, redis.DialDatabase(int(client.db_index)), redis.DialPassword(client.password)) | |
151 | + if err == nil { | |
152 | + client.m_conn = redis.PubSubConn{Conn: conn} | |
153 | + return | |
154 | + } | |
155 | + time.Sleep(time.Second) | |
156 | + //logger.DEBUG("reConnecting\n") | |
157 | + } | |
158 | +} | |
159 | + | |
160 | +func (client *PubSubClient) reSubscribe() { | |
161 | + for key := range client.m_subList { | |
162 | + client.Subscribe(key) | |
163 | + } | |
164 | + for key := range client.m_psubList { | |
165 | + client.PSubscribe(key) | |
166 | + } | |
167 | +} | |
168 | + | |
169 | +func (client *PubSubClient) StartSubscribe(cb subCallBack) error { | |
170 | + if cb == nil { | |
171 | + return errors.New("subCallback is not set!") | |
172 | + } | |
173 | + client.m_subCallback = cb | |
174 | + go func() { | |
175 | + //exit_loop: | |
176 | + for { | |
177 | + switch n := client.m_conn.Receive().(type) { | |
178 | + case redis.Message: | |
179 | + //logger.DEBUG("Message: chann[%s] data[%s]\n", n.Channel, n.Data) | |
180 | + if client.m_subCallback.OnMessage != nil { | |
181 | + client.m_subCallback.OnMessage(n.Channel, n.Data) | |
182 | + } | |
183 | + case redis.PMessage: | |
184 | + //logger.DEBUG("PMessage: pattern[%s] chann[%s] data[%s]\n", n.Pattern, n.Channel, n.Data) | |
185 | + if client.m_subCallback.OnPMessage != nil { | |
186 | + client.m_subCallback.OnPMessage(n.Pattern, n.Channel, n.Data) | |
187 | + } | |
188 | + case redis.Subscription: | |
189 | + //logger.DEBUG("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count) | |
190 | + if client.m_subCallback.OnSubscription != nil { | |
191 | + client.m_subCallback.OnSubscription(n.Kind, n.Channel, n.Count) | |
192 | + } | |
193 | + if n.Count == 0 { | |
194 | + //logger.DEBUG("exit ddd: \n") | |
195 | + //break exit_loop | |
196 | + } | |
197 | + case error: | |
198 | + if client.m_subCallback.OnError != nil { | |
199 | + client.m_subCallback.OnError(n) | |
200 | + } | |
201 | + //logger.DEBUG("error: %v\n", n) | |
202 | + client.reConnect() | |
203 | + client.reSubscribe() | |
204 | + //client.m_conn = redis.PubSubConn{Conn: m_psPool.Get()} | |
205 | + //break exit_loop | |
206 | + } | |
207 | + } | |
208 | + //logger.DEBUG("exit: \n") | |
209 | + }() | |
210 | + | |
211 | + return nil | |
212 | +} | ... | ... |
... | ... | @@ -0,0 +1,408 @@ |
1 | +package redis | |
2 | + | |
3 | +import ( | |
4 | + "fmt" | |
5 | + | |
6 | + "github.com/garyburd/redigo/redis" | |
7 | + //"math/rand" | |
8 | + | |
9 | + "strconv" | |
10 | + //"sync" | |
11 | + "errors" | |
12 | + "time" | |
13 | +) | |
14 | + | |
15 | +type RedisClient struct { | |
16 | + m_pool *redis.Pool | |
17 | +} | |
18 | + | |
19 | +func NewRedisClient(host string, password string, db_index uint32) *RedisClient { | |
20 | + client := new(RedisClient) | |
21 | + client.m_pool = &redis.Pool{ | |
22 | + MaxIdle: 64, | |
23 | + IdleTimeout: 60 * time.Second, | |
24 | + TestOnBorrow: func(c redis.Conn, t time.Time) error { | |
25 | + _, err := c.Do("PING") | |
26 | + | |
27 | + return err | |
28 | + }, | |
29 | + Dial: func() (redis.Conn, error) { | |
30 | + c, err := redis.Dial("tcp", host) | |
31 | + if err != nil { | |
32 | + return nil, err | |
33 | + } | |
34 | + if password != "" { | |
35 | + if _, err := c.Do("AUTH", password); err != nil { | |
36 | + c.Close() | |
37 | + return nil, err | |
38 | + } | |
39 | + } | |
40 | + _, err = c.Do("SELECT", db_index) | |
41 | + if err != nil { | |
42 | + return nil, err | |
43 | + } | |
44 | + | |
45 | + return c, err | |
46 | + }, | |
47 | + } | |
48 | + return client | |
49 | +} | |
50 | + | |
51 | +// 写入值永不过期 | |
52 | +func (client *RedisClient) SetString(key, value string) error { | |
53 | + conn := client.m_pool.Get() | |
54 | + defer conn.Close() | |
55 | + _, err := conn.Do("SET", key, value) | |
56 | + if err != nil { | |
57 | + //fmt.Println("redis set failed:", err) | |
58 | + } | |
59 | + return err | |
60 | +} | |
61 | + | |
62 | +func (client *RedisClient) SetBytes(key string, bys []byte) error { | |
63 | + conn := client.m_pool.Get() | |
64 | + defer conn.Close() | |
65 | + _, err := conn.Do("SET", key, bys) | |
66 | + if err != nil { | |
67 | + //fmt.Println("redis set failed:", err) | |
68 | + } | |
69 | + return err | |
70 | +} | |
71 | + | |
72 | +func (client *RedisClient) SetExString(key, value string, expireTime int /* seconds*/) error { | |
73 | + conn := client.m_pool.Get() | |
74 | + defer conn.Close() | |
75 | + expStr := strconv.Itoa(expireTime) | |
76 | + _, err := conn.Do("SETEX", key, expStr, value) | |
77 | + if err != nil { | |
78 | + //fmt.Println("redis set failed:", err) | |
79 | + } | |
80 | + return err | |
81 | +} | |
82 | + | |
83 | +func (client *RedisClient) Delete(key string) error { | |
84 | + conn := client.m_pool.Get() | |
85 | + defer conn.Close() | |
86 | + _, err := conn.Do("DEL", key) | |
87 | + if err != nil { | |
88 | + //fmt.Println("redis set failed:", err) | |
89 | + } | |
90 | + return err | |
91 | +} | |
92 | + | |
93 | +func (client *RedisClient) Expire(key string, expireTime int /* seconds*/) error { | |
94 | + conn := client.m_pool.Get() | |
95 | + defer conn.Close() | |
96 | + expStr := strconv.Itoa(expireTime) | |
97 | + _, err := conn.Do("EXPIRE", key, expStr) | |
98 | + if err != nil { | |
99 | + //fmt.Println("redis set failed:", err) | |
100 | + } | |
101 | + return err | |
102 | +} | |
103 | + | |
104 | +func (client *RedisClient) GetKeys(pattern string) ([]string, error) { | |
105 | + conn := client.m_pool.Get() | |
106 | + defer conn.Close() | |
107 | + value, err := redis.Strings(conn.Do("keys", pattern)) | |
108 | + if err != nil { | |
109 | + //fmt.Println("redis get failed:", err) | |
110 | + } | |
111 | + return value, err | |
112 | +} | |
113 | + | |
114 | +func (client *RedisClient) GetString(key string) (string, error) { | |
115 | + conn := client.m_pool.Get() | |
116 | + defer conn.Close() | |
117 | + value, err := redis.String(conn.Do("GET", key)) | |
118 | + if err != nil { | |
119 | + //fmt.Println("redis get failed:", err) | |
120 | + } | |
121 | + return value, err | |
122 | +} | |
123 | +func (client *RedisClient) GetBytes(key string) ([]byte, error) { | |
124 | + conn := client.m_pool.Get() | |
125 | + defer conn.Close() | |
126 | + value, err := conn.Do("GET", key) | |
127 | + if err != nil { | |
128 | + fmt.Println("redis get failed:", err) | |
129 | + } | |
130 | + return value.([]byte), err | |
131 | +} | |
132 | + | |
133 | +func (client *RedisClient) GetInt64(key string) (int64, error) { | |
134 | + conn := client.m_pool.Get() | |
135 | + defer conn.Close() | |
136 | + value, err := redis.Int64(conn.Do("GET", key)) | |
137 | + if err != nil { | |
138 | + //fmt.Println("redis get failed:", err) | |
139 | + } | |
140 | + return value, err | |
141 | +} | |
142 | + | |
143 | +func (client *RedisClient) GetUint64(key string) (uint64, error) { | |
144 | + conn := client.m_pool.Get() | |
145 | + defer conn.Close() | |
146 | + value, err := redis.Uint64(conn.Do("GET", key)) | |
147 | + if err != nil { | |
148 | + //fmt.Println("redis get failed:", err) | |
149 | + } | |
150 | + return value, err | |
151 | +} | |
152 | + | |
153 | +func (client *RedisClient) GetInt(key string) (int, error) { | |
154 | + conn := client.m_pool.Get() | |
155 | + defer conn.Close() | |
156 | + value, err := redis.Int(conn.Do("GET", key)) | |
157 | + if err != nil { | |
158 | + //fmt.Println("redis get failed:", err) | |
159 | + } | |
160 | + return value, err | |
161 | +} | |
162 | + | |
163 | +func (client *RedisClient) GetBool(key string) (bool, error) { | |
164 | + conn := client.m_pool.Get() | |
165 | + defer conn.Close() | |
166 | + value, err := redis.Bool(conn.Do("GET", key)) | |
167 | + if err != nil { | |
168 | + //fmt.Println("redis get failed:", err) | |
169 | + } | |
170 | + return value, err | |
171 | +} | |
172 | + | |
173 | +func (client *RedisClient) HExists(key string, field string) (bool, error) { | |
174 | + conn := client.m_pool.Get() | |
175 | + defer conn.Close() | |
176 | + value, err := redis.Bool(conn.Do("HEXISTS", key, field)) | |
177 | + if err != nil { | |
178 | + //fmt.Println("redis get failed:", err) | |
179 | + } | |
180 | + return value, err | |
181 | +} | |
182 | + | |
183 | +func (client *RedisClient) Exists(key string) (bool, error) { | |
184 | + conn := client.m_pool.Get() | |
185 | + defer conn.Close() | |
186 | + value, err := redis.Bool(conn.Do("EXISTS", key)) | |
187 | + if err != nil { | |
188 | + //fmt.Println("redis get failed:", err) | |
189 | + } | |
190 | + return value, err | |
191 | +} | |
192 | + | |
193 | +func (client *RedisClient) HMSet(key string, data interface{}) error { | |
194 | + conn := client.m_pool.Get() | |
195 | + defer conn.Close() | |
196 | + if _, err := conn.Do("HMSET", redis.Args{}.Add(key).AddFlat(data)...); err != nil { | |
197 | + //fmt.Println("redis set failed:", err) | |
198 | + return err | |
199 | + } | |
200 | + | |
201 | + return nil | |
202 | +} | |
203 | + | |
204 | +func (client *RedisClient) HSet(key, field, value string) error { | |
205 | + conn := client.m_pool.Get() | |
206 | + defer conn.Close() | |
207 | + if _, err := conn.Do("HSET", key, field, value); err != nil { | |
208 | + //fmt.Println("redis set failed:", err) | |
209 | + return err | |
210 | + } | |
211 | + | |
212 | + return nil | |
213 | +} | |
214 | + | |
215 | +func (client *RedisClient) Zadd(key string, score float64, member string) error { | |
216 | + conn := client.m_pool.Get() | |
217 | + defer conn.Close() | |
218 | + if _, err := conn.Do("ZADD", key, score, member); err != nil { | |
219 | + //fmt.Println("redis set failed:", err) | |
220 | + return err | |
221 | + } | |
222 | + | |
223 | + return nil | |
224 | +} | |
225 | + | |
226 | +func (client *RedisClient) ZRevRangewithIndex(key string, start, end int) ([]interface{}, error) { | |
227 | + conn := client.m_pool.Get() | |
228 | + defer conn.Close() | |
229 | + v, err := redis.Values(conn.Do("ZREVRANGE", key, start, end)) | |
230 | + return v, err | |
231 | +} | |
232 | + | |
233 | +func (client *RedisClient) HSetBytes(key, field string, value []byte) error { | |
234 | + conn := client.m_pool.Get() | |
235 | + defer conn.Close() | |
236 | + if _, err := conn.Do("HSET", key, field, value); err != nil { | |
237 | + //fmt.Println("redis set failed:", err) | |
238 | + return err | |
239 | + } | |
240 | + | |
241 | + return nil | |
242 | +} | |
243 | + | |
244 | +func (client *RedisClient) HDel(key, field string) error { | |
245 | + conn := client.m_pool.Get() | |
246 | + defer conn.Close() | |
247 | + if _, err := conn.Do("HDEL", key, field); err != nil { | |
248 | + return err | |
249 | + } | |
250 | + | |
251 | + return nil | |
252 | +} | |
253 | + | |
254 | +func (client *RedisClient) HGetAll(key string, data interface{}) error { | |
255 | + conn := client.m_pool.Get() | |
256 | + defer conn.Close() | |
257 | + v, err := redis.Values(conn.Do("HGETALL", key)) | |
258 | + if err != nil { | |
259 | + //fmt.Println("redis set failed:", err) | |
260 | + return err | |
261 | + } | |
262 | + if err := redis.ScanStruct(v, data); err != nil { | |
263 | + return err | |
264 | + } | |
265 | + return nil | |
266 | +} | |
267 | + | |
268 | +func (client *RedisClient) HGetAllValues(key string) ([]interface{}, error) { | |
269 | + conn := client.m_pool.Get() | |
270 | + defer conn.Close() | |
271 | + v, err := redis.Values(conn.Do("HGETALL", key)) | |
272 | + return v, err | |
273 | +} | |
274 | + | |
275 | +func (client *RedisClient) HGetAllKeys(key string) ([]interface{}, error) { | |
276 | + conn := client.m_pool.Get() | |
277 | + defer conn.Close() | |
278 | + v, err := redis.Values(conn.Do("hkeys", key)) | |
279 | + return v, err | |
280 | +} | |
281 | + | |
282 | +func (client *RedisClient) HGet(key string, field string) (string, error) { | |
283 | + conn := client.m_pool.Get() | |
284 | + defer conn.Close() | |
285 | + value, err := redis.String(conn.Do("HGET", key, field)) | |
286 | + if err != nil { | |
287 | + //fmt.Println("redis set failed:", err) | |
288 | + return "", err | |
289 | + } | |
290 | + return value, nil | |
291 | +} | |
292 | +func (client *RedisClient) HGetBytes(key, field string) ([]byte, error) { | |
293 | + conn := client.m_pool.Get() | |
294 | + defer conn.Close() | |
295 | + value, err := conn.Do("HGET", key, field) | |
296 | + if err != nil { | |
297 | + fmt.Println("redis hget failed:", err) | |
298 | + return []byte{}, err | |
299 | + } | |
300 | + if value == nil { | |
301 | + return []byte{}, errors.New("value nil") | |
302 | + } | |
303 | + return value.([]byte), err | |
304 | +} | |
305 | + | |
306 | +func (client *RedisClient) HGetInt(key string, field string) (int, error) { | |
307 | + conn := client.m_pool.Get() | |
308 | + defer conn.Close() | |
309 | + value, err := redis.Int(conn.Do("HGET", key, field)) | |
310 | + if err != nil { | |
311 | + //fmt.Println("redis set failed:", err) | |
312 | + return 0, err | |
313 | + } | |
314 | + return value, nil | |
315 | +} | |
316 | + | |
317 | +func (client *RedisClient) LPop(listName string) (string, error) { | |
318 | + conn := client.m_pool.Get() | |
319 | + defer conn.Close() | |
320 | + value, err := redis.String(conn.Do("LPOP", listName)) | |
321 | + if err != nil { | |
322 | + //fmt.Println("LPop redis get failed:", err) | |
323 | + } | |
324 | + return value, err | |
325 | +} | |
326 | + | |
327 | +func (client *RedisClient) LRem(listName string, count int, value string) error { | |
328 | + conn := client.m_pool.Get() | |
329 | + defer conn.Close() | |
330 | + cntStr := strconv.Itoa(count) | |
331 | + _, err := redis.Int(conn.Do("LREM", listName, cntStr, value)) | |
332 | + if err != nil { | |
333 | + //fmt.Println("LPop redis get failed:", err) | |
334 | + } | |
335 | + return err | |
336 | +} | |
337 | + | |
338 | +func (client *RedisClient) RPush(listName string, element string) error { | |
339 | + conn := client.m_pool.Get() | |
340 | + defer conn.Close() | |
341 | + _, err := redis.Int(conn.Do("RPUSH", listName, element)) | |
342 | + if err != nil { | |
343 | + //fmt.Println("RPush redis get failed:", err) | |
344 | + } | |
345 | + return err | |
346 | +} | |
347 | + | |
348 | +func (client *RedisClient) LLen(listName string) (int, error) { | |
349 | + conn := client.m_pool.Get() | |
350 | + defer conn.Close() | |
351 | + value, err := redis.Int(conn.Do("LLEN", listName)) | |
352 | + if err != nil { | |
353 | + //fmt.Println("LLEN redis get failed:", err) | |
354 | + } | |
355 | + return value, err | |
356 | +} | |
357 | + | |
358 | +func (client *RedisClient) LIndex(listName string, index int) (string, error) { | |
359 | + conn := client.m_pool.Get() | |
360 | + defer conn.Close() | |
361 | + idxStr := strconv.Itoa(index) | |
362 | + value, err := redis.String(conn.Do("LINDEX", listName, idxStr)) | |
363 | + if err != nil { | |
364 | + //fmt.Println("LIndex redis get failed:", err) | |
365 | + } | |
366 | + return value, err | |
367 | +} | |
368 | + | |
369 | +func (client *RedisClient) LRange(listName string, start int, stop int) ([]string, error) { | |
370 | + conn := client.m_pool.Get() | |
371 | + defer conn.Close() | |
372 | + value, err := redis.Strings(conn.Do("LRANGE", listName, start, stop)) | |
373 | + if err != nil { | |
374 | + //fmt.Println("redis get failed:", err) | |
375 | + } | |
376 | + return value, err | |
377 | +} | |
378 | + | |
379 | +func (client *RedisClient) Incr(key string) (uint64, error) { | |
380 | + conn := client.m_pool.Get() | |
381 | + defer conn.Close() | |
382 | + value, err := redis.Uint64(conn.Do("INCR", key)) | |
383 | + if err != nil { | |
384 | + //fmt.Println("RPush redis get failed:", err) | |
385 | + } | |
386 | + return value, err | |
387 | +} | |
388 | + | |
389 | +func (client *RedisClient) IncrBy(key string, incr int) (uint64, error) { | |
390 | + conn := client.m_pool.Get() | |
391 | + defer conn.Close() | |
392 | + value, err := redis.Uint64(conn.Do("INCRBY", key, incr)) | |
393 | + if err != nil { | |
394 | + fmt.Println("redis get failed:", err) | |
395 | + } | |
396 | + return value, err | |
397 | +} | |
398 | + | |
399 | +func (client *RedisClient) HMGets(key string) ([]interface{}, error) { | |
400 | + conn := client.m_pool.Get() | |
401 | + defer conn.Close() | |
402 | + v, err := redis.Values(conn.Do("HGETALL", key)) | |
403 | + if err != nil { | |
404 | + fmt.Println("redis get failed:", err) | |
405 | + return nil, err | |
406 | + } | |
407 | + return v, err | |
408 | +} | ... | ... |
No preview for this file type
src/github.com/garyburd/redigo/.github/ISSUE_TEMPLATE.md
0 → 100644
... | ... | @@ -0,0 +1 @@ |
1 | +Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis | ... | ... |
... | ... | @@ -0,0 +1,19 @@ |
1 | +language: go | |
2 | +sudo: false | |
3 | +services: | |
4 | + - redis-server | |
5 | + | |
6 | +go: | |
7 | + - 1.4 | |
8 | + - 1.5 | |
9 | + - 1.6 | |
10 | + - 1.7 | |
11 | + - 1.8 | |
12 | + - 1.9 | |
13 | + - tip | |
14 | + | |
15 | +script: | |
16 | + - go get -t -v ./... | |
17 | + - diff -u <(echo -n) <(gofmt -d .) | |
18 | + - go vet $(go list ./... | grep -v /vendor/) | |
19 | + - go test -v -race ./... | ... | ... |
... | ... | @@ -0,0 +1,175 @@ |
1 | + | |
2 | + Apache License | |
3 | + Version 2.0, January 2004 | |
4 | + http://www.apache.org/licenses/ | |
5 | + | |
6 | + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
7 | + | |
8 | + 1. Definitions. | |
9 | + | |
10 | + "License" shall mean the terms and conditions for use, reproduction, | |
11 | + and distribution as defined by Sections 1 through 9 of this document. | |
12 | + | |
13 | + "Licensor" shall mean the copyright owner or entity authorized by | |
14 | + the copyright owner that is granting the License. | |
15 | + | |
16 | + "Legal Entity" shall mean the union of the acting entity and all | |
17 | + other entities that control, are controlled by, or are under common | |
18 | + control with that entity. For the purposes of this definition, | |
19 | + "control" means (i) the power, direct or indirect, to cause the | |
20 | + direction or management of such entity, whether by contract or | |
21 | + otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
22 | + outstanding shares, or (iii) beneficial ownership of such entity. | |
23 | + | |
24 | + "You" (or "Your") shall mean an individual or Legal Entity | |
25 | + exercising permissions granted by this License. | |
26 | + | |
27 | + "Source" form shall mean the preferred form for making modifications, | |
28 | + including but not limited to software source code, documentation | |
29 | + source, and configuration files. | |
30 | + | |
31 | + "Object" form shall mean any form resulting from mechanical | |
32 | + transformation or translation of a Source form, including but | |
33 | + not limited to compiled object code, generated documentation, | |
34 | + and conversions to other media types. | |
35 | + | |
36 | + "Work" shall mean the work of authorship, whether in Source or | |
37 | + Object form, made available under the License, as indicated by a | |
38 | + copyright notice that is included in or attached to the work | |
39 | + (an example is provided in the Appendix below). | |
40 | + | |
41 | + "Derivative Works" shall mean any work, whether in Source or Object | |
42 | + form, that is based on (or derived from) the Work and for which the | |
43 | + editorial revisions, annotations, elaborations, or other modifications | |
44 | + represent, as a whole, an original work of authorship. For the purposes | |
45 | + of this License, Derivative Works shall not include works that remain | |
46 | + separable from, or merely link (or bind by name) to the interfaces of, | |
47 | + the Work and Derivative Works thereof. | |
48 | + | |
49 | + "Contribution" shall mean any work of authorship, including | |
50 | + the original version of the Work and any modifications or additions | |
51 | + to that Work or Derivative Works thereof, that is intentionally | |
52 | + submitted to Licensor for inclusion in the Work by the copyright owner | |
53 | + or by an individual or Legal Entity authorized to submit on behalf of | |
54 | + the copyright owner. For the purposes of this definition, "submitted" | |
55 | + means any form of electronic, verbal, or written communication sent | |
56 | + to the Licensor or its representatives, including but not limited to | |
57 | + communication on electronic mailing lists, source code control systems, | |
58 | + and issue tracking systems that are managed by, or on behalf of, the | |
59 | + Licensor for the purpose of discussing and improving the Work, but | |
60 | + excluding communication that is conspicuously marked or otherwise | |
61 | + designated in writing by the copyright owner as "Not a Contribution." | |
62 | + | |
63 | + "Contributor" shall mean Licensor and any individual or Legal Entity | |
64 | + on behalf of whom a Contribution has been received by Licensor and | |
65 | + subsequently incorporated within the Work. | |
66 | + | |
67 | + 2. Grant of Copyright License. Subject to the terms and conditions of | |
68 | + this License, each Contributor hereby grants to You a perpetual, | |
69 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
70 | + copyright license to reproduce, prepare Derivative Works of, | |
71 | + publicly display, publicly perform, sublicense, and distribute the | |
72 | + Work and such Derivative Works in Source or Object form. | |
73 | + | |
74 | + 3. Grant of Patent License. Subject to the terms and conditions of | |
75 | + this License, each Contributor hereby grants to You a perpetual, | |
76 | + worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
77 | + (except as stated in this section) patent license to make, have made, | |
78 | + use, offer to sell, sell, import, and otherwise transfer the Work, | |
79 | + where such license applies only to those patent claims licensable | |
80 | + by such Contributor that are necessarily infringed by their | |
81 | + Contribution(s) alone or by combination of their Contribution(s) | |
82 | + with the Work to which such Contribution(s) was submitted. If You | |
83 | + institute patent litigation against any entity (including a | |
84 | + cross-claim or counterclaim in a lawsuit) alleging that the Work | |
85 | + or a Contribution incorporated within the Work constitutes direct | |
86 | + or contributory patent infringement, then any patent licenses | |
87 | + granted to You under this License for that Work shall terminate | |
88 | + as of the date such litigation is filed. | |
89 | + | |
90 | + 4. Redistribution. You may reproduce and distribute copies of the | |
91 | + Work or Derivative Works thereof in any medium, with or without | |
92 | + modifications, and in Source or Object form, provided that You | |
93 | + meet the following conditions: | |
94 | + | |
95 | + (a) You must give any other recipients of the Work or | |
96 | + Derivative Works a copy of this License; and | |
97 | + | |
98 | + (b) You must cause any modified files to carry prominent notices | |
99 | + stating that You changed the files; and | |
100 | + | |
101 | + (c) You must retain, in the Source form of any Derivative Works | |
102 | + that You distribute, all copyright, patent, trademark, and | |
103 | + attribution notices from the Source form of the Work, | |
104 | + excluding those notices that do not pertain to any part of | |
105 | + the Derivative Works; and | |
106 | + | |
107 | + (d) If the Work includes a "NOTICE" text file as part of its | |
108 | + distribution, then any Derivative Works that You distribute must | |
109 | + include a readable copy of the attribution notices contained | |
110 | + within such NOTICE file, excluding those notices that do not | |
111 | + pertain to any part of the Derivative Works, in at least one | |
112 | + of the following places: within a NOTICE text file distributed | |
113 | + as part of the Derivative Works; within the Source form or | |
114 | + documentation, if provided along with the Derivative Works; or, | |
115 | + within a display generated by the Derivative Works, if and | |
116 | + wherever such third-party notices normally appear. The contents | |
117 | + of the NOTICE file are for informational purposes only and | |
118 | + do not modify the License. You may add Your own attribution | |
119 | + notices within Derivative Works that You distribute, alongside | |
120 | + or as an addendum to the NOTICE text from the Work, provided | |
121 | + that such additional attribution notices cannot be construed | |
122 | + as modifying the License. | |
123 | + | |
124 | + You may add Your own copyright statement to Your modifications and | |
125 | + may provide additional or different license terms and conditions | |
126 | + for use, reproduction, or distribution of Your modifications, or | |
127 | + for any such Derivative Works as a whole, provided Your use, | |
128 | + reproduction, and distribution of the Work otherwise complies with | |
129 | + the conditions stated in this License. | |
130 | + | |
131 | + 5. Submission of Contributions. Unless You explicitly state otherwise, | |
132 | + any Contribution intentionally submitted for inclusion in the Work | |
133 | + by You to the Licensor shall be under the terms and conditions of | |
134 | + this License, without any additional terms or conditions. | |
135 | + Notwithstanding the above, nothing herein shall supersede or modify | |
136 | + the terms of any separate license agreement you may have executed | |
137 | + with Licensor regarding such Contributions. | |
138 | + | |
139 | + 6. Trademarks. This License does not grant permission to use the trade | |
140 | + names, trademarks, service marks, or product names of the Licensor, | |
141 | + except as required for reasonable and customary use in describing the | |
142 | + origin of the Work and reproducing the content of the NOTICE file. | |
143 | + | |
144 | + 7. Disclaimer of Warranty. Unless required by applicable law or | |
145 | + agreed to in writing, Licensor provides the Work (and each | |
146 | + Contributor provides its Contributions) on an "AS IS" BASIS, | |
147 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
148 | + implied, including, without limitation, any warranties or conditions | |
149 | + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
150 | + PARTICULAR PURPOSE. You are solely responsible for determining the | |
151 | + appropriateness of using or redistributing the Work and assume any | |
152 | + risks associated with Your exercise of permissions under this License. | |
153 | + | |
154 | + 8. Limitation of Liability. In no event and under no legal theory, | |
155 | + whether in tort (including negligence), contract, or otherwise, | |
156 | + unless required by applicable law (such as deliberate and grossly | |
157 | + negligent acts) or agreed to in writing, shall any Contributor be | |
158 | + liable to You for damages, including any direct, indirect, special, | |
159 | + incidental, or consequential damages of any character arising as a | |
160 | + result of this License or out of the use or inability to use the | |
161 | + Work (including but not limited to damages for loss of goodwill, | |
162 | + work stoppage, computer failure or malfunction, or any and all | |
163 | + other commercial damages or losses), even if such Contributor | |
164 | + has been advised of the possibility of such damages. | |
165 | + | |
166 | + 9. Accepting Warranty or Additional Liability. While redistributing | |
167 | + the Work or Derivative Works thereof, You may choose to offer, | |
168 | + and charge a fee for, acceptance of support, warranty, indemnity, | |
169 | + or other liability obligations and/or rights consistent with this | |
170 | + License. However, in accepting such obligations, You may act only | |
171 | + on Your own behalf and on Your sole responsibility, not on behalf | |
172 | + of any other Contributor, and only if You agree to indemnify, | |
173 | + defend, and hold each Contributor harmless for any liability | |
174 | + incurred by, or claims asserted against, such Contributor by reason | |
175 | + of your accepting any such warranty or additional liability. | ... | ... |
... | ... | @@ -0,0 +1,51 @@ |
1 | +Redigo | |
2 | +====== | |
3 | + | |
4 | +[](https://travis-ci.org/garyburd/redigo) | |
5 | +[](https://godoc.org/github.com/garyburd/redigo/redis) | |
6 | + | |
7 | +Redigo is a [Go](http://golang.org/) client for the [Redis](http://redis.io/) database. | |
8 | + | |
9 | +Features | |
10 | +------- | |
11 | + | |
12 | +* A [Print-like](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Executing_Commands) API with support for all Redis commands. | |
13 | +* [Pipelining](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Pipelining), including pipelined transactions. | |
14 | +* [Publish/Subscribe](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Publish_and_Subscribe). | |
15 | +* [Connection pooling](http://godoc.org/github.com/garyburd/redigo/redis#Pool). | |
16 | +* [Script helper type](http://godoc.org/github.com/garyburd/redigo/redis#Script) with optimistic use of EVALSHA. | |
17 | +* [Helper functions](http://godoc.org/github.com/garyburd/redigo/redis#hdr-Reply_Helpers) for working with command replies. | |
18 | + | |
19 | +Documentation | |
20 | +------------- | |
21 | + | |
22 | +- [API Reference](http://godoc.org/github.com/garyburd/redigo/redis) | |
23 | +- [FAQ](https://github.com/garyburd/redigo/wiki/FAQ) | |
24 | +- [Examples](https://godoc.org/github.com/garyburd/redigo/redis#pkg-examples) | |
25 | + | |
26 | +Installation | |
27 | +------------ | |
28 | + | |
29 | +Install Redigo using the "go get" command: | |
30 | + | |
31 | + go get github.com/garyburd/redigo/redis | |
32 | + | |
33 | +The Go distribution is Redigo's only dependency. | |
34 | + | |
35 | +Related Projects | |
36 | +---------------- | |
37 | + | |
38 | +- [rafaeljusto/redigomock](https://godoc.org/github.com/rafaeljusto/redigomock) - A mock library for Redigo. | |
39 | +- [chasex/redis-go-cluster](https://github.com/chasex/redis-go-cluster) - A Redis cluster client implementation. | |
40 | +- [FZambia/go-sentinel](https://github.com/FZambia/go-sentinel) - Redis Sentinel support for Redigo | |
41 | +- [PuerkitoBio/redisc](https://github.com/PuerkitoBio/redisc) - Redis Cluster client built on top of Redigo | |
42 | + | |
43 | +Contributing | |
44 | +------------ | |
45 | + | |
46 | +See [CONTRIBUTING.md](https://github.com/garyburd/redigo/blob/master/.github/CONTRIBUTING.md). | |
47 | + | |
48 | +License | |
49 | +------- | |
50 | + | |
51 | +Redigo is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). | ... | ... |
... | ... | @@ -0,0 +1,54 @@ |
1 | +// Copyright 2014 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package internal // import "github.com/garyburd/redigo/internal" | |
16 | + | |
17 | +import ( | |
18 | + "strings" | |
19 | +) | |
20 | + | |
21 | +const ( | |
22 | + WatchState = 1 << iota | |
23 | + MultiState | |
24 | + SubscribeState | |
25 | + MonitorState | |
26 | +) | |
27 | + | |
28 | +type CommandInfo struct { | |
29 | + Set, Clear int | |
30 | +} | |
31 | + | |
32 | +var commandInfos = map[string]CommandInfo{ | |
33 | + "WATCH": {Set: WatchState}, | |
34 | + "UNWATCH": {Clear: WatchState}, | |
35 | + "MULTI": {Set: MultiState}, | |
36 | + "EXEC": {Clear: WatchState | MultiState}, | |
37 | + "DISCARD": {Clear: WatchState | MultiState}, | |
38 | + "PSUBSCRIBE": {Set: SubscribeState}, | |
39 | + "SUBSCRIBE": {Set: SubscribeState}, | |
40 | + "MONITOR": {Set: MonitorState}, | |
41 | +} | |
42 | + | |
43 | +func init() { | |
44 | + for n, ci := range commandInfos { | |
45 | + commandInfos[strings.ToLower(n)] = ci | |
46 | + } | |
47 | +} | |
48 | + | |
49 | +func LookupCommandInfo(commandName string) CommandInfo { | |
50 | + if ci, ok := commandInfos[commandName]; ok { | |
51 | + return ci | |
52 | + } | |
53 | + return commandInfos[strings.ToUpper(commandName)] | |
54 | +} | ... | ... |
src/github.com/garyburd/redigo/internal/commandinfo_test.go
0 → 100644
... | ... | @@ -0,0 +1,27 @@ |
1 | +package internal | |
2 | + | |
3 | +import "testing" | |
4 | + | |
5 | +func TestLookupCommandInfo(t *testing.T) { | |
6 | + for _, n := range []string{"watch", "WATCH", "wAtch"} { | |
7 | + if LookupCommandInfo(n) == (CommandInfo{}) { | |
8 | + t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n) | |
9 | + } | |
10 | + } | |
11 | +} | |
12 | + | |
13 | +func benchmarkLookupCommandInfo(b *testing.B, names ...string) { | |
14 | + for i := 0; i < b.N; i++ { | |
15 | + for _, c := range names { | |
16 | + LookupCommandInfo(c) | |
17 | + } | |
18 | + } | |
19 | +} | |
20 | + | |
21 | +func BenchmarkLookupCommandInfoCorrectCase(b *testing.B) { | |
22 | + benchmarkLookupCommandInfo(b, "watch", "WATCH", "monitor", "MONITOR") | |
23 | +} | |
24 | + | |
25 | +func BenchmarkLookupCommandInfoMixedCase(b *testing.B) { | |
26 | + benchmarkLookupCommandInfo(b, "wAtch", "WeTCH", "monItor", "MONiTOR") | |
27 | +} | ... | ... |
src/github.com/garyburd/redigo/internal/redistest/testdb.go
0 → 100644
... | ... | @@ -0,0 +1,68 @@ |
1 | +// Copyright 2014 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +// Package redistest contains utilities for writing Redigo tests. | |
16 | +package redistest | |
17 | + | |
18 | +import ( | |
19 | + "errors" | |
20 | + "time" | |
21 | + | |
22 | + "github.com/garyburd/redigo/redis" | |
23 | +) | |
24 | + | |
25 | +type testConn struct { | |
26 | + redis.Conn | |
27 | +} | |
28 | + | |
29 | +func (t testConn) Close() error { | |
30 | + _, err := t.Conn.Do("SELECT", "9") | |
31 | + if err != nil { | |
32 | + return nil | |
33 | + } | |
34 | + _, err = t.Conn.Do("FLUSHDB") | |
35 | + if err != nil { | |
36 | + return err | |
37 | + } | |
38 | + return t.Conn.Close() | |
39 | +} | |
40 | + | |
41 | +// Dial dials the local Redis server and selects database 9. To prevent | |
42 | +// stomping on real data, DialTestDB fails if database 9 contains data. The | |
43 | +// returned connection flushes database 9 on close. | |
44 | +func Dial() (redis.Conn, error) { | |
45 | + c, err := redis.DialTimeout("tcp", ":6379", 0, 1*time.Second, 1*time.Second) | |
46 | + if err != nil { | |
47 | + return nil, err | |
48 | + } | |
49 | + | |
50 | + _, err = c.Do("SELECT", "9") | |
51 | + if err != nil { | |
52 | + c.Close() | |
53 | + return nil, err | |
54 | + } | |
55 | + | |
56 | + n, err := redis.Int(c.Do("DBSIZE")) | |
57 | + if err != nil { | |
58 | + c.Close() | |
59 | + return nil, err | |
60 | + } | |
61 | + | |
62 | + if n != 0 { | |
63 | + c.Close() | |
64 | + return nil, errors.New("database #9 is not empty, test can not continue") | |
65 | + } | |
66 | + | |
67 | + return testConn{c}, nil | |
68 | +} | ... | ... |
... | ... | @@ -0,0 +1,651 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "bufio" | |
19 | + "bytes" | |
20 | + "crypto/tls" | |
21 | + "errors" | |
22 | + "fmt" | |
23 | + "io" | |
24 | + "net" | |
25 | + "net/url" | |
26 | + "regexp" | |
27 | + "strconv" | |
28 | + "sync" | |
29 | + "time" | |
30 | +) | |
31 | + | |
32 | +// conn is the low-level implementation of Conn | |
33 | +type conn struct { | |
34 | + // Shared | |
35 | + mu sync.Mutex | |
36 | + pending int | |
37 | + err error | |
38 | + conn net.Conn | |
39 | + | |
40 | + // Read | |
41 | + readTimeout time.Duration | |
42 | + br *bufio.Reader | |
43 | + | |
44 | + // Write | |
45 | + writeTimeout time.Duration | |
46 | + bw *bufio.Writer | |
47 | + | |
48 | + // Scratch space for formatting argument length. | |
49 | + // '*' or '$', length, "\r\n" | |
50 | + lenScratch [32]byte | |
51 | + | |
52 | + // Scratch space for formatting integers and floats. | |
53 | + numScratch [40]byte | |
54 | +} | |
55 | + | |
56 | +// DialTimeout acts like Dial but takes timeouts for establishing the | |
57 | +// connection to the server, writing a command and reading a reply. | |
58 | +// | |
59 | +// Deprecated: Use Dial with options instead. | |
60 | +func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) { | |
61 | + return Dial(network, address, | |
62 | + DialConnectTimeout(connectTimeout), | |
63 | + DialReadTimeout(readTimeout), | |
64 | + DialWriteTimeout(writeTimeout)) | |
65 | +} | |
66 | + | |
67 | +// DialOption specifies an option for dialing a Redis server. | |
68 | +type DialOption struct { | |
69 | + f func(*dialOptions) | |
70 | +} | |
71 | + | |
72 | +type dialOptions struct { | |
73 | + readTimeout time.Duration | |
74 | + writeTimeout time.Duration | |
75 | + dialer *net.Dialer | |
76 | + dial func(network, addr string) (net.Conn, error) | |
77 | + db int | |
78 | + password string | |
79 | + useTLS bool | |
80 | + skipVerify bool | |
81 | + tlsConfig *tls.Config | |
82 | +} | |
83 | + | |
84 | +// DialReadTimeout specifies the timeout for reading a single command reply. | |
85 | +func DialReadTimeout(d time.Duration) DialOption { | |
86 | + return DialOption{func(do *dialOptions) { | |
87 | + do.readTimeout = d | |
88 | + }} | |
89 | +} | |
90 | + | |
91 | +// DialWriteTimeout specifies the timeout for writing a single command. | |
92 | +func DialWriteTimeout(d time.Duration) DialOption { | |
93 | + return DialOption{func(do *dialOptions) { | |
94 | + do.writeTimeout = d | |
95 | + }} | |
96 | +} | |
97 | + | |
98 | +// DialConnectTimeout specifies the timeout for connecting to the Redis server when | |
99 | +// no DialNetDial option is specified. | |
100 | +func DialConnectTimeout(d time.Duration) DialOption { | |
101 | + return DialOption{func(do *dialOptions) { | |
102 | + do.dialer.Timeout = d | |
103 | + }} | |
104 | +} | |
105 | + | |
106 | +// DialKeepAlive specifies the keep-alive period for TCP connections to the Redis server | |
107 | +// when no DialNetDial option is specified. | |
108 | +// If zero, keep-alives are not enabled. If no DialKeepAlive option is specified then | |
109 | +// the default of 5 minutes is used to ensure that half-closed TCP sessions are detected. | |
110 | +func DialKeepAlive(d time.Duration) DialOption { | |
111 | + return DialOption{func(do *dialOptions) { | |
112 | + do.dialer.KeepAlive = d | |
113 | + }} | |
114 | +} | |
115 | + | |
116 | +// DialNetDial specifies a custom dial function for creating TCP | |
117 | +// connections, otherwise a net.Dialer customized via the other options is used. | |
118 | +// DialNetDial overrides DialConnectTimeout and DialKeepAlive. | |
119 | +func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { | |
120 | + return DialOption{func(do *dialOptions) { | |
121 | + do.dial = dial | |
122 | + }} | |
123 | +} | |
124 | + | |
125 | +// DialDatabase specifies the database to select when dialing a connection. | |
126 | +func DialDatabase(db int) DialOption { | |
127 | + return DialOption{func(do *dialOptions) { | |
128 | + do.db = db | |
129 | + }} | |
130 | +} | |
131 | + | |
132 | +// DialPassword specifies the password to use when connecting to | |
133 | +// the Redis server. | |
134 | +func DialPassword(password string) DialOption { | |
135 | + return DialOption{func(do *dialOptions) { | |
136 | + do.password = password | |
137 | + }} | |
138 | +} | |
139 | + | |
140 | +// DialTLSConfig specifies the config to use when a TLS connection is dialed. | |
141 | +// Has no effect when not dialing a TLS connection. | |
142 | +func DialTLSConfig(c *tls.Config) DialOption { | |
143 | + return DialOption{func(do *dialOptions) { | |
144 | + do.tlsConfig = c | |
145 | + }} | |
146 | +} | |
147 | + | |
148 | +// DialTLSSkipVerify disables server name verification when connecting over | |
149 | +// TLS. Has no effect when not dialing a TLS connection. | |
150 | +func DialTLSSkipVerify(skip bool) DialOption { | |
151 | + return DialOption{func(do *dialOptions) { | |
152 | + do.skipVerify = skip | |
153 | + }} | |
154 | +} | |
155 | + | |
156 | +// DialUseTLS specifies whether TLS should be used when connecting to the | |
157 | +// server. This option is ignore by DialURL. | |
158 | +func DialUseTLS(useTLS bool) DialOption { | |
159 | + return DialOption{func(do *dialOptions) { | |
160 | + do.useTLS = useTLS | |
161 | + }} | |
162 | +} | |
163 | + | |
164 | +// Dial connects to the Redis server at the given network and | |
165 | +// address using the specified options. | |
166 | +func Dial(network, address string, options ...DialOption) (Conn, error) { | |
167 | + do := dialOptions{ | |
168 | + dialer: &net.Dialer{ | |
169 | + KeepAlive: time.Minute * 5, | |
170 | + }, | |
171 | + } | |
172 | + for _, option := range options { | |
173 | + option.f(&do) | |
174 | + } | |
175 | + if do.dial == nil { | |
176 | + do.dial = do.dialer.Dial | |
177 | + } | |
178 | + | |
179 | + netConn, err := do.dial(network, address) | |
180 | + if err != nil { | |
181 | + return nil, err | |
182 | + } | |
183 | + | |
184 | + if do.useTLS { | |
185 | + tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify) | |
186 | + if tlsConfig.ServerName == "" { | |
187 | + host, _, err := net.SplitHostPort(address) | |
188 | + if err != nil { | |
189 | + netConn.Close() | |
190 | + return nil, err | |
191 | + } | |
192 | + tlsConfig.ServerName = host | |
193 | + } | |
194 | + | |
195 | + tlsConn := tls.Client(netConn, tlsConfig) | |
196 | + if err := tlsConn.Handshake(); err != nil { | |
197 | + netConn.Close() | |
198 | + return nil, err | |
199 | + } | |
200 | + netConn = tlsConn | |
201 | + } | |
202 | + | |
203 | + c := &conn{ | |
204 | + conn: netConn, | |
205 | + bw: bufio.NewWriter(netConn), | |
206 | + br: bufio.NewReader(netConn), | |
207 | + readTimeout: do.readTimeout, | |
208 | + writeTimeout: do.writeTimeout, | |
209 | + } | |
210 | + | |
211 | + if do.password != "" { | |
212 | + if _, err := c.Do("AUTH", do.password); err != nil { | |
213 | + netConn.Close() | |
214 | + return nil, err | |
215 | + } | |
216 | + } | |
217 | + | |
218 | + if do.db != 0 { | |
219 | + if _, err := c.Do("SELECT", do.db); err != nil { | |
220 | + netConn.Close() | |
221 | + return nil, err | |
222 | + } | |
223 | + } | |
224 | + | |
225 | + return c, nil | |
226 | +} | |
227 | + | |
228 | +var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`) | |
229 | + | |
230 | +// DialURL connects to a Redis server at the given URL using the Redis | |
231 | +// URI scheme. URLs should follow the draft IANA specification for the | |
232 | +// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). | |
233 | +func DialURL(rawurl string, options ...DialOption) (Conn, error) { | |
234 | + u, err := url.Parse(rawurl) | |
235 | + if err != nil { | |
236 | + return nil, err | |
237 | + } | |
238 | + | |
239 | + if u.Scheme != "redis" && u.Scheme != "rediss" { | |
240 | + return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) | |
241 | + } | |
242 | + | |
243 | + // As per the IANA draft spec, the host defaults to localhost and | |
244 | + // the port defaults to 6379. | |
245 | + host, port, err := net.SplitHostPort(u.Host) | |
246 | + if err != nil { | |
247 | + // assume port is missing | |
248 | + host = u.Host | |
249 | + port = "6379" | |
250 | + } | |
251 | + if host == "" { | |
252 | + host = "localhost" | |
253 | + } | |
254 | + address := net.JoinHostPort(host, port) | |
255 | + | |
256 | + if u.User != nil { | |
257 | + password, isSet := u.User.Password() | |
258 | + if isSet { | |
259 | + options = append(options, DialPassword(password)) | |
260 | + } | |
261 | + } | |
262 | + | |
263 | + match := pathDBRegexp.FindStringSubmatch(u.Path) | |
264 | + if len(match) == 2 { | |
265 | + db := 0 | |
266 | + if len(match[1]) > 0 { | |
267 | + db, err = strconv.Atoi(match[1]) | |
268 | + if err != nil { | |
269 | + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) | |
270 | + } | |
271 | + } | |
272 | + if db != 0 { | |
273 | + options = append(options, DialDatabase(db)) | |
274 | + } | |
275 | + } else if u.Path != "" { | |
276 | + return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) | |
277 | + } | |
278 | + | |
279 | + options = append(options, DialUseTLS(u.Scheme == "rediss")) | |
280 | + | |
281 | + return Dial("tcp", address, options...) | |
282 | +} | |
283 | + | |
284 | +// NewConn returns a new Redigo connection for the given net connection. | |
285 | +func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn { | |
286 | + return &conn{ | |
287 | + conn: netConn, | |
288 | + bw: bufio.NewWriter(netConn), | |
289 | + br: bufio.NewReader(netConn), | |
290 | + readTimeout: readTimeout, | |
291 | + writeTimeout: writeTimeout, | |
292 | + } | |
293 | +} | |
294 | + | |
295 | +func (c *conn) Close() error { | |
296 | + c.mu.Lock() | |
297 | + err := c.err | |
298 | + if c.err == nil { | |
299 | + c.err = errors.New("redigo: closed") | |
300 | + err = c.conn.Close() | |
301 | + } | |
302 | + c.mu.Unlock() | |
303 | + return err | |
304 | +} | |
305 | + | |
306 | +func (c *conn) fatal(err error) error { | |
307 | + c.mu.Lock() | |
308 | + if c.err == nil { | |
309 | + c.err = err | |
310 | + // Close connection to force errors on subsequent calls and to unblock | |
311 | + // other reader or writer. | |
312 | + c.conn.Close() | |
313 | + } | |
314 | + c.mu.Unlock() | |
315 | + return err | |
316 | +} | |
317 | + | |
318 | +func (c *conn) Err() error { | |
319 | + c.mu.Lock() | |
320 | + err := c.err | |
321 | + c.mu.Unlock() | |
322 | + return err | |
323 | +} | |
324 | + | |
325 | +func (c *conn) writeLen(prefix byte, n int) error { | |
326 | + c.lenScratch[len(c.lenScratch)-1] = '\n' | |
327 | + c.lenScratch[len(c.lenScratch)-2] = '\r' | |
328 | + i := len(c.lenScratch) - 3 | |
329 | + for { | |
330 | + c.lenScratch[i] = byte('0' + n%10) | |
331 | + i -= 1 | |
332 | + n = n / 10 | |
333 | + if n == 0 { | |
334 | + break | |
335 | + } | |
336 | + } | |
337 | + c.lenScratch[i] = prefix | |
338 | + _, err := c.bw.Write(c.lenScratch[i:]) | |
339 | + return err | |
340 | +} | |
341 | + | |
342 | +func (c *conn) writeString(s string) error { | |
343 | + c.writeLen('$', len(s)) | |
344 | + c.bw.WriteString(s) | |
345 | + _, err := c.bw.WriteString("\r\n") | |
346 | + return err | |
347 | +} | |
348 | + | |
349 | +func (c *conn) writeBytes(p []byte) error { | |
350 | + c.writeLen('$', len(p)) | |
351 | + c.bw.Write(p) | |
352 | + _, err := c.bw.WriteString("\r\n") | |
353 | + return err | |
354 | +} | |
355 | + | |
356 | +func (c *conn) writeInt64(n int64) error { | |
357 | + return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10)) | |
358 | +} | |
359 | + | |
360 | +func (c *conn) writeFloat64(n float64) error { | |
361 | + return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64)) | |
362 | +} | |
363 | + | |
364 | +func (c *conn) writeCommand(cmd string, args []interface{}) error { | |
365 | + c.writeLen('*', 1+len(args)) | |
366 | + if err := c.writeString(cmd); err != nil { | |
367 | + return err | |
368 | + } | |
369 | + for _, arg := range args { | |
370 | + if err := c.writeArg(arg, true); err != nil { | |
371 | + return err | |
372 | + } | |
373 | + } | |
374 | + return nil | |
375 | +} | |
376 | + | |
377 | +func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) { | |
378 | + switch arg := arg.(type) { | |
379 | + case string: | |
380 | + return c.writeString(arg) | |
381 | + case []byte: | |
382 | + return c.writeBytes(arg) | |
383 | + case int: | |
384 | + return c.writeInt64(int64(arg)) | |
385 | + case int64: | |
386 | + return c.writeInt64(arg) | |
387 | + case float64: | |
388 | + return c.writeFloat64(arg) | |
389 | + case bool: | |
390 | + if arg { | |
391 | + return c.writeString("1") | |
392 | + } else { | |
393 | + return c.writeString("0") | |
394 | + } | |
395 | + case nil: | |
396 | + return c.writeString("") | |
397 | + case Argument: | |
398 | + if argumentTypeOK { | |
399 | + return c.writeArg(arg.RedisArg(), false) | |
400 | + } | |
401 | + // See comment in default clause below. | |
402 | + var buf bytes.Buffer | |
403 | + fmt.Fprint(&buf, arg) | |
404 | + return c.writeBytes(buf.Bytes()) | |
405 | + default: | |
406 | + // This default clause is intended to handle builtin numeric types. | |
407 | + // The function should return an error for other types, but this is not | |
408 | + // done for compatibility with previous versions of the package. | |
409 | + var buf bytes.Buffer | |
410 | + fmt.Fprint(&buf, arg) | |
411 | + return c.writeBytes(buf.Bytes()) | |
412 | + } | |
413 | +} | |
414 | + | |
415 | +type protocolError string | |
416 | + | |
417 | +func (pe protocolError) Error() string { | |
418 | + return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe)) | |
419 | +} | |
420 | + | |
421 | +func (c *conn) readLine() ([]byte, error) { | |
422 | + p, err := c.br.ReadSlice('\n') | |
423 | + if err == bufio.ErrBufferFull { | |
424 | + return nil, protocolError("long response line") | |
425 | + } | |
426 | + if err != nil { | |
427 | + return nil, err | |
428 | + } | |
429 | + i := len(p) - 2 | |
430 | + if i < 0 || p[i] != '\r' { | |
431 | + return nil, protocolError("bad response line terminator") | |
432 | + } | |
433 | + return p[:i], nil | |
434 | +} | |
435 | + | |
436 | +// parseLen parses bulk string and array lengths. | |
437 | +func parseLen(p []byte) (int, error) { | |
438 | + if len(p) == 0 { | |
439 | + return -1, protocolError("malformed length") | |
440 | + } | |
441 | + | |
442 | + if p[0] == '-' && len(p) == 2 && p[1] == '1' { | |
443 | + // handle $-1 and $-1 null replies. | |
444 | + return -1, nil | |
445 | + } | |
446 | + | |
447 | + var n int | |
448 | + for _, b := range p { | |
449 | + n *= 10 | |
450 | + if b < '0' || b > '9' { | |
451 | + return -1, protocolError("illegal bytes in length") | |
452 | + } | |
453 | + n += int(b - '0') | |
454 | + } | |
455 | + | |
456 | + return n, nil | |
457 | +} | |
458 | + | |
459 | +// parseInt parses an integer reply. | |
460 | +func parseInt(p []byte) (interface{}, error) { | |
461 | + if len(p) == 0 { | |
462 | + return 0, protocolError("malformed integer") | |
463 | + } | |
464 | + | |
465 | + var negate bool | |
466 | + if p[0] == '-' { | |
467 | + negate = true | |
468 | + p = p[1:] | |
469 | + if len(p) == 0 { | |
470 | + return 0, protocolError("malformed integer") | |
471 | + } | |
472 | + } | |
473 | + | |
474 | + var n int64 | |
475 | + for _, b := range p { | |
476 | + n *= 10 | |
477 | + if b < '0' || b > '9' { | |
478 | + return 0, protocolError("illegal bytes in length") | |
479 | + } | |
480 | + n += int64(b - '0') | |
481 | + } | |
482 | + | |
483 | + if negate { | |
484 | + n = -n | |
485 | + } | |
486 | + return n, nil | |
487 | +} | |
488 | + | |
489 | +var ( | |
490 | + okReply interface{} = "OK" | |
491 | + pongReply interface{} = "PONG" | |
492 | +) | |
493 | + | |
494 | +func (c *conn) readReply() (interface{}, error) { | |
495 | + line, err := c.readLine() | |
496 | + if err != nil { | |
497 | + return nil, err | |
498 | + } | |
499 | + if len(line) == 0 { | |
500 | + return nil, protocolError("short response line") | |
501 | + } | |
502 | + switch line[0] { | |
503 | + case '+': | |
504 | + switch { | |
505 | + case len(line) == 3 && line[1] == 'O' && line[2] == 'K': | |
506 | + // Avoid allocation for frequent "+OK" response. | |
507 | + return okReply, nil | |
508 | + case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G': | |
509 | + // Avoid allocation in PING command benchmarks :) | |
510 | + return pongReply, nil | |
511 | + default: | |
512 | + return string(line[1:]), nil | |
513 | + } | |
514 | + case '-': | |
515 | + return Error(string(line[1:])), nil | |
516 | + case ':': | |
517 | + return parseInt(line[1:]) | |
518 | + case '$': | |
519 | + n, err := parseLen(line[1:]) | |
520 | + if n < 0 || err != nil { | |
521 | + return nil, err | |
522 | + } | |
523 | + p := make([]byte, n) | |
524 | + _, err = io.ReadFull(c.br, p) | |
525 | + if err != nil { | |
526 | + return nil, err | |
527 | + } | |
528 | + if line, err := c.readLine(); err != nil { | |
529 | + return nil, err | |
530 | + } else if len(line) != 0 { | |
531 | + return nil, protocolError("bad bulk string format") | |
532 | + } | |
533 | + return p, nil | |
534 | + case '*': | |
535 | + n, err := parseLen(line[1:]) | |
536 | + if n < 0 || err != nil { | |
537 | + return nil, err | |
538 | + } | |
539 | + r := make([]interface{}, n) | |
540 | + for i := range r { | |
541 | + r[i], err = c.readReply() | |
542 | + if err != nil { | |
543 | + return nil, err | |
544 | + } | |
545 | + } | |
546 | + return r, nil | |
547 | + } | |
548 | + return nil, protocolError("unexpected response line") | |
549 | +} | |
550 | + | |
551 | +func (c *conn) Send(cmd string, args ...interface{}) error { | |
552 | + c.mu.Lock() | |
553 | + c.pending += 1 | |
554 | + c.mu.Unlock() | |
555 | + if c.writeTimeout != 0 { | |
556 | + c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) | |
557 | + } | |
558 | + if err := c.writeCommand(cmd, args); err != nil { | |
559 | + return c.fatal(err) | |
560 | + } | |
561 | + return nil | |
562 | +} | |
563 | + | |
564 | +func (c *conn) Flush() error { | |
565 | + if c.writeTimeout != 0 { | |
566 | + c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) | |
567 | + } | |
568 | + if err := c.bw.Flush(); err != nil { | |
569 | + return c.fatal(err) | |
570 | + } | |
571 | + return nil | |
572 | +} | |
573 | + | |
574 | +func (c *conn) Receive() (reply interface{}, err error) { | |
575 | + if c.readTimeout != 0 { | |
576 | + c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) | |
577 | + } | |
578 | + if reply, err = c.readReply(); err != nil { | |
579 | + return nil, c.fatal(err) | |
580 | + } | |
581 | + // When using pub/sub, the number of receives can be greater than the | |
582 | + // number of sends. To enable normal use of the connection after | |
583 | + // unsubscribing from all channels, we do not decrement pending to a | |
584 | + // negative value. | |
585 | + // | |
586 | + // The pending field is decremented after the reply is read to handle the | |
587 | + // case where Receive is called before Send. | |
588 | + c.mu.Lock() | |
589 | + if c.pending > 0 { | |
590 | + c.pending -= 1 | |
591 | + } | |
592 | + c.mu.Unlock() | |
593 | + if err, ok := reply.(Error); ok { | |
594 | + return nil, err | |
595 | + } | |
596 | + return | |
597 | +} | |
598 | + | |
599 | +func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) { | |
600 | + c.mu.Lock() | |
601 | + pending := c.pending | |
602 | + c.pending = 0 | |
603 | + c.mu.Unlock() | |
604 | + | |
605 | + if cmd == "" && pending == 0 { | |
606 | + return nil, nil | |
607 | + } | |
608 | + | |
609 | + if c.writeTimeout != 0 { | |
610 | + c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) | |
611 | + } | |
612 | + | |
613 | + if cmd != "" { | |
614 | + if err := c.writeCommand(cmd, args); err != nil { | |
615 | + return nil, c.fatal(err) | |
616 | + } | |
617 | + } | |
618 | + | |
619 | + if err := c.bw.Flush(); err != nil { | |
620 | + return nil, c.fatal(err) | |
621 | + } | |
622 | + | |
623 | + if c.readTimeout != 0 { | |
624 | + c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) | |
625 | + } | |
626 | + | |
627 | + if cmd == "" { | |
628 | + reply := make([]interface{}, pending) | |
629 | + for i := range reply { | |
630 | + r, e := c.readReply() | |
631 | + if e != nil { | |
632 | + return nil, c.fatal(e) | |
633 | + } | |
634 | + reply[i] = r | |
635 | + } | |
636 | + return reply, nil | |
637 | + } | |
638 | + | |
639 | + var err error | |
640 | + var reply interface{} | |
641 | + for i := 0; i <= pending; i++ { | |
642 | + var e error | |
643 | + if reply, e = c.readReply(); e != nil { | |
644 | + return nil, c.fatal(e) | |
645 | + } | |
646 | + if e, ok := reply.(Error); ok && err == nil { | |
647 | + err = e | |
648 | + } | |
649 | + } | |
650 | + return reply, err | |
651 | +} | ... | ... |
... | ... | @@ -0,0 +1,823 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "bytes" | |
19 | + "crypto/tls" | |
20 | + "crypto/x509" | |
21 | + "fmt" | |
22 | + "io" | |
23 | + "math" | |
24 | + "net" | |
25 | + "os" | |
26 | + "reflect" | |
27 | + "strings" | |
28 | + "testing" | |
29 | + "time" | |
30 | + | |
31 | + "github.com/garyburd/redigo/redis" | |
32 | +) | |
33 | + | |
34 | +type testConn struct { | |
35 | + io.Reader | |
36 | + io.Writer | |
37 | +} | |
38 | + | |
39 | +func (*testConn) Close() error { return nil } | |
40 | +func (*testConn) LocalAddr() net.Addr { return nil } | |
41 | +func (*testConn) RemoteAddr() net.Addr { return nil } | |
42 | +func (*testConn) SetDeadline(t time.Time) error { return nil } | |
43 | +func (*testConn) SetReadDeadline(t time.Time) error { return nil } | |
44 | +func (*testConn) SetWriteDeadline(t time.Time) error { return nil } | |
45 | + | |
46 | +func dialTestConn(r string, w io.Writer) redis.DialOption { | |
47 | + return redis.DialNetDial(func(network, addr string) (net.Conn, error) { | |
48 | + return &testConn{Reader: strings.NewReader(r), Writer: w}, nil | |
49 | + }) | |
50 | +} | |
51 | + | |
52 | +type tlsTestConn struct { | |
53 | + net.Conn | |
54 | + done chan struct{} | |
55 | +} | |
56 | + | |
57 | +func (c *tlsTestConn) Close() error { | |
58 | + c.Conn.Close() | |
59 | + <-c.done | |
60 | + return nil | |
61 | +} | |
62 | + | |
63 | +func dialTestConnTLS(r string, w io.Writer) redis.DialOption { | |
64 | + return redis.DialNetDial(func(network, addr string) (net.Conn, error) { | |
65 | + client, server := net.Pipe() | |
66 | + tlsServer := tls.Server(server, &serverTLSConfig) | |
67 | + go io.Copy(tlsServer, strings.NewReader(r)) | |
68 | + done := make(chan struct{}) | |
69 | + go func() { | |
70 | + io.Copy(w, tlsServer) | |
71 | + close(done) | |
72 | + }() | |
73 | + return &tlsTestConn{Conn: client, done: done}, nil | |
74 | + }) | |
75 | +} | |
76 | + | |
77 | +type durationArg struct { | |
78 | + time.Duration | |
79 | +} | |
80 | + | |
81 | +func (t durationArg) RedisArg() interface{} { | |
82 | + return t.Seconds() | |
83 | +} | |
84 | + | |
85 | +type recursiveArg int | |
86 | + | |
87 | +func (v recursiveArg) RedisArg() interface{} { return v } | |
88 | + | |
89 | +var writeTests = []struct { | |
90 | + args []interface{} | |
91 | + expected string | |
92 | +}{ | |
93 | + { | |
94 | + []interface{}{"SET", "key", "value"}, | |
95 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", | |
96 | + }, | |
97 | + { | |
98 | + []interface{}{"SET", "key", "value"}, | |
99 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", | |
100 | + }, | |
101 | + { | |
102 | + []interface{}{"SET", "key", byte(100)}, | |
103 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", | |
104 | + }, | |
105 | + { | |
106 | + []interface{}{"SET", "key", 100}, | |
107 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", | |
108 | + }, | |
109 | + { | |
110 | + []interface{}{"SET", "key", int64(math.MinInt64)}, | |
111 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$20\r\n-9223372036854775808\r\n", | |
112 | + }, | |
113 | + { | |
114 | + []interface{}{"SET", "key", float64(1349673917.939762)}, | |
115 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$21\r\n1.349673917939762e+09\r\n", | |
116 | + }, | |
117 | + { | |
118 | + []interface{}{"SET", "key", ""}, | |
119 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", | |
120 | + }, | |
121 | + { | |
122 | + []interface{}{"SET", "key", nil}, | |
123 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", | |
124 | + }, | |
125 | + { | |
126 | + []interface{}{"SET", "key", durationArg{time.Minute}}, | |
127 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$2\r\n60\r\n", | |
128 | + }, | |
129 | + { | |
130 | + []interface{}{"SET", "key", recursiveArg(123)}, | |
131 | + "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n123\r\n", | |
132 | + }, | |
133 | + { | |
134 | + []interface{}{"ECHO", true, false}, | |
135 | + "*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n", | |
136 | + }, | |
137 | +} | |
138 | + | |
139 | +func TestWrite(t *testing.T) { | |
140 | + for _, tt := range writeTests { | |
141 | + var buf bytes.Buffer | |
142 | + c, _ := redis.Dial("", "", dialTestConn("", &buf)) | |
143 | + err := c.Send(tt.args[0].(string), tt.args[1:]...) | |
144 | + if err != nil { | |
145 | + t.Errorf("Send(%v) returned error %v", tt.args, err) | |
146 | + continue | |
147 | + } | |
148 | + c.Flush() | |
149 | + actual := buf.String() | |
150 | + if actual != tt.expected { | |
151 | + t.Errorf("Send(%v) = %q, want %q", tt.args, actual, tt.expected) | |
152 | + } | |
153 | + } | |
154 | +} | |
155 | + | |
156 | +var errorSentinel = &struct{}{} | |
157 | + | |
158 | +var readTests = []struct { | |
159 | + reply string | |
160 | + expected interface{} | |
161 | +}{ | |
162 | + { | |
163 | + "+OK\r\n", | |
164 | + "OK", | |
165 | + }, | |
166 | + { | |
167 | + "+PONG\r\n", | |
168 | + "PONG", | |
169 | + }, | |
170 | + { | |
171 | + "@OK\r\n", | |
172 | + errorSentinel, | |
173 | + }, | |
174 | + { | |
175 | + "$6\r\nfoobar\r\n", | |
176 | + []byte("foobar"), | |
177 | + }, | |
178 | + { | |
179 | + "$-1\r\n", | |
180 | + nil, | |
181 | + }, | |
182 | + { | |
183 | + ":1\r\n", | |
184 | + int64(1), | |
185 | + }, | |
186 | + { | |
187 | + ":-2\r\n", | |
188 | + int64(-2), | |
189 | + }, | |
190 | + { | |
191 | + "*0\r\n", | |
192 | + []interface{}{}, | |
193 | + }, | |
194 | + { | |
195 | + "*-1\r\n", | |
196 | + nil, | |
197 | + }, | |
198 | + { | |
199 | + "*4\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nHello\r\n$5\r\nWorld\r\n", | |
200 | + []interface{}{[]byte("foo"), []byte("bar"), []byte("Hello"), []byte("World")}, | |
201 | + }, | |
202 | + { | |
203 | + "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n", | |
204 | + []interface{}{[]byte("foo"), nil, []byte("bar")}, | |
205 | + }, | |
206 | + | |
207 | + { | |
208 | + // "x" is not a valid length | |
209 | + "$x\r\nfoobar\r\n", | |
210 | + errorSentinel, | |
211 | + }, | |
212 | + { | |
213 | + // -2 is not a valid length | |
214 | + "$-2\r\n", | |
215 | + errorSentinel, | |
216 | + }, | |
217 | + { | |
218 | + // "x" is not a valid integer | |
219 | + ":x\r\n", | |
220 | + errorSentinel, | |
221 | + }, | |
222 | + { | |
223 | + // missing \r\n following value | |
224 | + "$6\r\nfoobar", | |
225 | + errorSentinel, | |
226 | + }, | |
227 | + { | |
228 | + // short value | |
229 | + "$6\r\nxx", | |
230 | + errorSentinel, | |
231 | + }, | |
232 | + { | |
233 | + // long value | |
234 | + "$6\r\nfoobarx\r\n", | |
235 | + errorSentinel, | |
236 | + }, | |
237 | +} | |
238 | + | |
239 | +func TestRead(t *testing.T) { | |
240 | + for _, tt := range readTests { | |
241 | + c, _ := redis.Dial("", "", dialTestConn(tt.reply, nil)) | |
242 | + actual, err := c.Receive() | |
243 | + if tt.expected == errorSentinel { | |
244 | + if err == nil { | |
245 | + t.Errorf("Receive(%q) did not return expected error", tt.reply) | |
246 | + } | |
247 | + } else { | |
248 | + if err != nil { | |
249 | + t.Errorf("Receive(%q) returned error %v", tt.reply, err) | |
250 | + continue | |
251 | + } | |
252 | + if !reflect.DeepEqual(actual, tt.expected) { | |
253 | + t.Errorf("Receive(%q) = %v, want %v", tt.reply, actual, tt.expected) | |
254 | + } | |
255 | + } | |
256 | + } | |
257 | +} | |
258 | + | |
259 | +var testCommands = []struct { | |
260 | + args []interface{} | |
261 | + expected interface{} | |
262 | +}{ | |
263 | + { | |
264 | + []interface{}{"PING"}, | |
265 | + "PONG", | |
266 | + }, | |
267 | + { | |
268 | + []interface{}{"SET", "foo", "bar"}, | |
269 | + "OK", | |
270 | + }, | |
271 | + { | |
272 | + []interface{}{"GET", "foo"}, | |
273 | + []byte("bar"), | |
274 | + }, | |
275 | + { | |
276 | + []interface{}{"GET", "nokey"}, | |
277 | + nil, | |
278 | + }, | |
279 | + { | |
280 | + []interface{}{"MGET", "nokey", "foo"}, | |
281 | + []interface{}{nil, []byte("bar")}, | |
282 | + }, | |
283 | + { | |
284 | + []interface{}{"INCR", "mycounter"}, | |
285 | + int64(1), | |
286 | + }, | |
287 | + { | |
288 | + []interface{}{"LPUSH", "mylist", "foo"}, | |
289 | + int64(1), | |
290 | + }, | |
291 | + { | |
292 | + []interface{}{"LPUSH", "mylist", "bar"}, | |
293 | + int64(2), | |
294 | + }, | |
295 | + { | |
296 | + []interface{}{"LRANGE", "mylist", 0, -1}, | |
297 | + []interface{}{[]byte("bar"), []byte("foo")}, | |
298 | + }, | |
299 | + { | |
300 | + []interface{}{"MULTI"}, | |
301 | + "OK", | |
302 | + }, | |
303 | + { | |
304 | + []interface{}{"LRANGE", "mylist", 0, -1}, | |
305 | + "QUEUED", | |
306 | + }, | |
307 | + { | |
308 | + []interface{}{"PING"}, | |
309 | + "QUEUED", | |
310 | + }, | |
311 | + { | |
312 | + []interface{}{"EXEC"}, | |
313 | + []interface{}{ | |
314 | + []interface{}{[]byte("bar"), []byte("foo")}, | |
315 | + "PONG", | |
316 | + }, | |
317 | + }, | |
318 | +} | |
319 | + | |
320 | +func TestDoCommands(t *testing.T) { | |
321 | + c, err := redis.DialDefaultServer() | |
322 | + if err != nil { | |
323 | + t.Fatalf("error connection to database, %v", err) | |
324 | + } | |
325 | + defer c.Close() | |
326 | + | |
327 | + for _, cmd := range testCommands { | |
328 | + actual, err := c.Do(cmd.args[0].(string), cmd.args[1:]...) | |
329 | + if err != nil { | |
330 | + t.Errorf("Do(%v) returned error %v", cmd.args, err) | |
331 | + continue | |
332 | + } | |
333 | + if !reflect.DeepEqual(actual, cmd.expected) { | |
334 | + t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected) | |
335 | + } | |
336 | + } | |
337 | +} | |
338 | + | |
339 | +func TestPipelineCommands(t *testing.T) { | |
340 | + c, err := redis.DialDefaultServer() | |
341 | + if err != nil { | |
342 | + t.Fatalf("error connection to database, %v", err) | |
343 | + } | |
344 | + defer c.Close() | |
345 | + | |
346 | + for _, cmd := range testCommands { | |
347 | + if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { | |
348 | + t.Fatalf("Send(%v) returned error %v", cmd.args, err) | |
349 | + } | |
350 | + } | |
351 | + if err := c.Flush(); err != nil { | |
352 | + t.Errorf("Flush() returned error %v", err) | |
353 | + } | |
354 | + for _, cmd := range testCommands { | |
355 | + actual, err := c.Receive() | |
356 | + if err != nil { | |
357 | + t.Fatalf("Receive(%v) returned error %v", cmd.args, err) | |
358 | + } | |
359 | + if !reflect.DeepEqual(actual, cmd.expected) { | |
360 | + t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) | |
361 | + } | |
362 | + } | |
363 | +} | |
364 | + | |
365 | +func TestBlankCommmand(t *testing.T) { | |
366 | + c, err := redis.DialDefaultServer() | |
367 | + if err != nil { | |
368 | + t.Fatalf("error connection to database, %v", err) | |
369 | + } | |
370 | + defer c.Close() | |
371 | + | |
372 | + for _, cmd := range testCommands { | |
373 | + if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { | |
374 | + t.Fatalf("Send(%v) returned error %v", cmd.args, err) | |
375 | + } | |
376 | + } | |
377 | + reply, err := redis.Values(c.Do("")) | |
378 | + if err != nil { | |
379 | + t.Fatalf("Do() returned error %v", err) | |
380 | + } | |
381 | + if len(reply) != len(testCommands) { | |
382 | + t.Fatalf("len(reply)=%d, want %d", len(reply), len(testCommands)) | |
383 | + } | |
384 | + for i, cmd := range testCommands { | |
385 | + actual := reply[i] | |
386 | + if !reflect.DeepEqual(actual, cmd.expected) { | |
387 | + t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) | |
388 | + } | |
389 | + } | |
390 | +} | |
391 | + | |
392 | +func TestRecvBeforeSend(t *testing.T) { | |
393 | + c, err := redis.DialDefaultServer() | |
394 | + if err != nil { | |
395 | + t.Fatalf("error connection to database, %v", err) | |
396 | + } | |
397 | + defer c.Close() | |
398 | + done := make(chan struct{}) | |
399 | + go func() { | |
400 | + c.Receive() | |
401 | + close(done) | |
402 | + }() | |
403 | + time.Sleep(time.Millisecond) | |
404 | + c.Send("PING") | |
405 | + c.Flush() | |
406 | + <-done | |
407 | + _, err = c.Do("") | |
408 | + if err != nil { | |
409 | + t.Fatalf("error=%v", err) | |
410 | + } | |
411 | +} | |
412 | + | |
413 | +func TestError(t *testing.T) { | |
414 | + c, err := redis.DialDefaultServer() | |
415 | + if err != nil { | |
416 | + t.Fatalf("error connection to database, %v", err) | |
417 | + } | |
418 | + defer c.Close() | |
419 | + | |
420 | + c.Do("SET", "key", "val") | |
421 | + _, err = c.Do("HSET", "key", "fld", "val") | |
422 | + if err == nil { | |
423 | + t.Errorf("Expected err for HSET on string key.") | |
424 | + } | |
425 | + if c.Err() != nil { | |
426 | + t.Errorf("Conn has Err()=%v, expect nil", c.Err()) | |
427 | + } | |
428 | + _, err = c.Do("SET", "key", "val") | |
429 | + if err != nil { | |
430 | + t.Errorf("Do(SET, key, val) returned error %v, expected nil.", err) | |
431 | + } | |
432 | +} | |
433 | + | |
434 | +func TestReadTimeout(t *testing.T) { | |
435 | + l, err := net.Listen("tcp", "127.0.0.1:0") | |
436 | + if err != nil { | |
437 | + t.Fatalf("net.Listen returned %v", err) | |
438 | + } | |
439 | + defer l.Close() | |
440 | + | |
441 | + go func() { | |
442 | + for { | |
443 | + c, err := l.Accept() | |
444 | + if err != nil { | |
445 | + return | |
446 | + } | |
447 | + go func() { | |
448 | + time.Sleep(time.Second) | |
449 | + c.Write([]byte("+OK\r\n")) | |
450 | + c.Close() | |
451 | + }() | |
452 | + } | |
453 | + }() | |
454 | + | |
455 | + // Do | |
456 | + | |
457 | + c1, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond)) | |
458 | + if err != nil { | |
459 | + t.Fatalf("redis.Dial returned %v", err) | |
460 | + } | |
461 | + defer c1.Close() | |
462 | + | |
463 | + _, err = c1.Do("PING") | |
464 | + if err == nil { | |
465 | + t.Fatalf("c1.Do() returned nil, expect error") | |
466 | + } | |
467 | + if c1.Err() == nil { | |
468 | + t.Fatalf("c1.Err() = nil, expect error") | |
469 | + } | |
470 | + | |
471 | + // Send/Flush/Receive | |
472 | + | |
473 | + c2, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond)) | |
474 | + if err != nil { | |
475 | + t.Fatalf("redis.Dial returned %v", err) | |
476 | + } | |
477 | + defer c2.Close() | |
478 | + | |
479 | + c2.Send("PING") | |
480 | + c2.Flush() | |
481 | + _, err = c2.Receive() | |
482 | + if err == nil { | |
483 | + t.Fatalf("c2.Receive() returned nil, expect error") | |
484 | + } | |
485 | + if c2.Err() == nil { | |
486 | + t.Fatalf("c2.Err() = nil, expect error") | |
487 | + } | |
488 | +} | |
489 | + | |
490 | +var dialErrors = []struct { | |
491 | + rawurl string | |
492 | + expectedError string | |
493 | +}{ | |
494 | + { | |
495 | + "localhost", | |
496 | + "invalid redis URL scheme", | |
497 | + }, | |
498 | + // The error message for invalid hosts is different in different | |
499 | + // versions of Go, so just check that there is an error message. | |
500 | + { | |
501 | + "redis://weird url", | |
502 | + "", | |
503 | + }, | |
504 | + { | |
505 | + "redis://foo:bar:baz", | |
506 | + "", | |
507 | + }, | |
508 | + { | |
509 | + "http://www.google.com", | |
510 | + "invalid redis URL scheme: http", | |
511 | + }, | |
512 | + { | |
513 | + "redis://localhost:6379/abc123", | |
514 | + "invalid database: abc123", | |
515 | + }, | |
516 | +} | |
517 | + | |
518 | +func TestDialURLErrors(t *testing.T) { | |
519 | + for _, d := range dialErrors { | |
520 | + _, err := redis.DialURL(d.rawurl) | |
521 | + if err == nil || !strings.Contains(err.Error(), d.expectedError) { | |
522 | + t.Errorf("DialURL did not return expected error (expected %v to contain %s)", err, d.expectedError) | |
523 | + } | |
524 | + } | |
525 | +} | |
526 | + | |
527 | +func TestDialURLPort(t *testing.T) { | |
528 | + checkPort := func(network, address string) (net.Conn, error) { | |
529 | + if address != "localhost:6379" { | |
530 | + t.Errorf("DialURL did not set port to 6379 by default (got %v)", address) | |
531 | + } | |
532 | + return nil, nil | |
533 | + } | |
534 | + _, err := redis.DialURL("redis://localhost", redis.DialNetDial(checkPort)) | |
535 | + if err != nil { | |
536 | + t.Error("dial error:", err) | |
537 | + } | |
538 | +} | |
539 | + | |
540 | +func TestDialURLHost(t *testing.T) { | |
541 | + checkHost := func(network, address string) (net.Conn, error) { | |
542 | + if address != "localhost:6379" { | |
543 | + t.Errorf("DialURL did not set host to localhost by default (got %v)", address) | |
544 | + } | |
545 | + return nil, nil | |
546 | + } | |
547 | + _, err := redis.DialURL("redis://:6379", redis.DialNetDial(checkHost)) | |
548 | + if err != nil { | |
549 | + t.Error("dial error:", err) | |
550 | + } | |
551 | +} | |
552 | + | |
553 | +var dialURLTests = []struct { | |
554 | + description string | |
555 | + url string | |
556 | + r string | |
557 | + w string | |
558 | +}{ | |
559 | + {"password", "redis://x:abc123@localhost", "+OK\r\n", "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n"}, | |
560 | + {"database 3", "redis://localhost/3", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n"}, | |
561 | + {"database 99", "redis://localhost/99", "+OK\r\n", "*2\r\n$6\r\nSELECT\r\n$2\r\n99\r\n"}, | |
562 | + {"no database", "redis://localhost/", "+OK\r\n", ""}, | |
563 | +} | |
564 | + | |
565 | +func TestDialURL(t *testing.T) { | |
566 | + for _, tt := range dialURLTests { | |
567 | + var buf bytes.Buffer | |
568 | + // UseTLS should be ignored in all of these tests. | |
569 | + _, err := redis.DialURL(tt.url, dialTestConn(tt.r, &buf), redis.DialUseTLS(true)) | |
570 | + if err != nil { | |
571 | + t.Errorf("%s dial error: %v", tt.description, err) | |
572 | + continue | |
573 | + } | |
574 | + if w := buf.String(); w != tt.w { | |
575 | + t.Errorf("%s commands = %q, want %q", tt.description, w, tt.w) | |
576 | + } | |
577 | + } | |
578 | +} | |
579 | + | |
580 | +func checkPingPong(t *testing.T, buf *bytes.Buffer, c redis.Conn) { | |
581 | + resp, err := c.Do("PING") | |
582 | + if err != nil { | |
583 | + t.Fatal("ping error:", err) | |
584 | + } | |
585 | + // Close connection to ensure that writes to buf are complete. | |
586 | + c.Close() | |
587 | + expected := "*1\r\n$4\r\nPING\r\n" | |
588 | + actual := buf.String() | |
589 | + if actual != expected { | |
590 | + t.Errorf("commands = %q, want %q", actual, expected) | |
591 | + } | |
592 | + if resp != "PONG" { | |
593 | + t.Errorf("resp = %v, want %v", resp, "PONG") | |
594 | + } | |
595 | +} | |
596 | + | |
597 | +const pingResponse = "+PONG\r\n" | |
598 | + | |
599 | +func TestDialURLTLS(t *testing.T) { | |
600 | + var buf bytes.Buffer | |
601 | + c, err := redis.DialURL("rediss://example.com/", | |
602 | + redis.DialTLSConfig(&clientTLSConfig), | |
603 | + dialTestConnTLS(pingResponse, &buf)) | |
604 | + if err != nil { | |
605 | + t.Fatal("dial error:", err) | |
606 | + } | |
607 | + checkPingPong(t, &buf, c) | |
608 | +} | |
609 | + | |
610 | +func TestDialUseTLS(t *testing.T) { | |
611 | + var buf bytes.Buffer | |
612 | + c, err := redis.Dial("tcp", "example.com:6379", | |
613 | + redis.DialTLSConfig(&clientTLSConfig), | |
614 | + dialTestConnTLS(pingResponse, &buf), | |
615 | + redis.DialUseTLS(true)) | |
616 | + if err != nil { | |
617 | + t.Fatal("dial error:", err) | |
618 | + } | |
619 | + checkPingPong(t, &buf, c) | |
620 | +} | |
621 | + | |
622 | +func TestDialTLSSKipVerify(t *testing.T) { | |
623 | + var buf bytes.Buffer | |
624 | + c, err := redis.Dial("tcp", "example.com:6379", | |
625 | + dialTestConnTLS(pingResponse, &buf), | |
626 | + redis.DialTLSSkipVerify(true), | |
627 | + redis.DialUseTLS(true)) | |
628 | + if err != nil { | |
629 | + t.Fatal("dial error:", err) | |
630 | + } | |
631 | + checkPingPong(t, &buf, c) | |
632 | +} | |
633 | + | |
634 | +// Connect to local instance of Redis running on the default port. | |
635 | +func ExampleDial() { | |
636 | + c, err := redis.Dial("tcp", ":6379") | |
637 | + if err != nil { | |
638 | + // handle error | |
639 | + } | |
640 | + defer c.Close() | |
641 | +} | |
642 | + | |
643 | +// Connect to remote instance of Redis using a URL. | |
644 | +func ExampleDialURL() { | |
645 | + c, err := redis.DialURL(os.Getenv("REDIS_URL")) | |
646 | + if err != nil { | |
647 | + // handle connection error | |
648 | + } | |
649 | + defer c.Close() | |
650 | +} | |
651 | + | |
652 | +// TextExecError tests handling of errors in a transaction. See | |
653 | +// http://redis.io/topics/transactions for information on how Redis handles | |
654 | +// errors in a transaction. | |
655 | +func TestExecError(t *testing.T) { | |
656 | + c, err := redis.DialDefaultServer() | |
657 | + if err != nil { | |
658 | + t.Fatalf("error connection to database, %v", err) | |
659 | + } | |
660 | + defer c.Close() | |
661 | + | |
662 | + // Execute commands that fail before EXEC is called. | |
663 | + | |
664 | + c.Do("DEL", "k0") | |
665 | + c.Do("ZADD", "k0", 0, 0) | |
666 | + c.Send("MULTI") | |
667 | + c.Send("NOTACOMMAND", "k0", 0, 0) | |
668 | + c.Send("ZINCRBY", "k0", 0, 0) | |
669 | + v, err := c.Do("EXEC") | |
670 | + if err == nil { | |
671 | + t.Fatalf("EXEC returned values %v, expected error", v) | |
672 | + } | |
673 | + | |
674 | + // Execute commands that fail after EXEC is called. The first command | |
675 | + // returns an error. | |
676 | + | |
677 | + c.Do("DEL", "k1") | |
678 | + c.Do("ZADD", "k1", 0, 0) | |
679 | + c.Send("MULTI") | |
680 | + c.Send("HSET", "k1", 0, 0) | |
681 | + c.Send("ZINCRBY", "k1", 0, 0) | |
682 | + v, err = c.Do("EXEC") | |
683 | + if err != nil { | |
684 | + t.Fatalf("EXEC returned error %v", err) | |
685 | + } | |
686 | + | |
687 | + vs, err := redis.Values(v, nil) | |
688 | + if err != nil { | |
689 | + t.Fatalf("Values(v) returned error %v", err) | |
690 | + } | |
691 | + | |
692 | + if len(vs) != 2 { | |
693 | + t.Fatalf("len(vs) == %d, want 2", len(vs)) | |
694 | + } | |
695 | + | |
696 | + if _, ok := vs[0].(error); !ok { | |
697 | + t.Fatalf("first result is type %T, expected error", vs[0]) | |
698 | + } | |
699 | + | |
700 | + if _, ok := vs[1].([]byte); !ok { | |
701 | + t.Fatalf("second result is type %T, expected []byte", vs[1]) | |
702 | + } | |
703 | + | |
704 | + // Execute commands that fail after EXEC is called. The second command | |
705 | + // returns an error. | |
706 | + | |
707 | + c.Do("ZADD", "k2", 0, 0) | |
708 | + c.Send("MULTI") | |
709 | + c.Send("ZINCRBY", "k2", 0, 0) | |
710 | + c.Send("HSET", "k2", 0, 0) | |
711 | + v, err = c.Do("EXEC") | |
712 | + if err != nil { | |
713 | + t.Fatalf("EXEC returned error %v", err) | |
714 | + } | |
715 | + | |
716 | + vs, err = redis.Values(v, nil) | |
717 | + if err != nil { | |
718 | + t.Fatalf("Values(v) returned error %v", err) | |
719 | + } | |
720 | + | |
721 | + if len(vs) != 2 { | |
722 | + t.Fatalf("len(vs) == %d, want 2", len(vs)) | |
723 | + } | |
724 | + | |
725 | + if _, ok := vs[0].([]byte); !ok { | |
726 | + t.Fatalf("first result is type %T, expected []byte", vs[0]) | |
727 | + } | |
728 | + | |
729 | + if _, ok := vs[1].(error); !ok { | |
730 | + t.Fatalf("second result is type %T, expected error", vs[2]) | |
731 | + } | |
732 | +} | |
733 | + | |
734 | +func BenchmarkDoEmpty(b *testing.B) { | |
735 | + b.StopTimer() | |
736 | + c, err := redis.DialDefaultServer() | |
737 | + if err != nil { | |
738 | + b.Fatal(err) | |
739 | + } | |
740 | + defer c.Close() | |
741 | + b.StartTimer() | |
742 | + for i := 0; i < b.N; i++ { | |
743 | + if _, err := c.Do(""); err != nil { | |
744 | + b.Fatal(err) | |
745 | + } | |
746 | + } | |
747 | +} | |
748 | + | |
749 | +func BenchmarkDoPing(b *testing.B) { | |
750 | + b.StopTimer() | |
751 | + c, err := redis.DialDefaultServer() | |
752 | + if err != nil { | |
753 | + b.Fatal(err) | |
754 | + } | |
755 | + defer c.Close() | |
756 | + b.StartTimer() | |
757 | + for i := 0; i < b.N; i++ { | |
758 | + if _, err := c.Do("PING"); err != nil { | |
759 | + b.Fatal(err) | |
760 | + } | |
761 | + } | |
762 | +} | |
763 | + | |
764 | +var clientTLSConfig, serverTLSConfig tls.Config | |
765 | + | |
766 | +func init() { | |
767 | + // The certificate and key for testing TLS dial options was created | |
768 | + // using the command | |
769 | + // | |
770 | + // go run GOROOT/src/crypto/tls/generate_cert.go \ | |
771 | + // --rsa-bits 1024 \ | |
772 | + // --host 127.0.0.1,::1,example.com --ca \ | |
773 | + // --start-date "Jan 1 00:00:00 1970" \ | |
774 | + // --duration=1000000h | |
775 | + // | |
776 | + // where GOROOT is the value of GOROOT reported by go env. | |
777 | + localhostCert := []byte(` | |
778 | +-----BEGIN CERTIFICATE----- | |
779 | +MIICFDCCAX2gAwIBAgIRAJfBL4CUxkXcdlFurb3K+iowDQYJKoZIhvcNAQELBQAw | |
780 | +EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2 | |
781 | +MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw | |
782 | +gYkCgYEArizw8WxMUQ3bGHLeuJ4fDrEpy+L2pqrbYRlKk1DasJ/VkB8bImzIpe6+ | |
783 | +LGjiYIxvnDCOJ3f3QplcQuiuMyl6f2irJlJsbFT8Lo/3obnuTKAIaqUdJUqBg6y+ | |
784 | +JaL8Auk97FvunfKFv8U1AIhgiLzAfQ/3Eaq1yi87Ra6pMjGbTtcCAwEAAaNoMGYw | |
785 | +DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF | |
786 | +MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA | |
787 | +AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAdZ8daIVkyhVwflt5I19m0oq1TycbGO1+ | |
788 | +ach7T6cZiBQeNR/SJtxr/wKPEpmvUgbv2BfFrKJ8QoIHYsbNSURTWSEa02pfw4k9 | |
789 | +6RQhij3ZkG79Ituj5OYRORV6Z0HUW32r670BtcuHuAhq7YA6Nxy4FtSt7bAlVdRt | |
790 | +rrKgNsltzMk= | |
791 | +-----END CERTIFICATE-----`) | |
792 | + | |
793 | + localhostKey := []byte(` | |
794 | +-----BEGIN RSA PRIVATE KEY----- | |
795 | +MIICXAIBAAKBgQCuLPDxbExRDdsYct64nh8OsSnL4vamqtthGUqTUNqwn9WQHxsi | |
796 | +bMil7r4saOJgjG+cMI4nd/dCmVxC6K4zKXp/aKsmUmxsVPwuj/ehue5MoAhqpR0l | |
797 | +SoGDrL4lovwC6T3sW+6d8oW/xTUAiGCIvMB9D/cRqrXKLztFrqkyMZtO1wIDAQAB | |
798 | +AoGACrc5G6FOEK6JjDeE/Fa+EmlT6PdNtXNNi+vCas3Opo8u1G8VfEi1D4BgstrB | |
799 | +Eq+RLkrOdB8tVyuYQYWPMhabMqF+hhKJN72j0OwfuPlVvTInwb/cKjo/zbH1IA+Y | |
800 | +HenHNK4ywv7/p/9/MvQPJ3I32cQBCgGUW5chVSH5M1sj5gECQQDabQAI1X0uDqCm | |
801 | +KbX9gXVkAgxkFddrt6LBHt57xujFcqEKFE7nwKhDh7DweVs/VEJ+kpid4z+UnLOw | |
802 | +KjtP9JolAkEAzCNBphQ//IsbH5rNs10wIUw3Ks/Oepicvr6kUFbIv+neRzi1iJHa | |
803 | +m6H7EayK3PWgax6BAsR/t0Jc9XV7r2muSwJAVzN09BHnK+ADGtNEKLTqXMbEk6B0 | |
804 | +pDhn7ZmZUOkUPN+Kky+QYM11X6Bob1jDqQDGmymDbGUxGO+GfSofC8inUQJAGfci | |
805 | +Eo3g1a6b9JksMPRZeuLG4ZstGErxJRH6tH1Va5PDwitka8qhk8o2tTjNMO3NSdLH | |
806 | +diKoXBcE2/Pll5pJoQJBAIMiiMIzXJhnN4mX8may44J/HvMlMf2xuVH2gNMwmZuc | |
807 | +Bjqn3yoLHaoZVvbWOi0C2TCN4FjXjaLNZGifQPbIcaA= | |
808 | +-----END RSA PRIVATE KEY-----`) | |
809 | + | |
810 | + cert, err := tls.X509KeyPair(localhostCert, localhostKey) | |
811 | + if err != nil { | |
812 | + panic(fmt.Sprintf("error creating key pair: %v", err)) | |
813 | + } | |
814 | + serverTLSConfig.Certificates = []tls.Certificate{cert} | |
815 | + | |
816 | + certificate, err := x509.ParseCertificate(serverTLSConfig.Certificates[0].Certificate[0]) | |
817 | + if err != nil { | |
818 | + panic(fmt.Sprintf("error parsing x509 certificate: %v", err)) | |
819 | + } | |
820 | + | |
821 | + clientTLSConfig.RootCAs = x509.NewCertPool() | |
822 | + clientTLSConfig.RootCAs.AddCert(certificate) | |
823 | +} | ... | ... |
... | ... | @@ -0,0 +1,177 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +// Package redis is a client for the Redis database. | |
16 | +// | |
17 | +// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more | |
18 | +// documentation about this package. | |
19 | +// | |
20 | +// Connections | |
21 | +// | |
22 | +// The Conn interface is the primary interface for working with Redis. | |
23 | +// Applications create connections by calling the Dial, DialWithTimeout or | |
24 | +// NewConn functions. In the future, functions will be added for creating | |
25 | +// sharded and other types of connections. | |
26 | +// | |
27 | +// The application must call the connection Close method when the application | |
28 | +// is done with the connection. | |
29 | +// | |
30 | +// Executing Commands | |
31 | +// | |
32 | +// The Conn interface has a generic method for executing Redis commands: | |
33 | +// | |
34 | +// Do(commandName string, args ...interface{}) (reply interface{}, err error) | |
35 | +// | |
36 | +// The Redis command reference (http://redis.io/commands) lists the available | |
37 | +// commands. An example of using the Redis APPEND command is: | |
38 | +// | |
39 | +// n, err := conn.Do("APPEND", "key", "value") | |
40 | +// | |
41 | +// The Do method converts command arguments to bulk strings for transmission | |
42 | +// to the server as follows: | |
43 | +// | |
44 | +// Go Type Conversion | |
45 | +// []byte Sent as is | |
46 | +// string Sent as is | |
47 | +// int, int64 strconv.FormatInt(v) | |
48 | +// float64 strconv.FormatFloat(v, 'g', -1, 64) | |
49 | +// bool true -> "1", false -> "0" | |
50 | +// nil "" | |
51 | +// all other types fmt.Fprint(w, v) | |
52 | +// | |
53 | +// Redis command reply types are represented using the following Go types: | |
54 | +// | |
55 | +// Redis type Go type | |
56 | +// error redis.Error | |
57 | +// integer int64 | |
58 | +// simple string string | |
59 | +// bulk string []byte or nil if value not present. | |
60 | +// array []interface{} or nil if value not present. | |
61 | +// | |
62 | +// Use type assertions or the reply helper functions to convert from | |
63 | +// interface{} to the specific Go type for the command result. | |
64 | +// | |
65 | +// Pipelining | |
66 | +// | |
67 | +// Connections support pipelining using the Send, Flush and Receive methods. | |
68 | +// | |
69 | +// Send(commandName string, args ...interface{}) error | |
70 | +// Flush() error | |
71 | +// Receive() (reply interface{}, err error) | |
72 | +// | |
73 | +// Send writes the command to the connection's output buffer. Flush flushes the | |
74 | +// connection's output buffer to the server. Receive reads a single reply from | |
75 | +// the server. The following example shows a simple pipeline. | |
76 | +// | |
77 | +// c.Send("SET", "foo", "bar") | |
78 | +// c.Send("GET", "foo") | |
79 | +// c.Flush() | |
80 | +// c.Receive() // reply from SET | |
81 | +// v, err = c.Receive() // reply from GET | |
82 | +// | |
83 | +// The Do method combines the functionality of the Send, Flush and Receive | |
84 | +// methods. The Do method starts by writing the command and flushing the output | |
85 | +// buffer. Next, the Do method receives all pending replies including the reply | |
86 | +// for the command just sent by Do. If any of the received replies is an error, | |
87 | +// then Do returns the error. If there are no errors, then Do returns the last | |
88 | +// reply. If the command argument to the Do method is "", then the Do method | |
89 | +// will flush the output buffer and receive pending replies without sending a | |
90 | +// command. | |
91 | +// | |
92 | +// Use the Send and Do methods to implement pipelined transactions. | |
93 | +// | |
94 | +// c.Send("MULTI") | |
95 | +// c.Send("INCR", "foo") | |
96 | +// c.Send("INCR", "bar") | |
97 | +// r, err := c.Do("EXEC") | |
98 | +// fmt.Println(r) // prints [1, 1] | |
99 | +// | |
100 | +// Concurrency | |
101 | +// | |
102 | +// Connections support one concurrent caller to the Receive method and one | |
103 | +// concurrent caller to the Send and Flush methods. No other concurrency is | |
104 | +// supported including concurrent calls to the Do method. | |
105 | +// | |
106 | +// For full concurrent access to Redis, use the thread-safe Pool to get, use | |
107 | +// and release a connection from within a goroutine. Connections returned from | |
108 | +// a Pool have the concurrency restrictions described in the previous | |
109 | +// paragraph. | |
110 | +// | |
111 | +// Publish and Subscribe | |
112 | +// | |
113 | +// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers. | |
114 | +// | |
115 | +// c.Send("SUBSCRIBE", "example") | |
116 | +// c.Flush() | |
117 | +// for { | |
118 | +// reply, err := c.Receive() | |
119 | +// if err != nil { | |
120 | +// return err | |
121 | +// } | |
122 | +// // process pushed message | |
123 | +// } | |
124 | +// | |
125 | +// The PubSubConn type wraps a Conn with convenience methods for implementing | |
126 | +// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods | |
127 | +// send and flush a subscription management command. The receive method | |
128 | +// converts a pushed message to convenient types for use in a type switch. | |
129 | +// | |
130 | +// psc := redis.PubSubConn{Conn: c} | |
131 | +// psc.Subscribe("example") | |
132 | +// for { | |
133 | +// switch v := psc.Receive().(type) { | |
134 | +// case redis.Message: | |
135 | +// fmt.Printf("%s: message: %s\n", v.Channel, v.Data) | |
136 | +// case redis.Subscription: | |
137 | +// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) | |
138 | +// case error: | |
139 | +// return v | |
140 | +// } | |
141 | +// } | |
142 | +// | |
143 | +// Reply Helpers | |
144 | +// | |
145 | +// The Bool, Int, Bytes, String, Strings and Values functions convert a reply | |
146 | +// to a value of a specific type. To allow convenient wrapping of calls to the | |
147 | +// connection Do and Receive methods, the functions take a second argument of | |
148 | +// type error. If the error is non-nil, then the helper function returns the | |
149 | +// error. If the error is nil, the function converts the reply to the specified | |
150 | +// type: | |
151 | +// | |
152 | +// exists, err := redis.Bool(c.Do("EXISTS", "foo")) | |
153 | +// if err != nil { | |
154 | +// // handle error return from c.Do or type conversion error. | |
155 | +// } | |
156 | +// | |
157 | +// The Scan function converts elements of a array reply to Go types: | |
158 | +// | |
159 | +// var value1 int | |
160 | +// var value2 string | |
161 | +// reply, err := redis.Values(c.Do("MGET", "key1", "key2")) | |
162 | +// if err != nil { | |
163 | +// // handle error | |
164 | +// } | |
165 | +// if _, err := redis.Scan(reply, &value1, &value2); err != nil { | |
166 | +// // handle error | |
167 | +// } | |
168 | +// | |
169 | +// Errors | |
170 | +// | |
171 | +// Connection methods return error replies from the server as type redis.Error. | |
172 | +// | |
173 | +// Call the connection Err() method to determine if the connection encountered | |
174 | +// non-recoverable error such as a network error or protocol parsing error. If | |
175 | +// Err() returns a non-nil value, then the connection is not usable and should | |
176 | +// be closed. | |
177 | +package redis // import "github.com/garyburd/redigo/redis" | ... | ... |
... | ... | @@ -0,0 +1,33 @@ |
1 | +// +build go1.7 | |
2 | + | |
3 | +package redis | |
4 | + | |
5 | +import "crypto/tls" | |
6 | + | |
7 | +// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case | |
8 | +func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config { | |
9 | + if cfg == nil { | |
10 | + return &tls.Config{InsecureSkipVerify: skipVerify} | |
11 | + } | |
12 | + return &tls.Config{ | |
13 | + Rand: cfg.Rand, | |
14 | + Time: cfg.Time, | |
15 | + Certificates: cfg.Certificates, | |
16 | + NameToCertificate: cfg.NameToCertificate, | |
17 | + GetCertificate: cfg.GetCertificate, | |
18 | + RootCAs: cfg.RootCAs, | |
19 | + NextProtos: cfg.NextProtos, | |
20 | + ServerName: cfg.ServerName, | |
21 | + ClientAuth: cfg.ClientAuth, | |
22 | + ClientCAs: cfg.ClientCAs, | |
23 | + InsecureSkipVerify: cfg.InsecureSkipVerify, | |
24 | + CipherSuites: cfg.CipherSuites, | |
25 | + PreferServerCipherSuites: cfg.PreferServerCipherSuites, | |
26 | + ClientSessionCache: cfg.ClientSessionCache, | |
27 | + MinVersion: cfg.MinVersion, | |
28 | + MaxVersion: cfg.MaxVersion, | |
29 | + CurvePreferences: cfg.CurvePreferences, | |
30 | + DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled, | |
31 | + Renegotiation: cfg.Renegotiation, | |
32 | + } | |
33 | +} | ... | ... |
... | ... | @@ -0,0 +1,117 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "bytes" | |
19 | + "fmt" | |
20 | + "log" | |
21 | +) | |
22 | + | |
23 | +// NewLoggingConn returns a logging wrapper around a connection. | |
24 | +func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn { | |
25 | + if prefix != "" { | |
26 | + prefix = prefix + "." | |
27 | + } | |
28 | + return &loggingConn{conn, logger, prefix} | |
29 | +} | |
30 | + | |
31 | +type loggingConn struct { | |
32 | + Conn | |
33 | + logger *log.Logger | |
34 | + prefix string | |
35 | +} | |
36 | + | |
37 | +func (c *loggingConn) Close() error { | |
38 | + err := c.Conn.Close() | |
39 | + var buf bytes.Buffer | |
40 | + fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err) | |
41 | + c.logger.Output(2, buf.String()) | |
42 | + return err | |
43 | +} | |
44 | + | |
45 | +func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) { | |
46 | + const chop = 32 | |
47 | + switch v := v.(type) { | |
48 | + case []byte: | |
49 | + if len(v) > chop { | |
50 | + fmt.Fprintf(buf, "%q...", v[:chop]) | |
51 | + } else { | |
52 | + fmt.Fprintf(buf, "%q", v) | |
53 | + } | |
54 | + case string: | |
55 | + if len(v) > chop { | |
56 | + fmt.Fprintf(buf, "%q...", v[:chop]) | |
57 | + } else { | |
58 | + fmt.Fprintf(buf, "%q", v) | |
59 | + } | |
60 | + case []interface{}: | |
61 | + if len(v) == 0 { | |
62 | + buf.WriteString("[]") | |
63 | + } else { | |
64 | + sep := "[" | |
65 | + fin := "]" | |
66 | + if len(v) > chop { | |
67 | + v = v[:chop] | |
68 | + fin = "...]" | |
69 | + } | |
70 | + for _, vv := range v { | |
71 | + buf.WriteString(sep) | |
72 | + c.printValue(buf, vv) | |
73 | + sep = ", " | |
74 | + } | |
75 | + buf.WriteString(fin) | |
76 | + } | |
77 | + default: | |
78 | + fmt.Fprint(buf, v) | |
79 | + } | |
80 | +} | |
81 | + | |
82 | +func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) { | |
83 | + var buf bytes.Buffer | |
84 | + fmt.Fprintf(&buf, "%s%s(", c.prefix, method) | |
85 | + if method != "Receive" { | |
86 | + buf.WriteString(commandName) | |
87 | + for _, arg := range args { | |
88 | + buf.WriteString(", ") | |
89 | + c.printValue(&buf, arg) | |
90 | + } | |
91 | + } | |
92 | + buf.WriteString(") -> (") | |
93 | + if method != "Send" { | |
94 | + c.printValue(&buf, reply) | |
95 | + buf.WriteString(", ") | |
96 | + } | |
97 | + fmt.Fprintf(&buf, "%v)", err) | |
98 | + c.logger.Output(3, buf.String()) | |
99 | +} | |
100 | + | |
101 | +func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) { | |
102 | + reply, err := c.Conn.Do(commandName, args...) | |
103 | + c.print("Do", commandName, args, reply, err) | |
104 | + return reply, err | |
105 | +} | |
106 | + | |
107 | +func (c *loggingConn) Send(commandName string, args ...interface{}) error { | |
108 | + err := c.Conn.Send(commandName, args...) | |
109 | + c.print("Send", commandName, args, nil, err) | |
110 | + return err | |
111 | +} | |
112 | + | |
113 | +func (c *loggingConn) Receive() (interface{}, error) { | |
114 | + reply, err := c.Conn.Receive() | |
115 | + c.print("Receive", "", nil, reply, err) | |
116 | + return reply, err | |
117 | +} | ... | ... |
... | ... | @@ -0,0 +1,442 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "bytes" | |
19 | + "container/list" | |
20 | + "crypto/rand" | |
21 | + "crypto/sha1" | |
22 | + "errors" | |
23 | + "io" | |
24 | + "strconv" | |
25 | + "sync" | |
26 | + "time" | |
27 | + | |
28 | + "github.com/garyburd/redigo/internal" | |
29 | +) | |
30 | + | |
31 | +var nowFunc = time.Now // for testing | |
32 | + | |
33 | +// ErrPoolExhausted is returned from a pool connection method (Do, Send, | |
34 | +// Receive, Flush, Err) when the maximum number of database connections in the | |
35 | +// pool has been reached. | |
36 | +var ErrPoolExhausted = errors.New("redigo: connection pool exhausted") | |
37 | + | |
38 | +var ( | |
39 | + errPoolClosed = errors.New("redigo: connection pool closed") | |
40 | + errConnClosed = errors.New("redigo: connection closed") | |
41 | +) | |
42 | + | |
43 | +// Pool maintains a pool of connections. The application calls the Get method | |
44 | +// to get a connection from the pool and the connection's Close method to | |
45 | +// return the connection's resources to the pool. | |
46 | +// | |
47 | +// The following example shows how to use a pool in a web application. The | |
48 | +// application creates a pool at application startup and makes it available to | |
49 | +// request handlers using a package level variable. The pool configuration used | |
50 | +// here is an example, not a recommendation. | |
51 | +// | |
52 | +// func newPool(addr string) *redis.Pool { | |
53 | +// return &redis.Pool{ | |
54 | +// MaxIdle: 3, | |
55 | +// IdleTimeout: 240 * time.Second, | |
56 | +// Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) }, | |
57 | +// } | |
58 | +// } | |
59 | +// | |
60 | +// var ( | |
61 | +// pool *redis.Pool | |
62 | +// redisServer = flag.String("redisServer", ":6379", "") | |
63 | +// ) | |
64 | +// | |
65 | +// func main() { | |
66 | +// flag.Parse() | |
67 | +// pool = newPool(*redisServer) | |
68 | +// ... | |
69 | +// } | |
70 | +// | |
71 | +// A request handler gets a connection from the pool and closes the connection | |
72 | +// when the handler is done: | |
73 | +// | |
74 | +// func serveHome(w http.ResponseWriter, r *http.Request) { | |
75 | +// conn := pool.Get() | |
76 | +// defer conn.Close() | |
77 | +// ... | |
78 | +// } | |
79 | +// | |
80 | +// Use the Dial function to authenticate connections with the AUTH command or | |
81 | +// select a database with the SELECT command: | |
82 | +// | |
83 | +// pool := &redis.Pool{ | |
84 | +// // Other pool configuration not shown in this example. | |
85 | +// Dial: func () (redis.Conn, error) { | |
86 | +// c, err := redis.Dial("tcp", server) | |
87 | +// if err != nil { | |
88 | +// return nil, err | |
89 | +// } | |
90 | +// if _, err := c.Do("AUTH", password); err != nil { | |
91 | +// c.Close() | |
92 | +// return nil, err | |
93 | +// } | |
94 | +// if _, err := c.Do("SELECT", db); err != nil { | |
95 | +// c.Close() | |
96 | +// return nil, err | |
97 | +// } | |
98 | +// return c, nil | |
99 | +// } | |
100 | +// } | |
101 | +// | |
102 | +// Use the TestOnBorrow function to check the health of an idle connection | |
103 | +// before the connection is returned to the application. This example PINGs | |
104 | +// connections that have been idle more than a minute: | |
105 | +// | |
106 | +// pool := &redis.Pool{ | |
107 | +// // Other pool configuration not shown in this example. | |
108 | +// TestOnBorrow: func(c redis.Conn, t time.Time) error { | |
109 | +// if time.Since(t) < time.Minute { | |
110 | +// return nil | |
111 | +// } | |
112 | +// _, err := c.Do("PING") | |
113 | +// return err | |
114 | +// }, | |
115 | +// } | |
116 | +// | |
117 | +type Pool struct { | |
118 | + // Dial is an application supplied function for creating and configuring a | |
119 | + // connection. | |
120 | + // | |
121 | + // The connection returned from Dial must not be in a special state | |
122 | + // (subscribed to pubsub channel, transaction started, ...). | |
123 | + Dial func() (Conn, error) | |
124 | + | |
125 | + // TestOnBorrow is an optional application supplied function for checking | |
126 | + // the health of an idle connection before the connection is used again by | |
127 | + // the application. Argument t is the time that the connection was returned | |
128 | + // to the pool. If the function returns an error, then the connection is | |
129 | + // closed. | |
130 | + TestOnBorrow func(c Conn, t time.Time) error | |
131 | + | |
132 | + // Maximum number of idle connections in the pool. | |
133 | + MaxIdle int | |
134 | + | |
135 | + // Maximum number of connections allocated by the pool at a given time. | |
136 | + // When zero, there is no limit on the number of connections in the pool. | |
137 | + MaxActive int | |
138 | + | |
139 | + // Close connections after remaining idle for this duration. If the value | |
140 | + // is zero, then idle connections are not closed. Applications should set | |
141 | + // the timeout to a value less than the server's timeout. | |
142 | + IdleTimeout time.Duration | |
143 | + | |
144 | + // If Wait is true and the pool is at the MaxActive limit, then Get() waits | |
145 | + // for a connection to be returned to the pool before returning. | |
146 | + Wait bool | |
147 | + | |
148 | + // mu protects fields defined below. | |
149 | + mu sync.Mutex | |
150 | + cond *sync.Cond | |
151 | + closed bool | |
152 | + active int | |
153 | + | |
154 | + // Stack of idleConn with most recently used at the front. | |
155 | + idle list.List | |
156 | +} | |
157 | + | |
158 | +type idleConn struct { | |
159 | + c Conn | |
160 | + t time.Time | |
161 | +} | |
162 | + | |
163 | +// NewPool creates a new pool. | |
164 | +// | |
165 | +// Deprecated: Initialize the Pool directory as shown in the example. | |
166 | +func NewPool(newFn func() (Conn, error), maxIdle int) *Pool { | |
167 | + return &Pool{Dial: newFn, MaxIdle: maxIdle} | |
168 | +} | |
169 | + | |
170 | +// Get gets a connection. The application must close the returned connection. | |
171 | +// This method always returns a valid connection so that applications can defer | |
172 | +// error handling to the first use of the connection. If there is an error | |
173 | +// getting an underlying connection, then the connection Err, Do, Send, Flush | |
174 | +// and Receive methods return that error. | |
175 | +func (p *Pool) Get() Conn { | |
176 | + c, err := p.get() | |
177 | + if err != nil { | |
178 | + return errorConnection{err} | |
179 | + } | |
180 | + return &pooledConnection{p: p, c: c} | |
181 | +} | |
182 | + | |
183 | +// PoolStats contains pool statistics. | |
184 | +type PoolStats struct { | |
185 | + // ActiveCount is the number of connections in the pool. The count includes idle connections and connections in use. | |
186 | + ActiveCount int | |
187 | + // IdleCount is the number of idle connections in the pool. | |
188 | + IdleCount int | |
189 | +} | |
190 | + | |
191 | +// Stats returns pool's statistics. | |
192 | +func (p *Pool) Stats() PoolStats { | |
193 | + p.mu.Lock() | |
194 | + stats := PoolStats{ | |
195 | + ActiveCount: p.active, | |
196 | + IdleCount: p.idle.Len(), | |
197 | + } | |
198 | + p.mu.Unlock() | |
199 | + | |
200 | + return stats | |
201 | +} | |
202 | + | |
203 | +// ActiveCount returns the number of connections in the pool. The count includes idle connections and connections in use. | |
204 | +func (p *Pool) ActiveCount() int { | |
205 | + p.mu.Lock() | |
206 | + active := p.active | |
207 | + p.mu.Unlock() | |
208 | + return active | |
209 | +} | |
210 | + | |
211 | +// IdleCount returns the number of idle connections in the pool. | |
212 | +func (p *Pool) IdleCount() int { | |
213 | + p.mu.Lock() | |
214 | + idle := p.idle.Len() | |
215 | + p.mu.Unlock() | |
216 | + return idle | |
217 | +} | |
218 | + | |
219 | +// Close releases the resources used by the pool. | |
220 | +func (p *Pool) Close() error { | |
221 | + p.mu.Lock() | |
222 | + idle := p.idle | |
223 | + p.idle.Init() | |
224 | + p.closed = true | |
225 | + p.active -= idle.Len() | |
226 | + if p.cond != nil { | |
227 | + p.cond.Broadcast() | |
228 | + } | |
229 | + p.mu.Unlock() | |
230 | + for e := idle.Front(); e != nil; e = e.Next() { | |
231 | + e.Value.(idleConn).c.Close() | |
232 | + } | |
233 | + return nil | |
234 | +} | |
235 | + | |
236 | +// release decrements the active count and signals waiters. The caller must | |
237 | +// hold p.mu during the call. | |
238 | +func (p *Pool) release() { | |
239 | + p.active -= 1 | |
240 | + if p.cond != nil { | |
241 | + p.cond.Signal() | |
242 | + } | |
243 | +} | |
244 | + | |
245 | +// get prunes stale connections and returns a connection from the idle list or | |
246 | +// creates a new connection. | |
247 | +func (p *Pool) get() (Conn, error) { | |
248 | + p.mu.Lock() | |
249 | + | |
250 | + // Prune stale connections. | |
251 | + | |
252 | + if timeout := p.IdleTimeout; timeout > 0 { | |
253 | + for i, n := 0, p.idle.Len(); i < n; i++ { | |
254 | + e := p.idle.Back() | |
255 | + if e == nil { | |
256 | + break | |
257 | + } | |
258 | + ic := e.Value.(idleConn) | |
259 | + if ic.t.Add(timeout).After(nowFunc()) { | |
260 | + break | |
261 | + } | |
262 | + p.idle.Remove(e) | |
263 | + p.release() | |
264 | + p.mu.Unlock() | |
265 | + ic.c.Close() | |
266 | + p.mu.Lock() | |
267 | + } | |
268 | + } | |
269 | + | |
270 | + for { | |
271 | + // Get idle connection. | |
272 | + | |
273 | + for i, n := 0, p.idle.Len(); i < n; i++ { | |
274 | + e := p.idle.Front() | |
275 | + if e == nil { | |
276 | + break | |
277 | + } | |
278 | + ic := e.Value.(idleConn) | |
279 | + p.idle.Remove(e) | |
280 | + test := p.TestOnBorrow | |
281 | + p.mu.Unlock() | |
282 | + if test == nil || test(ic.c, ic.t) == nil { | |
283 | + return ic.c, nil | |
284 | + } | |
285 | + ic.c.Close() | |
286 | + p.mu.Lock() | |
287 | + p.release() | |
288 | + } | |
289 | + | |
290 | + // Check for pool closed before dialing a new connection. | |
291 | + | |
292 | + if p.closed { | |
293 | + p.mu.Unlock() | |
294 | + return nil, errors.New("redigo: get on closed pool") | |
295 | + } | |
296 | + | |
297 | + // Dial new connection if under limit. | |
298 | + | |
299 | + if p.MaxActive == 0 || p.active < p.MaxActive { | |
300 | + dial := p.Dial | |
301 | + p.active += 1 | |
302 | + p.mu.Unlock() | |
303 | + c, err := dial() | |
304 | + if err != nil { | |
305 | + p.mu.Lock() | |
306 | + p.release() | |
307 | + p.mu.Unlock() | |
308 | + c = nil | |
309 | + } | |
310 | + return c, err | |
311 | + } | |
312 | + | |
313 | + if !p.Wait { | |
314 | + p.mu.Unlock() | |
315 | + return nil, ErrPoolExhausted | |
316 | + } | |
317 | + | |
318 | + if p.cond == nil { | |
319 | + p.cond = sync.NewCond(&p.mu) | |
320 | + } | |
321 | + p.cond.Wait() | |
322 | + } | |
323 | +} | |
324 | + | |
325 | +func (p *Pool) put(c Conn, forceClose bool) error { | |
326 | + err := c.Err() | |
327 | + p.mu.Lock() | |
328 | + if !p.closed && err == nil && !forceClose { | |
329 | + p.idle.PushFront(idleConn{t: nowFunc(), c: c}) | |
330 | + if p.idle.Len() > p.MaxIdle { | |
331 | + c = p.idle.Remove(p.idle.Back()).(idleConn).c | |
332 | + } else { | |
333 | + c = nil | |
334 | + } | |
335 | + } | |
336 | + | |
337 | + if c == nil { | |
338 | + if p.cond != nil { | |
339 | + p.cond.Signal() | |
340 | + } | |
341 | + p.mu.Unlock() | |
342 | + return nil | |
343 | + } | |
344 | + | |
345 | + p.release() | |
346 | + p.mu.Unlock() | |
347 | + return c.Close() | |
348 | +} | |
349 | + | |
350 | +type pooledConnection struct { | |
351 | + p *Pool | |
352 | + c Conn | |
353 | + state int | |
354 | +} | |
355 | + | |
356 | +var ( | |
357 | + sentinel []byte | |
358 | + sentinelOnce sync.Once | |
359 | +) | |
360 | + | |
361 | +func initSentinel() { | |
362 | + p := make([]byte, 64) | |
363 | + if _, err := rand.Read(p); err == nil { | |
364 | + sentinel = p | |
365 | + } else { | |
366 | + h := sha1.New() | |
367 | + io.WriteString(h, "Oops, rand failed. Use time instead.") | |
368 | + io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) | |
369 | + sentinel = h.Sum(nil) | |
370 | + } | |
371 | +} | |
372 | + | |
373 | +func (pc *pooledConnection) Close() error { | |
374 | + c := pc.c | |
375 | + if _, ok := c.(errorConnection); ok { | |
376 | + return nil | |
377 | + } | |
378 | + pc.c = errorConnection{errConnClosed} | |
379 | + | |
380 | + if pc.state&internal.MultiState != 0 { | |
381 | + c.Send("DISCARD") | |
382 | + pc.state &^= (internal.MultiState | internal.WatchState) | |
383 | + } else if pc.state&internal.WatchState != 0 { | |
384 | + c.Send("UNWATCH") | |
385 | + pc.state &^= internal.WatchState | |
386 | + } | |
387 | + if pc.state&internal.SubscribeState != 0 { | |
388 | + c.Send("UNSUBSCRIBE") | |
389 | + c.Send("PUNSUBSCRIBE") | |
390 | + // To detect the end of the message stream, ask the server to echo | |
391 | + // a sentinel value and read until we see that value. | |
392 | + sentinelOnce.Do(initSentinel) | |
393 | + c.Send("ECHO", sentinel) | |
394 | + c.Flush() | |
395 | + for { | |
396 | + p, err := c.Receive() | |
397 | + if err != nil { | |
398 | + break | |
399 | + } | |
400 | + if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) { | |
401 | + pc.state &^= internal.SubscribeState | |
402 | + break | |
403 | + } | |
404 | + } | |
405 | + } | |
406 | + c.Do("") | |
407 | + pc.p.put(c, pc.state != 0) | |
408 | + return nil | |
409 | +} | |
410 | + | |
411 | +func (pc *pooledConnection) Err() error { | |
412 | + return pc.c.Err() | |
413 | +} | |
414 | + | |
415 | +func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) { | |
416 | + ci := internal.LookupCommandInfo(commandName) | |
417 | + pc.state = (pc.state | ci.Set) &^ ci.Clear | |
418 | + return pc.c.Do(commandName, args...) | |
419 | +} | |
420 | + | |
421 | +func (pc *pooledConnection) Send(commandName string, args ...interface{}) error { | |
422 | + ci := internal.LookupCommandInfo(commandName) | |
423 | + pc.state = (pc.state | ci.Set) &^ ci.Clear | |
424 | + return pc.c.Send(commandName, args...) | |
425 | +} | |
426 | + | |
427 | +func (pc *pooledConnection) Flush() error { | |
428 | + return pc.c.Flush() | |
429 | +} | |
430 | + | |
431 | +func (pc *pooledConnection) Receive() (reply interface{}, err error) { | |
432 | + return pc.c.Receive() | |
433 | +} | |
434 | + | |
435 | +type errorConnection struct{ err error } | |
436 | + | |
437 | +func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } | |
438 | +func (ec errorConnection) Send(string, ...interface{}) error { return ec.err } | |
439 | +func (ec errorConnection) Err() error { return ec.err } | |
440 | +func (ec errorConnection) Close() error { return ec.err } | |
441 | +func (ec errorConnection) Flush() error { return ec.err } | |
442 | +func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err } | ... | ... |
... | ... | @@ -0,0 +1,691 @@ |
1 | +// Copyright 2011 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "errors" | |
19 | + "io" | |
20 | + "reflect" | |
21 | + "sync" | |
22 | + "testing" | |
23 | + "time" | |
24 | + | |
25 | + "github.com/garyburd/redigo/redis" | |
26 | +) | |
27 | + | |
28 | +type poolTestConn struct { | |
29 | + d *poolDialer | |
30 | + err error | |
31 | + redis.Conn | |
32 | +} | |
33 | + | |
34 | +func (c *poolTestConn) Close() error { | |
35 | + c.d.mu.Lock() | |
36 | + c.d.open -= 1 | |
37 | + c.d.mu.Unlock() | |
38 | + return c.Conn.Close() | |
39 | +} | |
40 | + | |
41 | +func (c *poolTestConn) Err() error { return c.err } | |
42 | + | |
43 | +func (c *poolTestConn) Do(commandName string, args ...interface{}) (interface{}, error) { | |
44 | + if commandName == "ERR" { | |
45 | + c.err = args[0].(error) | |
46 | + commandName = "PING" | |
47 | + } | |
48 | + if commandName != "" { | |
49 | + c.d.commands = append(c.d.commands, commandName) | |
50 | + } | |
51 | + return c.Conn.Do(commandName, args...) | |
52 | +} | |
53 | + | |
54 | +func (c *poolTestConn) Send(commandName string, args ...interface{}) error { | |
55 | + c.d.commands = append(c.d.commands, commandName) | |
56 | + return c.Conn.Send(commandName, args...) | |
57 | +} | |
58 | + | |
59 | +type poolDialer struct { | |
60 | + mu sync.Mutex | |
61 | + t *testing.T | |
62 | + dialed int | |
63 | + open int | |
64 | + commands []string | |
65 | + dialErr error | |
66 | +} | |
67 | + | |
68 | +func (d *poolDialer) dial() (redis.Conn, error) { | |
69 | + d.mu.Lock() | |
70 | + d.dialed += 1 | |
71 | + dialErr := d.dialErr | |
72 | + d.mu.Unlock() | |
73 | + if dialErr != nil { | |
74 | + return nil, d.dialErr | |
75 | + } | |
76 | + c, err := redis.DialDefaultServer() | |
77 | + if err != nil { | |
78 | + return nil, err | |
79 | + } | |
80 | + d.mu.Lock() | |
81 | + d.open += 1 | |
82 | + d.mu.Unlock() | |
83 | + return &poolTestConn{d: d, Conn: c}, nil | |
84 | +} | |
85 | + | |
86 | +func (d *poolDialer) check(message string, p *redis.Pool, dialed, open, inuse int) { | |
87 | + d.mu.Lock() | |
88 | + if d.dialed != dialed { | |
89 | + d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed) | |
90 | + } | |
91 | + if d.open != open { | |
92 | + d.t.Errorf("%s: open=%d, want %d", message, d.open, open) | |
93 | + } | |
94 | + | |
95 | + stats := p.Stats() | |
96 | + | |
97 | + if stats.ActiveCount != open { | |
98 | + d.t.Errorf("%s: active=%d, want %d", message, stats.ActiveCount, open) | |
99 | + } | |
100 | + if stats.IdleCount != open-inuse { | |
101 | + d.t.Errorf("%s: idle=%d, want %d", message, stats.IdleCount, open-inuse) | |
102 | + } | |
103 | + | |
104 | + d.mu.Unlock() | |
105 | +} | |
106 | + | |
107 | +func TestPoolReuse(t *testing.T) { | |
108 | + d := poolDialer{t: t} | |
109 | + p := &redis.Pool{ | |
110 | + MaxIdle: 2, | |
111 | + Dial: d.dial, | |
112 | + } | |
113 | + | |
114 | + for i := 0; i < 10; i++ { | |
115 | + c1 := p.Get() | |
116 | + c1.Do("PING") | |
117 | + c2 := p.Get() | |
118 | + c2.Do("PING") | |
119 | + c1.Close() | |
120 | + c2.Close() | |
121 | + } | |
122 | + | |
123 | + d.check("before close", p, 2, 2, 0) | |
124 | + p.Close() | |
125 | + d.check("after close", p, 2, 0, 0) | |
126 | +} | |
127 | + | |
128 | +func TestPoolMaxIdle(t *testing.T) { | |
129 | + d := poolDialer{t: t} | |
130 | + p := &redis.Pool{ | |
131 | + MaxIdle: 2, | |
132 | + Dial: d.dial, | |
133 | + } | |
134 | + defer p.Close() | |
135 | + | |
136 | + for i := 0; i < 10; i++ { | |
137 | + c1 := p.Get() | |
138 | + c1.Do("PING") | |
139 | + c2 := p.Get() | |
140 | + c2.Do("PING") | |
141 | + c3 := p.Get() | |
142 | + c3.Do("PING") | |
143 | + c1.Close() | |
144 | + c2.Close() | |
145 | + c3.Close() | |
146 | + } | |
147 | + d.check("before close", p, 12, 2, 0) | |
148 | + p.Close() | |
149 | + d.check("after close", p, 12, 0, 0) | |
150 | +} | |
151 | + | |
152 | +func TestPoolError(t *testing.T) { | |
153 | + d := poolDialer{t: t} | |
154 | + p := &redis.Pool{ | |
155 | + MaxIdle: 2, | |
156 | + Dial: d.dial, | |
157 | + } | |
158 | + defer p.Close() | |
159 | + | |
160 | + c := p.Get() | |
161 | + c.Do("ERR", io.EOF) | |
162 | + if c.Err() == nil { | |
163 | + t.Errorf("expected c.Err() != nil") | |
164 | + } | |
165 | + c.Close() | |
166 | + | |
167 | + c = p.Get() | |
168 | + c.Do("ERR", io.EOF) | |
169 | + c.Close() | |
170 | + | |
171 | + d.check(".", p, 2, 0, 0) | |
172 | +} | |
173 | + | |
174 | +func TestPoolClose(t *testing.T) { | |
175 | + d := poolDialer{t: t} | |
176 | + p := &redis.Pool{ | |
177 | + MaxIdle: 2, | |
178 | + Dial: d.dial, | |
179 | + } | |
180 | + defer p.Close() | |
181 | + | |
182 | + c1 := p.Get() | |
183 | + c1.Do("PING") | |
184 | + c2 := p.Get() | |
185 | + c2.Do("PING") | |
186 | + c3 := p.Get() | |
187 | + c3.Do("PING") | |
188 | + | |
189 | + c1.Close() | |
190 | + if _, err := c1.Do("PING"); err == nil { | |
191 | + t.Errorf("expected error after connection closed") | |
192 | + } | |
193 | + | |
194 | + c2.Close() | |
195 | + c2.Close() | |
196 | + | |
197 | + p.Close() | |
198 | + | |
199 | + d.check("after pool close", p, 3, 1, 1) | |
200 | + | |
201 | + if _, err := c1.Do("PING"); err == nil { | |
202 | + t.Errorf("expected error after connection and pool closed") | |
203 | + } | |
204 | + | |
205 | + c3.Close() | |
206 | + | |
207 | + d.check("after conn close", p, 3, 0, 0) | |
208 | + | |
209 | + c1 = p.Get() | |
210 | + if _, err := c1.Do("PING"); err == nil { | |
211 | + t.Errorf("expected error after pool closed") | |
212 | + } | |
213 | +} | |
214 | + | |
215 | +func TestPoolTimeout(t *testing.T) { | |
216 | + d := poolDialer{t: t} | |
217 | + p := &redis.Pool{ | |
218 | + MaxIdle: 2, | |
219 | + IdleTimeout: 300 * time.Second, | |
220 | + Dial: d.dial, | |
221 | + } | |
222 | + defer p.Close() | |
223 | + | |
224 | + now := time.Now() | |
225 | + redis.SetNowFunc(func() time.Time { return now }) | |
226 | + defer redis.SetNowFunc(time.Now) | |
227 | + | |
228 | + c := p.Get() | |
229 | + c.Do("PING") | |
230 | + c.Close() | |
231 | + | |
232 | + d.check("1", p, 1, 1, 0) | |
233 | + | |
234 | + now = now.Add(p.IdleTimeout) | |
235 | + | |
236 | + c = p.Get() | |
237 | + c.Do("PING") | |
238 | + c.Close() | |
239 | + | |
240 | + d.check("2", p, 2, 1, 0) | |
241 | +} | |
242 | + | |
243 | +func TestPoolConcurrenSendReceive(t *testing.T) { | |
244 | + p := &redis.Pool{ | |
245 | + Dial: redis.DialDefaultServer, | |
246 | + } | |
247 | + defer p.Close() | |
248 | + | |
249 | + c := p.Get() | |
250 | + done := make(chan error, 1) | |
251 | + go func() { | |
252 | + _, err := c.Receive() | |
253 | + done <- err | |
254 | + }() | |
255 | + c.Send("PING") | |
256 | + c.Flush() | |
257 | + err := <-done | |
258 | + if err != nil { | |
259 | + t.Fatalf("Receive() returned error %v", err) | |
260 | + } | |
261 | + _, err = c.Do("") | |
262 | + if err != nil { | |
263 | + t.Fatalf("Do() returned error %v", err) | |
264 | + } | |
265 | + c.Close() | |
266 | +} | |
267 | + | |
268 | +func TestPoolBorrowCheck(t *testing.T) { | |
269 | + d := poolDialer{t: t} | |
270 | + p := &redis.Pool{ | |
271 | + MaxIdle: 2, | |
272 | + Dial: d.dial, | |
273 | + TestOnBorrow: func(redis.Conn, time.Time) error { return redis.Error("BLAH") }, | |
274 | + } | |
275 | + defer p.Close() | |
276 | + | |
277 | + for i := 0; i < 10; i++ { | |
278 | + c := p.Get() | |
279 | + c.Do("PING") | |
280 | + c.Close() | |
281 | + } | |
282 | + d.check("1", p, 10, 1, 0) | |
283 | +} | |
284 | + | |
285 | +func TestPoolMaxActive(t *testing.T) { | |
286 | + d := poolDialer{t: t} | |
287 | + p := &redis.Pool{ | |
288 | + MaxIdle: 2, | |
289 | + MaxActive: 2, | |
290 | + Dial: d.dial, | |
291 | + } | |
292 | + defer p.Close() | |
293 | + | |
294 | + c1 := p.Get() | |
295 | + c1.Do("PING") | |
296 | + c2 := p.Get() | |
297 | + c2.Do("PING") | |
298 | + | |
299 | + d.check("1", p, 2, 2, 2) | |
300 | + | |
301 | + c3 := p.Get() | |
302 | + if _, err := c3.Do("PING"); err != redis.ErrPoolExhausted { | |
303 | + t.Errorf("expected pool exhausted") | |
304 | + } | |
305 | + | |
306 | + c3.Close() | |
307 | + d.check("2", p, 2, 2, 2) | |
308 | + c2.Close() | |
309 | + d.check("3", p, 2, 2, 1) | |
310 | + | |
311 | + c3 = p.Get() | |
312 | + if _, err := c3.Do("PING"); err != nil { | |
313 | + t.Errorf("expected good channel, err=%v", err) | |
314 | + } | |
315 | + c3.Close() | |
316 | + | |
317 | + d.check("4", p, 2, 2, 1) | |
318 | +} | |
319 | + | |
320 | +func TestPoolMonitorCleanup(t *testing.T) { | |
321 | + d := poolDialer{t: t} | |
322 | + p := &redis.Pool{ | |
323 | + MaxIdle: 2, | |
324 | + MaxActive: 2, | |
325 | + Dial: d.dial, | |
326 | + } | |
327 | + defer p.Close() | |
328 | + | |
329 | + c := p.Get() | |
330 | + c.Send("MONITOR") | |
331 | + c.Close() | |
332 | + | |
333 | + d.check("", p, 1, 0, 0) | |
334 | +} | |
335 | + | |
336 | +func TestPoolPubSubCleanup(t *testing.T) { | |
337 | + d := poolDialer{t: t} | |
338 | + p := &redis.Pool{ | |
339 | + MaxIdle: 2, | |
340 | + MaxActive: 2, | |
341 | + Dial: d.dial, | |
342 | + } | |
343 | + defer p.Close() | |
344 | + | |
345 | + c := p.Get() | |
346 | + c.Send("SUBSCRIBE", "x") | |
347 | + c.Close() | |
348 | + | |
349 | + want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} | |
350 | + if !reflect.DeepEqual(d.commands, want) { | |
351 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
352 | + } | |
353 | + d.commands = nil | |
354 | + | |
355 | + c = p.Get() | |
356 | + c.Send("PSUBSCRIBE", "x*") | |
357 | + c.Close() | |
358 | + | |
359 | + want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} | |
360 | + if !reflect.DeepEqual(d.commands, want) { | |
361 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
362 | + } | |
363 | + d.commands = nil | |
364 | +} | |
365 | + | |
366 | +func TestPoolTransactionCleanup(t *testing.T) { | |
367 | + d := poolDialer{t: t} | |
368 | + p := &redis.Pool{ | |
369 | + MaxIdle: 2, | |
370 | + MaxActive: 2, | |
371 | + Dial: d.dial, | |
372 | + } | |
373 | + defer p.Close() | |
374 | + | |
375 | + c := p.Get() | |
376 | + c.Do("WATCH", "key") | |
377 | + c.Do("PING") | |
378 | + c.Close() | |
379 | + | |
380 | + want := []string{"WATCH", "PING", "UNWATCH"} | |
381 | + if !reflect.DeepEqual(d.commands, want) { | |
382 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
383 | + } | |
384 | + d.commands = nil | |
385 | + | |
386 | + c = p.Get() | |
387 | + c.Do("WATCH", "key") | |
388 | + c.Do("UNWATCH") | |
389 | + c.Do("PING") | |
390 | + c.Close() | |
391 | + | |
392 | + want = []string{"WATCH", "UNWATCH", "PING"} | |
393 | + if !reflect.DeepEqual(d.commands, want) { | |
394 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
395 | + } | |
396 | + d.commands = nil | |
397 | + | |
398 | + c = p.Get() | |
399 | + c.Do("WATCH", "key") | |
400 | + c.Do("MULTI") | |
401 | + c.Do("PING") | |
402 | + c.Close() | |
403 | + | |
404 | + want = []string{"WATCH", "MULTI", "PING", "DISCARD"} | |
405 | + if !reflect.DeepEqual(d.commands, want) { | |
406 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
407 | + } | |
408 | + d.commands = nil | |
409 | + | |
410 | + c = p.Get() | |
411 | + c.Do("WATCH", "key") | |
412 | + c.Do("MULTI") | |
413 | + c.Do("DISCARD") | |
414 | + c.Do("PING") | |
415 | + c.Close() | |
416 | + | |
417 | + want = []string{"WATCH", "MULTI", "DISCARD", "PING"} | |
418 | + if !reflect.DeepEqual(d.commands, want) { | |
419 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
420 | + } | |
421 | + d.commands = nil | |
422 | + | |
423 | + c = p.Get() | |
424 | + c.Do("WATCH", "key") | |
425 | + c.Do("MULTI") | |
426 | + c.Do("EXEC") | |
427 | + c.Do("PING") | |
428 | + c.Close() | |
429 | + | |
430 | + want = []string{"WATCH", "MULTI", "EXEC", "PING"} | |
431 | + if !reflect.DeepEqual(d.commands, want) { | |
432 | + t.Errorf("got commands %v, want %v", d.commands, want) | |
433 | + } | |
434 | + d.commands = nil | |
435 | +} | |
436 | + | |
437 | +func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error { | |
438 | + errs := make(chan error, 10) | |
439 | + for i := 0; i < cap(errs); i++ { | |
440 | + go func() { | |
441 | + c := p.Get() | |
442 | + _, err := c.Do(cmd, args...) | |
443 | + c.Close() | |
444 | + errs <- err | |
445 | + }() | |
446 | + } | |
447 | + | |
448 | + // Wait for goroutines to block. | |
449 | + time.Sleep(time.Second / 4) | |
450 | + | |
451 | + return errs | |
452 | +} | |
453 | + | |
454 | +func TestWaitPool(t *testing.T) { | |
455 | + d := poolDialer{t: t} | |
456 | + p := &redis.Pool{ | |
457 | + MaxIdle: 1, | |
458 | + MaxActive: 1, | |
459 | + Dial: d.dial, | |
460 | + Wait: true, | |
461 | + } | |
462 | + defer p.Close() | |
463 | + | |
464 | + c := p.Get() | |
465 | + errs := startGoroutines(p, "PING") | |
466 | + d.check("before close", p, 1, 1, 1) | |
467 | + c.Close() | |
468 | + timeout := time.After(2 * time.Second) | |
469 | + for i := 0; i < cap(errs); i++ { | |
470 | + select { | |
471 | + case err := <-errs: | |
472 | + if err != nil { | |
473 | + t.Fatal(err) | |
474 | + } | |
475 | + case <-timeout: | |
476 | + t.Fatalf("timeout waiting for blocked goroutine %d", i) | |
477 | + } | |
478 | + } | |
479 | + d.check("done", p, 1, 1, 0) | |
480 | +} | |
481 | + | |
482 | +func TestWaitPoolClose(t *testing.T) { | |
483 | + d := poolDialer{t: t} | |
484 | + p := &redis.Pool{ | |
485 | + MaxIdle: 1, | |
486 | + MaxActive: 1, | |
487 | + Dial: d.dial, | |
488 | + Wait: true, | |
489 | + } | |
490 | + defer p.Close() | |
491 | + | |
492 | + c := p.Get() | |
493 | + if _, err := c.Do("PING"); err != nil { | |
494 | + t.Fatal(err) | |
495 | + } | |
496 | + errs := startGoroutines(p, "PING") | |
497 | + d.check("before close", p, 1, 1, 1) | |
498 | + p.Close() | |
499 | + timeout := time.After(2 * time.Second) | |
500 | + for i := 0; i < cap(errs); i++ { | |
501 | + select { | |
502 | + case err := <-errs: | |
503 | + switch err { | |
504 | + case nil: | |
505 | + t.Fatal("blocked goroutine did not get error") | |
506 | + case redis.ErrPoolExhausted: | |
507 | + t.Fatal("blocked goroutine got pool exhausted error") | |
508 | + } | |
509 | + case <-timeout: | |
510 | + t.Fatal("timeout waiting for blocked goroutine") | |
511 | + } | |
512 | + } | |
513 | + c.Close() | |
514 | + d.check("done", p, 1, 0, 0) | |
515 | +} | |
516 | + | |
517 | +func TestWaitPoolCommandError(t *testing.T) { | |
518 | + testErr := errors.New("test") | |
519 | + d := poolDialer{t: t} | |
520 | + p := &redis.Pool{ | |
521 | + MaxIdle: 1, | |
522 | + MaxActive: 1, | |
523 | + Dial: d.dial, | |
524 | + Wait: true, | |
525 | + } | |
526 | + defer p.Close() | |
527 | + | |
528 | + c := p.Get() | |
529 | + errs := startGoroutines(p, "ERR", testErr) | |
530 | + d.check("before close", p, 1, 1, 1) | |
531 | + c.Close() | |
532 | + timeout := time.After(2 * time.Second) | |
533 | + for i := 0; i < cap(errs); i++ { | |
534 | + select { | |
535 | + case err := <-errs: | |
536 | + if err != nil { | |
537 | + t.Fatal(err) | |
538 | + } | |
539 | + case <-timeout: | |
540 | + t.Fatalf("timeout waiting for blocked goroutine %d", i) | |
541 | + } | |
542 | + } | |
543 | + d.check("done", p, cap(errs), 0, 0) | |
544 | +} | |
545 | + | |
546 | +func TestWaitPoolDialError(t *testing.T) { | |
547 | + testErr := errors.New("test") | |
548 | + d := poolDialer{t: t} | |
549 | + p := &redis.Pool{ | |
550 | + MaxIdle: 1, | |
551 | + MaxActive: 1, | |
552 | + Dial: d.dial, | |
553 | + Wait: true, | |
554 | + } | |
555 | + defer p.Close() | |
556 | + | |
557 | + c := p.Get() | |
558 | + errs := startGoroutines(p, "ERR", testErr) | |
559 | + d.check("before close", p, 1, 1, 1) | |
560 | + | |
561 | + d.dialErr = errors.New("dial") | |
562 | + c.Close() | |
563 | + | |
564 | + nilCount := 0 | |
565 | + errCount := 0 | |
566 | + timeout := time.After(2 * time.Second) | |
567 | + for i := 0; i < cap(errs); i++ { | |
568 | + select { | |
569 | + case err := <-errs: | |
570 | + switch err { | |
571 | + case nil: | |
572 | + nilCount++ | |
573 | + case d.dialErr: | |
574 | + errCount++ | |
575 | + default: | |
576 | + t.Fatalf("expected dial error or nil, got %v", err) | |
577 | + } | |
578 | + case <-timeout: | |
579 | + t.Fatalf("timeout waiting for blocked goroutine %d", i) | |
580 | + } | |
581 | + } | |
582 | + if nilCount != 1 { | |
583 | + t.Errorf("expected one nil error, got %d", nilCount) | |
584 | + } | |
585 | + if errCount != cap(errs)-1 { | |
586 | + t.Errorf("expected %d dial errors, got %d", cap(errs)-1, errCount) | |
587 | + } | |
588 | + d.check("done", p, cap(errs), 0, 0) | |
589 | +} | |
590 | + | |
591 | +// Borrowing requires us to iterate over the idle connections, unlock the pool, | |
592 | +// and perform a blocking operation to check the connection still works. If | |
593 | +// TestOnBorrow fails, we must reacquire the lock and continue iteration. This | |
594 | +// test ensures that iteration will work correctly if multiple threads are | |
595 | +// iterating simultaneously. | |
596 | +func TestLocking_TestOnBorrowFails_PoolDoesntCrash(t *testing.T) { | |
597 | + const count = 100 | |
598 | + | |
599 | + // First we'll Create a pool where the pilfering of idle connections fails. | |
600 | + d := poolDialer{t: t} | |
601 | + p := &redis.Pool{ | |
602 | + MaxIdle: count, | |
603 | + MaxActive: count, | |
604 | + Dial: d.dial, | |
605 | + TestOnBorrow: func(c redis.Conn, t time.Time) error { | |
606 | + return errors.New("No way back into the real world.") | |
607 | + }, | |
608 | + } | |
609 | + defer p.Close() | |
610 | + | |
611 | + // Fill the pool with idle connections. | |
612 | + conns := make([]redis.Conn, count) | |
613 | + for i := range conns { | |
614 | + conns[i] = p.Get() | |
615 | + } | |
616 | + for i := range conns { | |
617 | + conns[i].Close() | |
618 | + } | |
619 | + | |
620 | + // Spawn a bunch of goroutines to thrash the pool. | |
621 | + var wg sync.WaitGroup | |
622 | + wg.Add(count) | |
623 | + for i := 0; i < count; i++ { | |
624 | + go func() { | |
625 | + c := p.Get() | |
626 | + if c.Err() != nil { | |
627 | + t.Errorf("pool get failed: %v", c.Err()) | |
628 | + } | |
629 | + c.Close() | |
630 | + wg.Done() | |
631 | + }() | |
632 | + } | |
633 | + wg.Wait() | |
634 | + if d.dialed != count*2 { | |
635 | + t.Errorf("Expected %d dials, got %d", count*2, d.dialed) | |
636 | + } | |
637 | +} | |
638 | + | |
639 | +func BenchmarkPoolGet(b *testing.B) { | |
640 | + b.StopTimer() | |
641 | + p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2} | |
642 | + c := p.Get() | |
643 | + if err := c.Err(); err != nil { | |
644 | + b.Fatal(err) | |
645 | + } | |
646 | + c.Close() | |
647 | + defer p.Close() | |
648 | + b.StartTimer() | |
649 | + for i := 0; i < b.N; i++ { | |
650 | + c = p.Get() | |
651 | + c.Close() | |
652 | + } | |
653 | +} | |
654 | + | |
655 | +func BenchmarkPoolGetErr(b *testing.B) { | |
656 | + b.StopTimer() | |
657 | + p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2} | |
658 | + c := p.Get() | |
659 | + if err := c.Err(); err != nil { | |
660 | + b.Fatal(err) | |
661 | + } | |
662 | + c.Close() | |
663 | + defer p.Close() | |
664 | + b.StartTimer() | |
665 | + for i := 0; i < b.N; i++ { | |
666 | + c = p.Get() | |
667 | + if err := c.Err(); err != nil { | |
668 | + b.Fatal(err) | |
669 | + } | |
670 | + c.Close() | |
671 | + } | |
672 | +} | |
673 | + | |
674 | +func BenchmarkPoolGetPing(b *testing.B) { | |
675 | + b.StopTimer() | |
676 | + p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2} | |
677 | + c := p.Get() | |
678 | + if err := c.Err(); err != nil { | |
679 | + b.Fatal(err) | |
680 | + } | |
681 | + c.Close() | |
682 | + defer p.Close() | |
683 | + b.StartTimer() | |
684 | + for i := 0; i < b.N; i++ { | |
685 | + c = p.Get() | |
686 | + if _, err := c.Do("PING"); err != nil { | |
687 | + b.Fatal(err) | |
688 | + } | |
689 | + c.Close() | |
690 | + } | |
691 | +} | ... | ... |
... | ... | @@ -0,0 +1,31 @@ |
1 | +// +build !go1.7 | |
2 | + | |
3 | +package redis | |
4 | + | |
5 | +import "crypto/tls" | |
6 | + | |
7 | +// similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case | |
8 | +func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config { | |
9 | + if cfg == nil { | |
10 | + return &tls.Config{InsecureSkipVerify: skipVerify} | |
11 | + } | |
12 | + return &tls.Config{ | |
13 | + Rand: cfg.Rand, | |
14 | + Time: cfg.Time, | |
15 | + Certificates: cfg.Certificates, | |
16 | + NameToCertificate: cfg.NameToCertificate, | |
17 | + GetCertificate: cfg.GetCertificate, | |
18 | + RootCAs: cfg.RootCAs, | |
19 | + NextProtos: cfg.NextProtos, | |
20 | + ServerName: cfg.ServerName, | |
21 | + ClientAuth: cfg.ClientAuth, | |
22 | + ClientCAs: cfg.ClientCAs, | |
23 | + InsecureSkipVerify: cfg.InsecureSkipVerify, | |
24 | + CipherSuites: cfg.CipherSuites, | |
25 | + PreferServerCipherSuites: cfg.PreferServerCipherSuites, | |
26 | + ClientSessionCache: cfg.ClientSessionCache, | |
27 | + MinVersion: cfg.MinVersion, | |
28 | + MaxVersion: cfg.MaxVersion, | |
29 | + CurvePreferences: cfg.CurvePreferences, | |
30 | + } | |
31 | +} | ... | ... |
... | ... | @@ -0,0 +1,144 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import "errors" | |
18 | + | |
19 | +// Subscription represents a subscribe or unsubscribe notification. | |
20 | +type Subscription struct { | |
21 | + // Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" | |
22 | + Kind string | |
23 | + | |
24 | + // The channel that was changed. | |
25 | + Channel string | |
26 | + | |
27 | + // The current number of subscriptions for connection. | |
28 | + Count int | |
29 | +} | |
30 | + | |
31 | +// Message represents a message notification. | |
32 | +type Message struct { | |
33 | + // The originating channel. | |
34 | + Channel string | |
35 | + | |
36 | + // The message data. | |
37 | + Data []byte | |
38 | +} | |
39 | + | |
40 | +// PMessage represents a pmessage notification. | |
41 | +type PMessage struct { | |
42 | + // The matched pattern. | |
43 | + Pattern string | |
44 | + | |
45 | + // The originating channel. | |
46 | + Channel string | |
47 | + | |
48 | + // The message data. | |
49 | + Data []byte | |
50 | +} | |
51 | + | |
52 | +// Pong represents a pubsub pong notification. | |
53 | +type Pong struct { | |
54 | + Data string | |
55 | +} | |
56 | + | |
57 | +// PubSubConn wraps a Conn with convenience methods for subscribers. | |
58 | +type PubSubConn struct { | |
59 | + Conn Conn | |
60 | +} | |
61 | + | |
62 | +// Close closes the connection. | |
63 | +func (c PubSubConn) Close() error { | |
64 | + return c.Conn.Close() | |
65 | +} | |
66 | + | |
67 | +// Subscribe subscribes the connection to the specified channels. | |
68 | +func (c PubSubConn) Subscribe(channel ...interface{}) error { | |
69 | + c.Conn.Send("SUBSCRIBE", channel...) | |
70 | + return c.Conn.Flush() | |
71 | +} | |
72 | + | |
73 | +// PSubscribe subscribes the connection to the given patterns. | |
74 | +func (c PubSubConn) PSubscribe(channel ...interface{}) error { | |
75 | + c.Conn.Send("PSUBSCRIBE", channel...) | |
76 | + return c.Conn.Flush() | |
77 | +} | |
78 | + | |
79 | +// Unsubscribe unsubscribes the connection from the given channels, or from all | |
80 | +// of them if none is given. | |
81 | +func (c PubSubConn) Unsubscribe(channel ...interface{}) error { | |
82 | + c.Conn.Send("UNSUBSCRIBE", channel...) | |
83 | + return c.Conn.Flush() | |
84 | +} | |
85 | + | |
86 | +// PUnsubscribe unsubscribes the connection from the given patterns, or from all | |
87 | +// of them if none is given. | |
88 | +func (c PubSubConn) PUnsubscribe(channel ...interface{}) error { | |
89 | + c.Conn.Send("PUNSUBSCRIBE", channel...) | |
90 | + return c.Conn.Flush() | |
91 | +} | |
92 | + | |
93 | +// Ping sends a PING to the server with the specified data. | |
94 | +// | |
95 | +// The connection must be subscribed to at least one channel or pattern when | |
96 | +// calling this method. | |
97 | +func (c PubSubConn) Ping(data string) error { | |
98 | + c.Conn.Send("PING", data) | |
99 | + return c.Conn.Flush() | |
100 | +} | |
101 | + | |
102 | +// Receive returns a pushed message as a Subscription, Message, PMessage, Pong | |
103 | +// or error. The return value is intended to be used directly in a type switch | |
104 | +// as illustrated in the PubSubConn example. | |
105 | +func (c PubSubConn) Receive() interface{} { | |
106 | + reply, err := Values(c.Conn.Receive()) | |
107 | + if err != nil { | |
108 | + return err | |
109 | + } | |
110 | + | |
111 | + var kind string | |
112 | + reply, err = Scan(reply, &kind) | |
113 | + if err != nil { | |
114 | + return err | |
115 | + } | |
116 | + | |
117 | + switch kind { | |
118 | + case "message": | |
119 | + var m Message | |
120 | + if _, err := Scan(reply, &m.Channel, &m.Data); err != nil { | |
121 | + return err | |
122 | + } | |
123 | + return m | |
124 | + case "pmessage": | |
125 | + var pm PMessage | |
126 | + if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil { | |
127 | + return err | |
128 | + } | |
129 | + return pm | |
130 | + case "subscribe", "psubscribe", "unsubscribe", "punsubscribe": | |
131 | + s := Subscription{Kind: kind} | |
132 | + if _, err := Scan(reply, &s.Channel, &s.Count); err != nil { | |
133 | + return err | |
134 | + } | |
135 | + return s | |
136 | + case "pong": | |
137 | + var p Pong | |
138 | + if _, err := Scan(reply, &p.Data); err != nil { | |
139 | + return err | |
140 | + } | |
141 | + return p | |
142 | + } | |
143 | + return errors.New("redigo: unknown pubsub notification") | |
144 | +} | ... | ... |
src/github.com/garyburd/redigo/redis/pubsub_example_test.go
0 → 100644
... | ... | @@ -0,0 +1,165 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +// +build go1.7 | |
16 | + | |
17 | +package redis_test | |
18 | + | |
19 | +import ( | |
20 | + "context" | |
21 | + "fmt" | |
22 | + "time" | |
23 | + | |
24 | + "github.com/garyburd/redigo/redis" | |
25 | +) | |
26 | + | |
27 | +// listenPubSubChannels listens for messages on Redis pubsub channels. The | |
28 | +// onStart function is called after the channels are subscribed. The onMessage | |
29 | +// function is called for each message. | |
30 | +func listenPubSubChannels(ctx context.Context, redisServerAddr string, | |
31 | + onStart func() error, | |
32 | + onMessage func(channel string, data []byte) error, | |
33 | + channels ...string) error { | |
34 | + // A ping is set to the server with this period to test for the health of | |
35 | + // the connection and server. | |
36 | + const healthCheckPeriod = time.Minute | |
37 | + | |
38 | + c, err := redis.Dial("tcp", redisServerAddr, | |
39 | + // Read timeout on server should be greater than ping period. | |
40 | + redis.DialReadTimeout(healthCheckPeriod+10*time.Second), | |
41 | + redis.DialWriteTimeout(10*time.Second)) | |
42 | + if err != nil { | |
43 | + return err | |
44 | + } | |
45 | + defer c.Close() | |
46 | + | |
47 | + psc := redis.PubSubConn{Conn: c} | |
48 | + | |
49 | + if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil { | |
50 | + return err | |
51 | + } | |
52 | + | |
53 | + done := make(chan error, 1) | |
54 | + | |
55 | + // Start a goroutine to receive notifications from the server. | |
56 | + go func() { | |
57 | + for { | |
58 | + switch n := psc.Receive().(type) { | |
59 | + case error: | |
60 | + done <- n | |
61 | + return | |
62 | + case redis.Message: | |
63 | + if err := onMessage(n.Channel, n.Data); err != nil { | |
64 | + done <- err | |
65 | + return | |
66 | + } | |
67 | + case redis.Subscription: | |
68 | + switch n.Count { | |
69 | + case len(channels): | |
70 | + // Notify application when all channels are subscribed. | |
71 | + if err := onStart(); err != nil { | |
72 | + done <- err | |
73 | + return | |
74 | + } | |
75 | + case 0: | |
76 | + // Return from the goroutine when all channels are unsubscribed. | |
77 | + done <- nil | |
78 | + return | |
79 | + } | |
80 | + } | |
81 | + } | |
82 | + }() | |
83 | + | |
84 | + ticker := time.NewTicker(healthCheckPeriod) | |
85 | + defer ticker.Stop() | |
86 | +loop: | |
87 | + for err == nil { | |
88 | + select { | |
89 | + case <-ticker.C: | |
90 | + // Send ping to test health of connection and server. If | |
91 | + // corresponding pong is not received, then receive on the | |
92 | + // connection will timeout and the receive goroutine will exit. | |
93 | + if err = psc.Ping(""); err != nil { | |
94 | + break loop | |
95 | + } | |
96 | + case <-ctx.Done(): | |
97 | + break loop | |
98 | + case err := <-done: | |
99 | + // Return error from the receive goroutine. | |
100 | + return err | |
101 | + } | |
102 | + } | |
103 | + | |
104 | + // Signal the receiving goroutine to exit by unsubscribing from all channels. | |
105 | + psc.Unsubscribe() | |
106 | + | |
107 | + // Wait for goroutine to complete. | |
108 | + return <-done | |
109 | +} | |
110 | + | |
111 | +func publish() { | |
112 | + c, err := dial() | |
113 | + if err != nil { | |
114 | + fmt.Println(err) | |
115 | + return | |
116 | + } | |
117 | + defer c.Close() | |
118 | + | |
119 | + c.Do("PUBLISH", "c1", "hello") | |
120 | + c.Do("PUBLISH", "c2", "world") | |
121 | + c.Do("PUBLISH", "c1", "goodbye") | |
122 | +} | |
123 | + | |
124 | +// This example shows how receive pubsub notifications with cancelation and | |
125 | +// health checks. | |
126 | +func ExamplePubSubConn() { | |
127 | + redisServerAddr, err := serverAddr() | |
128 | + if err != nil { | |
129 | + fmt.Println(err) | |
130 | + return | |
131 | + } | |
132 | + | |
133 | + ctx, cancel := context.WithCancel(context.Background()) | |
134 | + | |
135 | + err = listenPubSubChannels(ctx, | |
136 | + redisServerAddr, | |
137 | + func() error { | |
138 | + // The start callback is a good place to backfill missed | |
139 | + // notifications. For the purpose of this example, a goroutine is | |
140 | + // started to send notifications. | |
141 | + go publish() | |
142 | + return nil | |
143 | + }, | |
144 | + func(channel string, message []byte) error { | |
145 | + fmt.Printf("channel: %s, message: %s\n", channel, message) | |
146 | + | |
147 | + // For the purpose of this example, cancel the listener's context | |
148 | + // after receiving last message sent by publish(). | |
149 | + if string(message) == "goodbye" { | |
150 | + cancel() | |
151 | + } | |
152 | + return nil | |
153 | + }, | |
154 | + "c1", "c2") | |
155 | + | |
156 | + if err != nil { | |
157 | + fmt.Println(err) | |
158 | + return | |
159 | + } | |
160 | + | |
161 | + // Output: | |
162 | + // channel: c1, message: hello | |
163 | + // channel: c2, message: world | |
164 | + // channel: c1, message: goodbye | |
165 | +} | ... | ... |
... | ... | @@ -0,0 +1,67 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "reflect" | |
19 | + "testing" | |
20 | + | |
21 | + "github.com/garyburd/redigo/redis" | |
22 | +) | |
23 | + | |
24 | +func expectPushed(t *testing.T, c redis.PubSubConn, message string, expected interface{}) { | |
25 | + actual := c.Receive() | |
26 | + if !reflect.DeepEqual(actual, expected) { | |
27 | + t.Errorf("%s = %v, want %v", message, actual, expected) | |
28 | + } | |
29 | +} | |
30 | + | |
31 | +func TestPushed(t *testing.T) { | |
32 | + pc, err := redis.DialDefaultServer() | |
33 | + if err != nil { | |
34 | + t.Fatalf("error connection to database, %v", err) | |
35 | + } | |
36 | + defer pc.Close() | |
37 | + | |
38 | + sc, err := redis.DialDefaultServer() | |
39 | + if err != nil { | |
40 | + t.Fatalf("error connection to database, %v", err) | |
41 | + } | |
42 | + defer sc.Close() | |
43 | + | |
44 | + c := redis.PubSubConn{Conn: sc} | |
45 | + | |
46 | + c.Subscribe("c1") | |
47 | + expectPushed(t, c, "Subscribe(c1)", redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1}) | |
48 | + c.Subscribe("c2") | |
49 | + expectPushed(t, c, "Subscribe(c2)", redis.Subscription{Kind: "subscribe", Channel: "c2", Count: 2}) | |
50 | + c.PSubscribe("p1") | |
51 | + expectPushed(t, c, "PSubscribe(p1)", redis.Subscription{Kind: "psubscribe", Channel: "p1", Count: 3}) | |
52 | + c.PSubscribe("p2") | |
53 | + expectPushed(t, c, "PSubscribe(p2)", redis.Subscription{Kind: "psubscribe", Channel: "p2", Count: 4}) | |
54 | + c.PUnsubscribe() | |
55 | + expectPushed(t, c, "Punsubscribe(p1)", redis.Subscription{Kind: "punsubscribe", Channel: "p1", Count: 3}) | |
56 | + expectPushed(t, c, "Punsubscribe()", redis.Subscription{Kind: "punsubscribe", Channel: "p2", Count: 2}) | |
57 | + | |
58 | + pc.Do("PUBLISH", "c1", "hello") | |
59 | + expectPushed(t, c, "PUBLISH c1 hello", redis.Message{Channel: "c1", Data: []byte("hello")}) | |
60 | + | |
61 | + c.Ping("hello") | |
62 | + expectPushed(t, c, `Ping("hello")`, redis.Pong{Data: "hello"}) | |
63 | + | |
64 | + c.Conn.Send("PING") | |
65 | + c.Conn.Flush() | |
66 | + expectPushed(t, c, `Send("PING")`, redis.Pong{}) | |
67 | +} | ... | ... |
... | ... | @@ -0,0 +1,61 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +// Error represents an error returned in a command reply. | |
18 | +type Error string | |
19 | + | |
20 | +func (err Error) Error() string { return string(err) } | |
21 | + | |
22 | +// Conn represents a connection to a Redis server. | |
23 | +type Conn interface { | |
24 | + // Close closes the connection. | |
25 | + Close() error | |
26 | + | |
27 | + // Err returns a non-nil value when the connection is not usable. | |
28 | + Err() error | |
29 | + | |
30 | + // Do sends a command to the server and returns the received reply. | |
31 | + Do(commandName string, args ...interface{}) (reply interface{}, err error) | |
32 | + | |
33 | + // Send writes the command to the client's output buffer. | |
34 | + Send(commandName string, args ...interface{}) error | |
35 | + | |
36 | + // Flush flushes the output buffer to the Redis server. | |
37 | + Flush() error | |
38 | + | |
39 | + // Receive receives a single reply from the Redis server | |
40 | + Receive() (reply interface{}, err error) | |
41 | +} | |
42 | + | |
43 | +// Argument is the interface implemented by an object which wants to control how | |
44 | +// the object is converted to Redis bulk strings. | |
45 | +type Argument interface { | |
46 | + // RedisArg returns a value to be encoded as a bulk string per the | |
47 | + // conversions listed in the section 'Executing Commands'. | |
48 | + // Implementations should typically return a []byte or string. | |
49 | + RedisArg() interface{} | |
50 | +} | |
51 | + | |
52 | +// Scanner is implemented by an object which wants to control its value is | |
53 | +// interpreted when read from Redis. | |
54 | +type Scanner interface { | |
55 | + // RedisScan assigns a value from a Redis value. The argument src is one of | |
56 | + // the reply types listed in the section `Executing Commands`. | |
57 | + // | |
58 | + // An error should be returned if the value cannot be stored without | |
59 | + // loss of information. | |
60 | + RedisScan(src interface{}) error | |
61 | +} | ... | ... |
... | ... | @@ -0,0 +1,479 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "errors" | |
19 | + "fmt" | |
20 | + "strconv" | |
21 | +) | |
22 | + | |
23 | +// ErrNil indicates that a reply value is nil. | |
24 | +var ErrNil = errors.New("redigo: nil returned") | |
25 | + | |
26 | +// Int is a helper that converts a command reply to an integer. If err is not | |
27 | +// equal to nil, then Int returns 0, err. Otherwise, Int converts the | |
28 | +// reply to an int as follows: | |
29 | +// | |
30 | +// Reply type Result | |
31 | +// integer int(reply), nil | |
32 | +// bulk string parsed reply, nil | |
33 | +// nil 0, ErrNil | |
34 | +// other 0, error | |
35 | +func Int(reply interface{}, err error) (int, error) { | |
36 | + if err != nil { | |
37 | + return 0, err | |
38 | + } | |
39 | + switch reply := reply.(type) { | |
40 | + case int64: | |
41 | + x := int(reply) | |
42 | + if int64(x) != reply { | |
43 | + return 0, strconv.ErrRange | |
44 | + } | |
45 | + return x, nil | |
46 | + case []byte: | |
47 | + n, err := strconv.ParseInt(string(reply), 10, 0) | |
48 | + return int(n), err | |
49 | + case nil: | |
50 | + return 0, ErrNil | |
51 | + case Error: | |
52 | + return 0, reply | |
53 | + } | |
54 | + return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply) | |
55 | +} | |
56 | + | |
57 | +// Int64 is a helper that converts a command reply to 64 bit integer. If err is | |
58 | +// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the | |
59 | +// reply to an int64 as follows: | |
60 | +// | |
61 | +// Reply type Result | |
62 | +// integer reply, nil | |
63 | +// bulk string parsed reply, nil | |
64 | +// nil 0, ErrNil | |
65 | +// other 0, error | |
66 | +func Int64(reply interface{}, err error) (int64, error) { | |
67 | + if err != nil { | |
68 | + return 0, err | |
69 | + } | |
70 | + switch reply := reply.(type) { | |
71 | + case int64: | |
72 | + return reply, nil | |
73 | + case []byte: | |
74 | + n, err := strconv.ParseInt(string(reply), 10, 64) | |
75 | + return n, err | |
76 | + case nil: | |
77 | + return 0, ErrNil | |
78 | + case Error: | |
79 | + return 0, reply | |
80 | + } | |
81 | + return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply) | |
82 | +} | |
83 | + | |
84 | +var errNegativeInt = errors.New("redigo: unexpected value for Uint64") | |
85 | + | |
86 | +// Uint64 is a helper that converts a command reply to 64 bit integer. If err is | |
87 | +// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the | |
88 | +// reply to an int64 as follows: | |
89 | +// | |
90 | +// Reply type Result | |
91 | +// integer reply, nil | |
92 | +// bulk string parsed reply, nil | |
93 | +// nil 0, ErrNil | |
94 | +// other 0, error | |
95 | +func Uint64(reply interface{}, err error) (uint64, error) { | |
96 | + if err != nil { | |
97 | + return 0, err | |
98 | + } | |
99 | + switch reply := reply.(type) { | |
100 | + case int64: | |
101 | + if reply < 0 { | |
102 | + return 0, errNegativeInt | |
103 | + } | |
104 | + return uint64(reply), nil | |
105 | + case []byte: | |
106 | + n, err := strconv.ParseUint(string(reply), 10, 64) | |
107 | + return n, err | |
108 | + case nil: | |
109 | + return 0, ErrNil | |
110 | + case Error: | |
111 | + return 0, reply | |
112 | + } | |
113 | + return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply) | |
114 | +} | |
115 | + | |
116 | +// Float64 is a helper that converts a command reply to 64 bit float. If err is | |
117 | +// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts | |
118 | +// the reply to an int as follows: | |
119 | +// | |
120 | +// Reply type Result | |
121 | +// bulk string parsed reply, nil | |
122 | +// nil 0, ErrNil | |
123 | +// other 0, error | |
124 | +func Float64(reply interface{}, err error) (float64, error) { | |
125 | + if err != nil { | |
126 | + return 0, err | |
127 | + } | |
128 | + switch reply := reply.(type) { | |
129 | + case []byte: | |
130 | + n, err := strconv.ParseFloat(string(reply), 64) | |
131 | + return n, err | |
132 | + case nil: | |
133 | + return 0, ErrNil | |
134 | + case Error: | |
135 | + return 0, reply | |
136 | + } | |
137 | + return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply) | |
138 | +} | |
139 | + | |
140 | +// String is a helper that converts a command reply to a string. If err is not | |
141 | +// equal to nil, then String returns "", err. Otherwise String converts the | |
142 | +// reply to a string as follows: | |
143 | +// | |
144 | +// Reply type Result | |
145 | +// bulk string string(reply), nil | |
146 | +// simple string reply, nil | |
147 | +// nil "", ErrNil | |
148 | +// other "", error | |
149 | +func String(reply interface{}, err error) (string, error) { | |
150 | + if err != nil { | |
151 | + return "", err | |
152 | + } | |
153 | + switch reply := reply.(type) { | |
154 | + case []byte: | |
155 | + return string(reply), nil | |
156 | + case string: | |
157 | + return reply, nil | |
158 | + case nil: | |
159 | + return "", ErrNil | |
160 | + case Error: | |
161 | + return "", reply | |
162 | + } | |
163 | + return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply) | |
164 | +} | |
165 | + | |
166 | +// Bytes is a helper that converts a command reply to a slice of bytes. If err | |
167 | +// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts | |
168 | +// the reply to a slice of bytes as follows: | |
169 | +// | |
170 | +// Reply type Result | |
171 | +// bulk string reply, nil | |
172 | +// simple string []byte(reply), nil | |
173 | +// nil nil, ErrNil | |
174 | +// other nil, error | |
175 | +func Bytes(reply interface{}, err error) ([]byte, error) { | |
176 | + if err != nil { | |
177 | + return nil, err | |
178 | + } | |
179 | + switch reply := reply.(type) { | |
180 | + case []byte: | |
181 | + return reply, nil | |
182 | + case string: | |
183 | + return []byte(reply), nil | |
184 | + case nil: | |
185 | + return nil, ErrNil | |
186 | + case Error: | |
187 | + return nil, reply | |
188 | + } | |
189 | + return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply) | |
190 | +} | |
191 | + | |
192 | +// Bool is a helper that converts a command reply to a boolean. If err is not | |
193 | +// equal to nil, then Bool returns false, err. Otherwise Bool converts the | |
194 | +// reply to boolean as follows: | |
195 | +// | |
196 | +// Reply type Result | |
197 | +// integer value != 0, nil | |
198 | +// bulk string strconv.ParseBool(reply) | |
199 | +// nil false, ErrNil | |
200 | +// other false, error | |
201 | +func Bool(reply interface{}, err error) (bool, error) { | |
202 | + if err != nil { | |
203 | + return false, err | |
204 | + } | |
205 | + switch reply := reply.(type) { | |
206 | + case int64: | |
207 | + return reply != 0, nil | |
208 | + case []byte: | |
209 | + return strconv.ParseBool(string(reply)) | |
210 | + case nil: | |
211 | + return false, ErrNil | |
212 | + case Error: | |
213 | + return false, reply | |
214 | + } | |
215 | + return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply) | |
216 | +} | |
217 | + | |
218 | +// MultiBulk is a helper that converts an array command reply to a []interface{}. | |
219 | +// | |
220 | +// Deprecated: Use Values instead. | |
221 | +func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) } | |
222 | + | |
223 | +// Values is a helper that converts an array command reply to a []interface{}. | |
224 | +// If err is not equal to nil, then Values returns nil, err. Otherwise, Values | |
225 | +// converts the reply as follows: | |
226 | +// | |
227 | +// Reply type Result | |
228 | +// array reply, nil | |
229 | +// nil nil, ErrNil | |
230 | +// other nil, error | |
231 | +func Values(reply interface{}, err error) ([]interface{}, error) { | |
232 | + if err != nil { | |
233 | + return nil, err | |
234 | + } | |
235 | + switch reply := reply.(type) { | |
236 | + case []interface{}: | |
237 | + return reply, nil | |
238 | + case nil: | |
239 | + return nil, ErrNil | |
240 | + case Error: | |
241 | + return nil, reply | |
242 | + } | |
243 | + return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply) | |
244 | +} | |
245 | + | |
246 | +func sliceHelper(reply interface{}, err error, name string, makeSlice func(int), assign func(int, interface{}) error) error { | |
247 | + if err != nil { | |
248 | + return err | |
249 | + } | |
250 | + switch reply := reply.(type) { | |
251 | + case []interface{}: | |
252 | + makeSlice(len(reply)) | |
253 | + for i := range reply { | |
254 | + if reply[i] == nil { | |
255 | + continue | |
256 | + } | |
257 | + if err := assign(i, reply[i]); err != nil { | |
258 | + return err | |
259 | + } | |
260 | + } | |
261 | + return nil | |
262 | + case nil: | |
263 | + return ErrNil | |
264 | + case Error: | |
265 | + return reply | |
266 | + } | |
267 | + return fmt.Errorf("redigo: unexpected type for %s, got type %T", name, reply) | |
268 | +} | |
269 | + | |
270 | +// Float64s is a helper that converts an array command reply to a []float64. If | |
271 | +// err is not equal to nil, then Float64s returns nil, err. Nil array items are | |
272 | +// converted to 0 in the output slice. Floats64 returns an error if an array | |
273 | +// item is not a bulk string or nil. | |
274 | +func Float64s(reply interface{}, err error) ([]float64, error) { | |
275 | + var result []float64 | |
276 | + err = sliceHelper(reply, err, "Float64s", func(n int) { result = make([]float64, n) }, func(i int, v interface{}) error { | |
277 | + p, ok := v.([]byte) | |
278 | + if !ok { | |
279 | + return fmt.Errorf("redigo: unexpected element type for Floats64, got type %T", v) | |
280 | + } | |
281 | + f, err := strconv.ParseFloat(string(p), 64) | |
282 | + result[i] = f | |
283 | + return err | |
284 | + }) | |
285 | + return result, err | |
286 | +} | |
287 | + | |
288 | +// Strings is a helper that converts an array command reply to a []string. If | |
289 | +// err is not equal to nil, then Strings returns nil, err. Nil array items are | |
290 | +// converted to "" in the output slice. Strings returns an error if an array | |
291 | +// item is not a bulk string or nil. | |
292 | +func Strings(reply interface{}, err error) ([]string, error) { | |
293 | + var result []string | |
294 | + err = sliceHelper(reply, err, "Strings", func(n int) { result = make([]string, n) }, func(i int, v interface{}) error { | |
295 | + switch v := v.(type) { | |
296 | + case string: | |
297 | + result[i] = v | |
298 | + return nil | |
299 | + case []byte: | |
300 | + result[i] = string(v) | |
301 | + return nil | |
302 | + default: | |
303 | + return fmt.Errorf("redigo: unexpected element type for Strings, got type %T", v) | |
304 | + } | |
305 | + }) | |
306 | + return result, err | |
307 | +} | |
308 | + | |
309 | +// ByteSlices is a helper that converts an array command reply to a [][]byte. | |
310 | +// If err is not equal to nil, then ByteSlices returns nil, err. Nil array | |
311 | +// items are stay nil. ByteSlices returns an error if an array item is not a | |
312 | +// bulk string or nil. | |
313 | +func ByteSlices(reply interface{}, err error) ([][]byte, error) { | |
314 | + var result [][]byte | |
315 | + err = sliceHelper(reply, err, "ByteSlices", func(n int) { result = make([][]byte, n) }, func(i int, v interface{}) error { | |
316 | + p, ok := v.([]byte) | |
317 | + if !ok { | |
318 | + return fmt.Errorf("redigo: unexpected element type for ByteSlices, got type %T", v) | |
319 | + } | |
320 | + result[i] = p | |
321 | + return nil | |
322 | + }) | |
323 | + return result, err | |
324 | +} | |
325 | + | |
326 | +// Int64s is a helper that converts an array command reply to a []int64. | |
327 | +// If err is not equal to nil, then Int64s returns nil, err. Nil array | |
328 | +// items are stay nil. Int64s returns an error if an array item is not a | |
329 | +// bulk string or nil. | |
330 | +func Int64s(reply interface{}, err error) ([]int64, error) { | |
331 | + var result []int64 | |
332 | + err = sliceHelper(reply, err, "Int64s", func(n int) { result = make([]int64, n) }, func(i int, v interface{}) error { | |
333 | + switch v := v.(type) { | |
334 | + case int64: | |
335 | + result[i] = v | |
336 | + return nil | |
337 | + case []byte: | |
338 | + n, err := strconv.ParseInt(string(v), 10, 64) | |
339 | + result[i] = n | |
340 | + return err | |
341 | + default: | |
342 | + return fmt.Errorf("redigo: unexpected element type for Int64s, got type %T", v) | |
343 | + } | |
344 | + }) | |
345 | + return result, err | |
346 | +} | |
347 | + | |
348 | +// Ints is a helper that converts an array command reply to a []in. | |
349 | +// If err is not equal to nil, then Ints returns nil, err. Nil array | |
350 | +// items are stay nil. Ints returns an error if an array item is not a | |
351 | +// bulk string or nil. | |
352 | +func Ints(reply interface{}, err error) ([]int, error) { | |
353 | + var result []int | |
354 | + err = sliceHelper(reply, err, "Ints", func(n int) { result = make([]int, n) }, func(i int, v interface{}) error { | |
355 | + switch v := v.(type) { | |
356 | + case int64: | |
357 | + n := int(v) | |
358 | + if int64(n) != v { | |
359 | + return strconv.ErrRange | |
360 | + } | |
361 | + result[i] = n | |
362 | + return nil | |
363 | + case []byte: | |
364 | + n, err := strconv.Atoi(string(v)) | |
365 | + result[i] = n | |
366 | + return err | |
367 | + default: | |
368 | + return fmt.Errorf("redigo: unexpected element type for Ints, got type %T", v) | |
369 | + } | |
370 | + }) | |
371 | + return result, err | |
372 | +} | |
373 | + | |
374 | +// StringMap is a helper that converts an array of strings (alternating key, value) | |
375 | +// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. | |
376 | +// Requires an even number of values in result. | |
377 | +func StringMap(result interface{}, err error) (map[string]string, error) { | |
378 | + values, err := Values(result, err) | |
379 | + if err != nil { | |
380 | + return nil, err | |
381 | + } | |
382 | + if len(values)%2 != 0 { | |
383 | + return nil, errors.New("redigo: StringMap expects even number of values result") | |
384 | + } | |
385 | + m := make(map[string]string, len(values)/2) | |
386 | + for i := 0; i < len(values); i += 2 { | |
387 | + key, okKey := values[i].([]byte) | |
388 | + value, okValue := values[i+1].([]byte) | |
389 | + if !okKey || !okValue { | |
390 | + return nil, errors.New("redigo: StringMap key not a bulk string value") | |
391 | + } | |
392 | + m[string(key)] = string(value) | |
393 | + } | |
394 | + return m, nil | |
395 | +} | |
396 | + | |
397 | +// IntMap is a helper that converts an array of strings (alternating key, value) | |
398 | +// into a map[string]int. The HGETALL commands return replies in this format. | |
399 | +// Requires an even number of values in result. | |
400 | +func IntMap(result interface{}, err error) (map[string]int, error) { | |
401 | + values, err := Values(result, err) | |
402 | + if err != nil { | |
403 | + return nil, err | |
404 | + } | |
405 | + if len(values)%2 != 0 { | |
406 | + return nil, errors.New("redigo: IntMap expects even number of values result") | |
407 | + } | |
408 | + m := make(map[string]int, len(values)/2) | |
409 | + for i := 0; i < len(values); i += 2 { | |
410 | + key, ok := values[i].([]byte) | |
411 | + if !ok { | |
412 | + return nil, errors.New("redigo: IntMap key not a bulk string value") | |
413 | + } | |
414 | + value, err := Int(values[i+1], nil) | |
415 | + if err != nil { | |
416 | + return nil, err | |
417 | + } | |
418 | + m[string(key)] = value | |
419 | + } | |
420 | + return m, nil | |
421 | +} | |
422 | + | |
423 | +// Int64Map is a helper that converts an array of strings (alternating key, value) | |
424 | +// into a map[string]int64. The HGETALL commands return replies in this format. | |
425 | +// Requires an even number of values in result. | |
426 | +func Int64Map(result interface{}, err error) (map[string]int64, error) { | |
427 | + values, err := Values(result, err) | |
428 | + if err != nil { | |
429 | + return nil, err | |
430 | + } | |
431 | + if len(values)%2 != 0 { | |
432 | + return nil, errors.New("redigo: Int64Map expects even number of values result") | |
433 | + } | |
434 | + m := make(map[string]int64, len(values)/2) | |
435 | + for i := 0; i < len(values); i += 2 { | |
436 | + key, ok := values[i].([]byte) | |
437 | + if !ok { | |
438 | + return nil, errors.New("redigo: Int64Map key not a bulk string value") | |
439 | + } | |
440 | + value, err := Int64(values[i+1], nil) | |
441 | + if err != nil { | |
442 | + return nil, err | |
443 | + } | |
444 | + m[string(key)] = value | |
445 | + } | |
446 | + return m, nil | |
447 | +} | |
448 | + | |
449 | +// Positions is a helper that converts an array of positions (lat, long) | |
450 | +// into a [][2]float64. The GEOPOS command returns replies in this format. | |
451 | +func Positions(result interface{}, err error) ([]*[2]float64, error) { | |
452 | + values, err := Values(result, err) | |
453 | + if err != nil { | |
454 | + return nil, err | |
455 | + } | |
456 | + positions := make([]*[2]float64, len(values)) | |
457 | + for i := range values { | |
458 | + if values[i] == nil { | |
459 | + continue | |
460 | + } | |
461 | + p, ok := values[i].([]interface{}) | |
462 | + if !ok { | |
463 | + return nil, fmt.Errorf("redigo: unexpected element type for interface slice, got type %T", values[i]) | |
464 | + } | |
465 | + if len(p) != 2 { | |
466 | + return nil, fmt.Errorf("redigo: unexpected number of values for a member position, got %d", len(p)) | |
467 | + } | |
468 | + lat, err := Float64(p[0], nil) | |
469 | + if err != nil { | |
470 | + return nil, err | |
471 | + } | |
472 | + long, err := Float64(p[1], nil) | |
473 | + if err != nil { | |
474 | + return nil, err | |
475 | + } | |
476 | + positions[i] = &[2]float64{lat, long} | |
477 | + } | |
478 | + return positions, nil | |
479 | +} | ... | ... |
... | ... | @@ -0,0 +1,209 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "fmt" | |
19 | + "reflect" | |
20 | + "testing" | |
21 | + | |
22 | + "github.com/garyburd/redigo/redis" | |
23 | +) | |
24 | + | |
25 | +type valueError struct { | |
26 | + v interface{} | |
27 | + err error | |
28 | +} | |
29 | + | |
30 | +func ve(v interface{}, err error) valueError { | |
31 | + return valueError{v, err} | |
32 | +} | |
33 | + | |
34 | +var replyTests = []struct { | |
35 | + name interface{} | |
36 | + actual valueError | |
37 | + expected valueError | |
38 | +}{ | |
39 | + { | |
40 | + "ints([[]byte, []byte])", | |
41 | + ve(redis.Ints([]interface{}{[]byte("4"), []byte("5")}, nil)), | |
42 | + ve([]int{4, 5}, nil), | |
43 | + }, | |
44 | + { | |
45 | + "ints([nt64, int64])", | |
46 | + ve(redis.Ints([]interface{}{int64(4), int64(5)}, nil)), | |
47 | + ve([]int{4, 5}, nil), | |
48 | + }, | |
49 | + { | |
50 | + "ints([[]byte, nil, []byte])", | |
51 | + ve(redis.Ints([]interface{}{[]byte("4"), nil, []byte("5")}, nil)), | |
52 | + ve([]int{4, 0, 5}, nil), | |
53 | + }, | |
54 | + { | |
55 | + "ints(nil)", | |
56 | + ve(redis.Ints(nil, nil)), | |
57 | + ve([]int(nil), redis.ErrNil), | |
58 | + }, | |
59 | + { | |
60 | + "int64s([[]byte, []byte])", | |
61 | + ve(redis.Int64s([]interface{}{[]byte("4"), []byte("5")}, nil)), | |
62 | + ve([]int64{4, 5}, nil), | |
63 | + }, | |
64 | + { | |
65 | + "int64s([int64, int64])", | |
66 | + ve(redis.Int64s([]interface{}{int64(4), int64(5)}, nil)), | |
67 | + ve([]int64{4, 5}, nil), | |
68 | + }, | |
69 | + { | |
70 | + "strings([[]byte, []bytev2])", | |
71 | + ve(redis.Strings([]interface{}{[]byte("v1"), []byte("v2")}, nil)), | |
72 | + ve([]string{"v1", "v2"}, nil), | |
73 | + }, | |
74 | + { | |
75 | + "strings([string, string])", | |
76 | + ve(redis.Strings([]interface{}{"v1", "v2"}, nil)), | |
77 | + ve([]string{"v1", "v2"}, nil), | |
78 | + }, | |
79 | + { | |
80 | + "byteslices([v1, v2])", | |
81 | + ve(redis.ByteSlices([]interface{}{[]byte("v1"), []byte("v2")}, nil)), | |
82 | + ve([][]byte{[]byte("v1"), []byte("v2")}, nil), | |
83 | + }, | |
84 | + { | |
85 | + "float64s([v1, v2])", | |
86 | + ve(redis.Float64s([]interface{}{[]byte("1.234"), []byte("5.678")}, nil)), | |
87 | + ve([]float64{1.234, 5.678}, nil), | |
88 | + }, | |
89 | + { | |
90 | + "values([v1, v2])", | |
91 | + ve(redis.Values([]interface{}{[]byte("v1"), []byte("v2")}, nil)), | |
92 | + ve([]interface{}{[]byte("v1"), []byte("v2")}, nil), | |
93 | + }, | |
94 | + { | |
95 | + "values(nil)", | |
96 | + ve(redis.Values(nil, nil)), | |
97 | + ve([]interface{}(nil), redis.ErrNil), | |
98 | + }, | |
99 | + { | |
100 | + "float64(1.0)", | |
101 | + ve(redis.Float64([]byte("1.0"), nil)), | |
102 | + ve(float64(1.0), nil), | |
103 | + }, | |
104 | + { | |
105 | + "float64(nil)", | |
106 | + ve(redis.Float64(nil, nil)), | |
107 | + ve(float64(0.0), redis.ErrNil), | |
108 | + }, | |
109 | + { | |
110 | + "uint64(1)", | |
111 | + ve(redis.Uint64(int64(1), nil)), | |
112 | + ve(uint64(1), nil), | |
113 | + }, | |
114 | + { | |
115 | + "uint64(-1)", | |
116 | + ve(redis.Uint64(int64(-1), nil)), | |
117 | + ve(uint64(0), redis.ErrNegativeInt), | |
118 | + }, | |
119 | + { | |
120 | + "positions([[1, 2], nil, [3, 4]])", | |
121 | + ve(redis.Positions([]interface{}{[]interface{}{[]byte("1"), []byte("2")}, nil, []interface{}{[]byte("3"), []byte("4")}}, nil)), | |
122 | + ve([]*[2]float64{{1.0, 2.0}, nil, {3.0, 4.0}}, nil), | |
123 | + }, | |
124 | +} | |
125 | + | |
126 | +func TestReply(t *testing.T) { | |
127 | + for _, rt := range replyTests { | |
128 | + if rt.actual.err != rt.expected.err { | |
129 | + t.Errorf("%s returned err %v, want %v", rt.name, rt.actual.err, rt.expected.err) | |
130 | + continue | |
131 | + } | |
132 | + if !reflect.DeepEqual(rt.actual.v, rt.expected.v) { | |
133 | + t.Errorf("%s=%+v, want %+v", rt.name, rt.actual.v, rt.expected.v) | |
134 | + } | |
135 | + } | |
136 | +} | |
137 | + | |
138 | +// dial wraps DialDefaultServer() with a more suitable function name for examples. | |
139 | +func dial() (redis.Conn, error) { | |
140 | + return redis.DialDefaultServer() | |
141 | +} | |
142 | + | |
143 | +// serverAddr wraps DefaultServerAddr() with a more suitable function name for examples. | |
144 | +func serverAddr() (string, error) { | |
145 | + return redis.DefaultServerAddr() | |
146 | +} | |
147 | + | |
148 | +func ExampleBool() { | |
149 | + c, err := dial() | |
150 | + if err != nil { | |
151 | + fmt.Println(err) | |
152 | + return | |
153 | + } | |
154 | + defer c.Close() | |
155 | + | |
156 | + c.Do("SET", "foo", 1) | |
157 | + exists, _ := redis.Bool(c.Do("EXISTS", "foo")) | |
158 | + fmt.Printf("%#v\n", exists) | |
159 | + // Output: | |
160 | + // true | |
161 | +} | |
162 | + | |
163 | +func ExampleInt() { | |
164 | + c, err := dial() | |
165 | + if err != nil { | |
166 | + fmt.Println(err) | |
167 | + return | |
168 | + } | |
169 | + defer c.Close() | |
170 | + | |
171 | + c.Do("SET", "k1", 1) | |
172 | + n, _ := redis.Int(c.Do("GET", "k1")) | |
173 | + fmt.Printf("%#v\n", n) | |
174 | + n, _ = redis.Int(c.Do("INCR", "k1")) | |
175 | + fmt.Printf("%#v\n", n) | |
176 | + // Output: | |
177 | + // 1 | |
178 | + // 2 | |
179 | +} | |
180 | + | |
181 | +func ExampleInts() { | |
182 | + c, err := dial() | |
183 | + if err != nil { | |
184 | + fmt.Println(err) | |
185 | + return | |
186 | + } | |
187 | + defer c.Close() | |
188 | + | |
189 | + c.Do("SADD", "set_with_integers", 4, 5, 6) | |
190 | + ints, _ := redis.Ints(c.Do("SMEMBERS", "set_with_integers")) | |
191 | + fmt.Printf("%#v\n", ints) | |
192 | + // Output: | |
193 | + // []int{4, 5, 6} | |
194 | +} | |
195 | + | |
196 | +func ExampleString() { | |
197 | + c, err := dial() | |
198 | + if err != nil { | |
199 | + fmt.Println(err) | |
200 | + return | |
201 | + } | |
202 | + defer c.Close() | |
203 | + | |
204 | + c.Do("SET", "hello", "world") | |
205 | + s, err := redis.String(c.Do("GET", "hello")) | |
206 | + fmt.Printf("%#v\n", s) | |
207 | + // Output: | |
208 | + // "world" | |
209 | +} | ... | ... |
... | ... | @@ -0,0 +1,585 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "errors" | |
19 | + "fmt" | |
20 | + "reflect" | |
21 | + "strconv" | |
22 | + "strings" | |
23 | + "sync" | |
24 | +) | |
25 | + | |
26 | +func ensureLen(d reflect.Value, n int) { | |
27 | + if n > d.Cap() { | |
28 | + d.Set(reflect.MakeSlice(d.Type(), n, n)) | |
29 | + } else { | |
30 | + d.SetLen(n) | |
31 | + } | |
32 | +} | |
33 | + | |
34 | +func cannotConvert(d reflect.Value, s interface{}) error { | |
35 | + var sname string | |
36 | + switch s.(type) { | |
37 | + case string: | |
38 | + sname = "Redis simple string" | |
39 | + case Error: | |
40 | + sname = "Redis error" | |
41 | + case int64: | |
42 | + sname = "Redis integer" | |
43 | + case []byte: | |
44 | + sname = "Redis bulk string" | |
45 | + case []interface{}: | |
46 | + sname = "Redis array" | |
47 | + default: | |
48 | + sname = reflect.TypeOf(s).String() | |
49 | + } | |
50 | + return fmt.Errorf("cannot convert from %s to %s", sname, d.Type()) | |
51 | +} | |
52 | + | |
53 | +func convertAssignBulkString(d reflect.Value, s []byte) (err error) { | |
54 | + switch d.Type().Kind() { | |
55 | + case reflect.Float32, reflect.Float64: | |
56 | + var x float64 | |
57 | + x, err = strconv.ParseFloat(string(s), d.Type().Bits()) | |
58 | + d.SetFloat(x) | |
59 | + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
60 | + var x int64 | |
61 | + x, err = strconv.ParseInt(string(s), 10, d.Type().Bits()) | |
62 | + d.SetInt(x) | |
63 | + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |
64 | + var x uint64 | |
65 | + x, err = strconv.ParseUint(string(s), 10, d.Type().Bits()) | |
66 | + d.SetUint(x) | |
67 | + case reflect.Bool: | |
68 | + var x bool | |
69 | + x, err = strconv.ParseBool(string(s)) | |
70 | + d.SetBool(x) | |
71 | + case reflect.String: | |
72 | + d.SetString(string(s)) | |
73 | + case reflect.Slice: | |
74 | + if d.Type().Elem().Kind() != reflect.Uint8 { | |
75 | + err = cannotConvert(d, s) | |
76 | + } else { | |
77 | + d.SetBytes(s) | |
78 | + } | |
79 | + default: | |
80 | + err = cannotConvert(d, s) | |
81 | + } | |
82 | + return | |
83 | +} | |
84 | + | |
85 | +func convertAssignInt(d reflect.Value, s int64) (err error) { | |
86 | + switch d.Type().Kind() { | |
87 | + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
88 | + d.SetInt(s) | |
89 | + if d.Int() != s { | |
90 | + err = strconv.ErrRange | |
91 | + d.SetInt(0) | |
92 | + } | |
93 | + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: | |
94 | + if s < 0 { | |
95 | + err = strconv.ErrRange | |
96 | + } else { | |
97 | + x := uint64(s) | |
98 | + d.SetUint(x) | |
99 | + if d.Uint() != x { | |
100 | + err = strconv.ErrRange | |
101 | + d.SetUint(0) | |
102 | + } | |
103 | + } | |
104 | + case reflect.Bool: | |
105 | + d.SetBool(s != 0) | |
106 | + default: | |
107 | + err = cannotConvert(d, s) | |
108 | + } | |
109 | + return | |
110 | +} | |
111 | + | |
112 | +func convertAssignValue(d reflect.Value, s interface{}) (err error) { | |
113 | + if d.Kind() != reflect.Ptr { | |
114 | + if d.CanAddr() { | |
115 | + d2 := d.Addr() | |
116 | + if d2.CanInterface() { | |
117 | + if scanner, ok := d2.Interface().(Scanner); ok { | |
118 | + return scanner.RedisScan(s) | |
119 | + } | |
120 | + } | |
121 | + } | |
122 | + } else if d.CanInterface() { | |
123 | + // Already a reflect.Ptr | |
124 | + if d.IsNil() { | |
125 | + d.Set(reflect.New(d.Type().Elem())) | |
126 | + } | |
127 | + if scanner, ok := d.Interface().(Scanner); ok { | |
128 | + return scanner.RedisScan(s) | |
129 | + } | |
130 | + } | |
131 | + | |
132 | + switch s := s.(type) { | |
133 | + case []byte: | |
134 | + err = convertAssignBulkString(d, s) | |
135 | + case int64: | |
136 | + err = convertAssignInt(d, s) | |
137 | + default: | |
138 | + err = cannotConvert(d, s) | |
139 | + } | |
140 | + return err | |
141 | +} | |
142 | + | |
143 | +func convertAssignArray(d reflect.Value, s []interface{}) error { | |
144 | + if d.Type().Kind() != reflect.Slice { | |
145 | + return cannotConvert(d, s) | |
146 | + } | |
147 | + ensureLen(d, len(s)) | |
148 | + for i := 0; i < len(s); i++ { | |
149 | + if err := convertAssignValue(d.Index(i), s[i]); err != nil { | |
150 | + return err | |
151 | + } | |
152 | + } | |
153 | + return nil | |
154 | +} | |
155 | + | |
156 | +func convertAssign(d interface{}, s interface{}) (err error) { | |
157 | + if scanner, ok := d.(Scanner); ok { | |
158 | + return scanner.RedisScan(s) | |
159 | + } | |
160 | + | |
161 | + // Handle the most common destination types using type switches and | |
162 | + // fall back to reflection for all other types. | |
163 | + switch s := s.(type) { | |
164 | + case nil: | |
165 | + // ignore | |
166 | + case []byte: | |
167 | + switch d := d.(type) { | |
168 | + case *string: | |
169 | + *d = string(s) | |
170 | + case *int: | |
171 | + *d, err = strconv.Atoi(string(s)) | |
172 | + case *bool: | |
173 | + *d, err = strconv.ParseBool(string(s)) | |
174 | + case *[]byte: | |
175 | + *d = s | |
176 | + case *interface{}: | |
177 | + *d = s | |
178 | + case nil: | |
179 | + // skip value | |
180 | + default: | |
181 | + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { | |
182 | + err = cannotConvert(d, s) | |
183 | + } else { | |
184 | + err = convertAssignBulkString(d.Elem(), s) | |
185 | + } | |
186 | + } | |
187 | + case int64: | |
188 | + switch d := d.(type) { | |
189 | + case *int: | |
190 | + x := int(s) | |
191 | + if int64(x) != s { | |
192 | + err = strconv.ErrRange | |
193 | + x = 0 | |
194 | + } | |
195 | + *d = x | |
196 | + case *bool: | |
197 | + *d = s != 0 | |
198 | + case *interface{}: | |
199 | + *d = s | |
200 | + case nil: | |
201 | + // skip value | |
202 | + default: | |
203 | + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { | |
204 | + err = cannotConvert(d, s) | |
205 | + } else { | |
206 | + err = convertAssignInt(d.Elem(), s) | |
207 | + } | |
208 | + } | |
209 | + case string: | |
210 | + switch d := d.(type) { | |
211 | + case *string: | |
212 | + *d = s | |
213 | + case *interface{}: | |
214 | + *d = s | |
215 | + case nil: | |
216 | + // skip value | |
217 | + default: | |
218 | + err = cannotConvert(reflect.ValueOf(d), s) | |
219 | + } | |
220 | + case []interface{}: | |
221 | + switch d := d.(type) { | |
222 | + case *[]interface{}: | |
223 | + *d = s | |
224 | + case *interface{}: | |
225 | + *d = s | |
226 | + case nil: | |
227 | + // skip value | |
228 | + default: | |
229 | + if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { | |
230 | + err = cannotConvert(d, s) | |
231 | + } else { | |
232 | + err = convertAssignArray(d.Elem(), s) | |
233 | + } | |
234 | + } | |
235 | + case Error: | |
236 | + err = s | |
237 | + default: | |
238 | + err = cannotConvert(reflect.ValueOf(d), s) | |
239 | + } | |
240 | + return | |
241 | +} | |
242 | + | |
243 | +// Scan copies from src to the values pointed at by dest. | |
244 | +// | |
245 | +// Scan uses RedisScan if available otherwise: | |
246 | +// | |
247 | +// The values pointed at by dest must be an integer, float, boolean, string, | |
248 | +// []byte, interface{} or slices of these types. Scan uses the standard strconv | |
249 | +// package to convert bulk strings to numeric and boolean types. | |
250 | +// | |
251 | +// If a dest value is nil, then the corresponding src value is skipped. | |
252 | +// | |
253 | +// If a src element is nil, then the corresponding dest value is not modified. | |
254 | +// | |
255 | +// To enable easy use of Scan in a loop, Scan returns the slice of src | |
256 | +// following the copied values. | |
257 | +func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) { | |
258 | + if len(src) < len(dest) { | |
259 | + return nil, errors.New("redigo.Scan: array short") | |
260 | + } | |
261 | + var err error | |
262 | + for i, d := range dest { | |
263 | + err = convertAssign(d, src[i]) | |
264 | + if err != nil { | |
265 | + err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err) | |
266 | + break | |
267 | + } | |
268 | + } | |
269 | + return src[len(dest):], err | |
270 | +} | |
271 | + | |
272 | +type fieldSpec struct { | |
273 | + name string | |
274 | + index []int | |
275 | + omitEmpty bool | |
276 | +} | |
277 | + | |
278 | +type structSpec struct { | |
279 | + m map[string]*fieldSpec | |
280 | + l []*fieldSpec | |
281 | +} | |
282 | + | |
283 | +func (ss *structSpec) fieldSpec(name []byte) *fieldSpec { | |
284 | + return ss.m[string(name)] | |
285 | +} | |
286 | + | |
287 | +func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) { | |
288 | + for i := 0; i < t.NumField(); i++ { | |
289 | + f := t.Field(i) | |
290 | + switch { | |
291 | + case f.PkgPath != "" && !f.Anonymous: | |
292 | + // Ignore unexported fields. | |
293 | + case f.Anonymous: | |
294 | + // TODO: Handle pointers. Requires change to decoder and | |
295 | + // protection against infinite recursion. | |
296 | + if f.Type.Kind() == reflect.Struct { | |
297 | + compileStructSpec(f.Type, depth, append(index, i), ss) | |
298 | + } | |
299 | + default: | |
300 | + fs := &fieldSpec{name: f.Name} | |
301 | + tag := f.Tag.Get("redis") | |
302 | + p := strings.Split(tag, ",") | |
303 | + if len(p) > 0 { | |
304 | + if p[0] == "-" { | |
305 | + continue | |
306 | + } | |
307 | + if len(p[0]) > 0 { | |
308 | + fs.name = p[0] | |
309 | + } | |
310 | + for _, s := range p[1:] { | |
311 | + switch s { | |
312 | + case "omitempty": | |
313 | + fs.omitEmpty = true | |
314 | + default: | |
315 | + panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name())) | |
316 | + } | |
317 | + } | |
318 | + } | |
319 | + d, found := depth[fs.name] | |
320 | + if !found { | |
321 | + d = 1 << 30 | |
322 | + } | |
323 | + switch { | |
324 | + case len(index) == d: | |
325 | + // At same depth, remove from result. | |
326 | + delete(ss.m, fs.name) | |
327 | + j := 0 | |
328 | + for i := 0; i < len(ss.l); i++ { | |
329 | + if fs.name != ss.l[i].name { | |
330 | + ss.l[j] = ss.l[i] | |
331 | + j += 1 | |
332 | + } | |
333 | + } | |
334 | + ss.l = ss.l[:j] | |
335 | + case len(index) < d: | |
336 | + fs.index = make([]int, len(index)+1) | |
337 | + copy(fs.index, index) | |
338 | + fs.index[len(index)] = i | |
339 | + depth[fs.name] = len(index) | |
340 | + ss.m[fs.name] = fs | |
341 | + ss.l = append(ss.l, fs) | |
342 | + } | |
343 | + } | |
344 | + } | |
345 | +} | |
346 | + | |
347 | +var ( | |
348 | + structSpecMutex sync.RWMutex | |
349 | + structSpecCache = make(map[reflect.Type]*structSpec) | |
350 | + defaultFieldSpec = &fieldSpec{} | |
351 | +) | |
352 | + | |
353 | +func structSpecForType(t reflect.Type) *structSpec { | |
354 | + | |
355 | + structSpecMutex.RLock() | |
356 | + ss, found := structSpecCache[t] | |
357 | + structSpecMutex.RUnlock() | |
358 | + if found { | |
359 | + return ss | |
360 | + } | |
361 | + | |
362 | + structSpecMutex.Lock() | |
363 | + defer structSpecMutex.Unlock() | |
364 | + ss, found = structSpecCache[t] | |
365 | + if found { | |
366 | + return ss | |
367 | + } | |
368 | + | |
369 | + ss = &structSpec{m: make(map[string]*fieldSpec)} | |
370 | + compileStructSpec(t, make(map[string]int), nil, ss) | |
371 | + structSpecCache[t] = ss | |
372 | + return ss | |
373 | +} | |
374 | + | |
375 | +var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct") | |
376 | + | |
377 | +// ScanStruct scans alternating names and values from src to a struct. The | |
378 | +// HGETALL and CONFIG GET commands return replies in this format. | |
379 | +// | |
380 | +// ScanStruct uses exported field names to match values in the response. Use | |
381 | +// 'redis' field tag to override the name: | |
382 | +// | |
383 | +// Field int `redis:"myName"` | |
384 | +// | |
385 | +// Fields with the tag redis:"-" are ignored. | |
386 | +// | |
387 | +// Each field uses RedisScan if available otherwise: | |
388 | +// Integer, float, boolean, string and []byte fields are supported. Scan uses the | |
389 | +// standard strconv package to convert bulk string values to numeric and | |
390 | +// boolean types. | |
391 | +// | |
392 | +// If a src element is nil, then the corresponding field is not modified. | |
393 | +func ScanStruct(src []interface{}, dest interface{}) error { | |
394 | + d := reflect.ValueOf(dest) | |
395 | + if d.Kind() != reflect.Ptr || d.IsNil() { | |
396 | + return errScanStructValue | |
397 | + } | |
398 | + d = d.Elem() | |
399 | + if d.Kind() != reflect.Struct { | |
400 | + return errScanStructValue | |
401 | + } | |
402 | + ss := structSpecForType(d.Type()) | |
403 | + | |
404 | + if len(src)%2 != 0 { | |
405 | + return errors.New("redigo.ScanStruct: number of values not a multiple of 2") | |
406 | + } | |
407 | + | |
408 | + for i := 0; i < len(src); i += 2 { | |
409 | + s := src[i+1] | |
410 | + if s == nil { | |
411 | + continue | |
412 | + } | |
413 | + name, ok := src[i].([]byte) | |
414 | + if !ok { | |
415 | + return fmt.Errorf("redigo.ScanStruct: key %d not a bulk string value", i) | |
416 | + } | |
417 | + fs := ss.fieldSpec(name) | |
418 | + if fs == nil { | |
419 | + continue | |
420 | + } | |
421 | + if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { | |
422 | + return fmt.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err) | |
423 | + } | |
424 | + } | |
425 | + return nil | |
426 | +} | |
427 | + | |
428 | +var ( | |
429 | + errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct") | |
430 | +) | |
431 | + | |
432 | +// ScanSlice scans src to the slice pointed to by dest. The elements the dest | |
433 | +// slice must be integer, float, boolean, string, struct or pointer to struct | |
434 | +// values. | |
435 | +// | |
436 | +// Struct fields must be integer, float, boolean or string values. All struct | |
437 | +// fields are used unless a subset is specified using fieldNames. | |
438 | +func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error { | |
439 | + d := reflect.ValueOf(dest) | |
440 | + if d.Kind() != reflect.Ptr || d.IsNil() { | |
441 | + return errScanSliceValue | |
442 | + } | |
443 | + d = d.Elem() | |
444 | + if d.Kind() != reflect.Slice { | |
445 | + return errScanSliceValue | |
446 | + } | |
447 | + | |
448 | + isPtr := false | |
449 | + t := d.Type().Elem() | |
450 | + if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { | |
451 | + isPtr = true | |
452 | + t = t.Elem() | |
453 | + } | |
454 | + | |
455 | + if t.Kind() != reflect.Struct { | |
456 | + ensureLen(d, len(src)) | |
457 | + for i, s := range src { | |
458 | + if s == nil { | |
459 | + continue | |
460 | + } | |
461 | + if err := convertAssignValue(d.Index(i), s); err != nil { | |
462 | + return fmt.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err) | |
463 | + } | |
464 | + } | |
465 | + return nil | |
466 | + } | |
467 | + | |
468 | + ss := structSpecForType(t) | |
469 | + fss := ss.l | |
470 | + if len(fieldNames) > 0 { | |
471 | + fss = make([]*fieldSpec, len(fieldNames)) | |
472 | + for i, name := range fieldNames { | |
473 | + fss[i] = ss.m[name] | |
474 | + if fss[i] == nil { | |
475 | + return fmt.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name) | |
476 | + } | |
477 | + } | |
478 | + } | |
479 | + | |
480 | + if len(fss) == 0 { | |
481 | + return errors.New("redigo.ScanSlice: no struct fields") | |
482 | + } | |
483 | + | |
484 | + n := len(src) / len(fss) | |
485 | + if n*len(fss) != len(src) { | |
486 | + return errors.New("redigo.ScanSlice: length not a multiple of struct field count") | |
487 | + } | |
488 | + | |
489 | + ensureLen(d, n) | |
490 | + for i := 0; i < n; i++ { | |
491 | + d := d.Index(i) | |
492 | + if isPtr { | |
493 | + if d.IsNil() { | |
494 | + d.Set(reflect.New(t)) | |
495 | + } | |
496 | + d = d.Elem() | |
497 | + } | |
498 | + for j, fs := range fss { | |
499 | + s := src[i*len(fss)+j] | |
500 | + if s == nil { | |
501 | + continue | |
502 | + } | |
503 | + if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { | |
504 | + return fmt.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err) | |
505 | + } | |
506 | + } | |
507 | + } | |
508 | + return nil | |
509 | +} | |
510 | + | |
511 | +// Args is a helper for constructing command arguments from structured values. | |
512 | +type Args []interface{} | |
513 | + | |
514 | +// Add returns the result of appending value to args. | |
515 | +func (args Args) Add(value ...interface{}) Args { | |
516 | + return append(args, value...) | |
517 | +} | |
518 | + | |
519 | +// AddFlat returns the result of appending the flattened value of v to args. | |
520 | +// | |
521 | +// Maps are flattened by appending the alternating keys and map values to args. | |
522 | +// | |
523 | +// Slices are flattened by appending the slice elements to args. | |
524 | +// | |
525 | +// Structs are flattened by appending the alternating names and values of | |
526 | +// exported fields to args. If v is a nil struct pointer, then nothing is | |
527 | +// appended. The 'redis' field tag overrides struct field names. See ScanStruct | |
528 | +// for more information on the use of the 'redis' field tag. | |
529 | +// | |
530 | +// Other types are appended to args as is. | |
531 | +func (args Args) AddFlat(v interface{}) Args { | |
532 | + rv := reflect.ValueOf(v) | |
533 | + switch rv.Kind() { | |
534 | + case reflect.Struct: | |
535 | + args = flattenStruct(args, rv) | |
536 | + case reflect.Slice: | |
537 | + for i := 0; i < rv.Len(); i++ { | |
538 | + args = append(args, rv.Index(i).Interface()) | |
539 | + } | |
540 | + case reflect.Map: | |
541 | + for _, k := range rv.MapKeys() { | |
542 | + args = append(args, k.Interface(), rv.MapIndex(k).Interface()) | |
543 | + } | |
544 | + case reflect.Ptr: | |
545 | + if rv.Type().Elem().Kind() == reflect.Struct { | |
546 | + if !rv.IsNil() { | |
547 | + args = flattenStruct(args, rv.Elem()) | |
548 | + } | |
549 | + } else { | |
550 | + args = append(args, v) | |
551 | + } | |
552 | + default: | |
553 | + args = append(args, v) | |
554 | + } | |
555 | + return args | |
556 | +} | |
557 | + | |
558 | +func flattenStruct(args Args, v reflect.Value) Args { | |
559 | + ss := structSpecForType(v.Type()) | |
560 | + for _, fs := range ss.l { | |
561 | + fv := v.FieldByIndex(fs.index) | |
562 | + if fs.omitEmpty { | |
563 | + var empty = false | |
564 | + switch fv.Kind() { | |
565 | + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: | |
566 | + empty = fv.Len() == 0 | |
567 | + case reflect.Bool: | |
568 | + empty = !fv.Bool() | |
569 | + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
570 | + empty = fv.Int() == 0 | |
571 | + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: | |
572 | + empty = fv.Uint() == 0 | |
573 | + case reflect.Float32, reflect.Float64: | |
574 | + empty = fv.Float() == 0 | |
575 | + case reflect.Interface, reflect.Ptr: | |
576 | + empty = fv.IsNil() | |
577 | + } | |
578 | + if empty { | |
579 | + continue | |
580 | + } | |
581 | + } | |
582 | + args = append(args, fs.name, fv.Interface()) | |
583 | + } | |
584 | + return args | |
585 | +} | ... | ... |
... | ... | @@ -0,0 +1,494 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "fmt" | |
19 | + "math" | |
20 | + "reflect" | |
21 | + "testing" | |
22 | + "time" | |
23 | + | |
24 | + "github.com/garyburd/redigo/redis" | |
25 | +) | |
26 | + | |
27 | +type durationScan struct { | |
28 | + time.Duration `redis:"sd"` | |
29 | +} | |
30 | + | |
31 | +func (t *durationScan) RedisScan(src interface{}) (err error) { | |
32 | + if t == nil { | |
33 | + return fmt.Errorf("nil pointer") | |
34 | + } | |
35 | + switch src := src.(type) { | |
36 | + case string: | |
37 | + t.Duration, err = time.ParseDuration(src) | |
38 | + case []byte: | |
39 | + t.Duration, err = time.ParseDuration(string(src)) | |
40 | + case int64: | |
41 | + t.Duration = time.Duration(src) | |
42 | + default: | |
43 | + err = fmt.Errorf("cannot convert from %T to %T", src, t) | |
44 | + } | |
45 | + return err | |
46 | +} | |
47 | + | |
48 | +var scanConversionTests = []struct { | |
49 | + src interface{} | |
50 | + dest interface{} | |
51 | +}{ | |
52 | + {[]byte("-inf"), math.Inf(-1)}, | |
53 | + {[]byte("+inf"), math.Inf(1)}, | |
54 | + {[]byte("0"), float64(0)}, | |
55 | + {[]byte("3.14159"), float64(3.14159)}, | |
56 | + {[]byte("3.14"), float32(3.14)}, | |
57 | + {[]byte("-100"), int(-100)}, | |
58 | + {[]byte("101"), int(101)}, | |
59 | + {int64(102), int(102)}, | |
60 | + {[]byte("103"), uint(103)}, | |
61 | + {int64(104), uint(104)}, | |
62 | + {[]byte("105"), int8(105)}, | |
63 | + {int64(106), int8(106)}, | |
64 | + {[]byte("107"), uint8(107)}, | |
65 | + {int64(108), uint8(108)}, | |
66 | + {[]byte("0"), false}, | |
67 | + {int64(0), false}, | |
68 | + {[]byte("f"), false}, | |
69 | + {[]byte("1"), true}, | |
70 | + {int64(1), true}, | |
71 | + {[]byte("t"), true}, | |
72 | + {"hello", "hello"}, | |
73 | + {[]byte("hello"), "hello"}, | |
74 | + {[]byte("world"), []byte("world")}, | |
75 | + {[]interface{}{[]byte("foo")}, []interface{}{[]byte("foo")}}, | |
76 | + {[]interface{}{[]byte("foo")}, []string{"foo"}}, | |
77 | + {[]interface{}{[]byte("hello"), []byte("world")}, []string{"hello", "world"}}, | |
78 | + {[]interface{}{[]byte("bar")}, [][]byte{[]byte("bar")}}, | |
79 | + {[]interface{}{[]byte("1")}, []int{1}}, | |
80 | + {[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}}, | |
81 | + {[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}}, | |
82 | + {[]interface{}{[]byte("1")}, []byte{1}}, | |
83 | + {[]interface{}{[]byte("1")}, []bool{true}}, | |
84 | + {"1m", durationScan{Duration: time.Minute}}, | |
85 | + {[]byte("1m"), durationScan{Duration: time.Minute}}, | |
86 | + {time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}}, | |
87 | + {[]interface{}{[]byte("1m")}, []durationScan{durationScan{Duration: time.Minute}}}, | |
88 | + {[]interface{}{[]byte("1m")}, []*durationScan{&durationScan{Duration: time.Minute}}}, | |
89 | +} | |
90 | + | |
91 | +func TestScanConversion(t *testing.T) { | |
92 | + for _, tt := range scanConversionTests { | |
93 | + values := []interface{}{tt.src} | |
94 | + dest := reflect.New(reflect.TypeOf(tt.dest)) | |
95 | + values, err := redis.Scan(values, dest.Interface()) | |
96 | + if err != nil { | |
97 | + t.Errorf("Scan(%v) returned error %v", tt, err) | |
98 | + continue | |
99 | + } | |
100 | + if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) { | |
101 | + t.Errorf("Scan(%v) returned %v, want %v", tt, dest.Elem().Interface(), tt.dest) | |
102 | + } | |
103 | + } | |
104 | +} | |
105 | + | |
106 | +var scanConversionErrorTests = []struct { | |
107 | + src interface{} | |
108 | + dest interface{} | |
109 | +}{ | |
110 | + {[]byte("1234"), byte(0)}, | |
111 | + {int64(1234), byte(0)}, | |
112 | + {[]byte("-1"), byte(0)}, | |
113 | + {int64(-1), byte(0)}, | |
114 | + {[]byte("junk"), false}, | |
115 | + {redis.Error("blah"), false}, | |
116 | + {redis.Error("blah"), durationScan{Duration: time.Minute}}, | |
117 | + {"invalid", durationScan{Duration: time.Minute}}, | |
118 | +} | |
119 | + | |
120 | +func TestScanConversionError(t *testing.T) { | |
121 | + for _, tt := range scanConversionErrorTests { | |
122 | + values := []interface{}{tt.src} | |
123 | + dest := reflect.New(reflect.TypeOf(tt.dest)) | |
124 | + values, err := redis.Scan(values, dest.Interface()) | |
125 | + if err == nil { | |
126 | + t.Errorf("Scan(%v) did not return error", tt) | |
127 | + } | |
128 | + } | |
129 | +} | |
130 | + | |
131 | +func ExampleScan() { | |
132 | + c, err := dial() | |
133 | + if err != nil { | |
134 | + fmt.Println(err) | |
135 | + return | |
136 | + } | |
137 | + defer c.Close() | |
138 | + | |
139 | + c.Send("HMSET", "album:1", "title", "Red", "rating", 5) | |
140 | + c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1) | |
141 | + c.Send("HMSET", "album:3", "title", "Beat") | |
142 | + c.Send("LPUSH", "albums", "1") | |
143 | + c.Send("LPUSH", "albums", "2") | |
144 | + c.Send("LPUSH", "albums", "3") | |
145 | + values, err := redis.Values(c.Do("SORT", "albums", | |
146 | + "BY", "album:*->rating", | |
147 | + "GET", "album:*->title", | |
148 | + "GET", "album:*->rating")) | |
149 | + if err != nil { | |
150 | + fmt.Println(err) | |
151 | + return | |
152 | + } | |
153 | + | |
154 | + for len(values) > 0 { | |
155 | + var title string | |
156 | + rating := -1 // initialize to illegal value to detect nil. | |
157 | + values, err = redis.Scan(values, &title, &rating) | |
158 | + if err != nil { | |
159 | + fmt.Println(err) | |
160 | + return | |
161 | + } | |
162 | + if rating == -1 { | |
163 | + fmt.Println(title, "not-rated") | |
164 | + } else { | |
165 | + fmt.Println(title, rating) | |
166 | + } | |
167 | + } | |
168 | + // Output: | |
169 | + // Beat not-rated | |
170 | + // Earthbound 1 | |
171 | + // Red 5 | |
172 | +} | |
173 | + | |
174 | +type s0 struct { | |
175 | + X int | |
176 | + Y int `redis:"y"` | |
177 | + Bt bool | |
178 | +} | |
179 | + | |
180 | +type s1 struct { | |
181 | + X int `redis:"-"` | |
182 | + I int `redis:"i"` | |
183 | + U uint `redis:"u"` | |
184 | + S string `redis:"s"` | |
185 | + P []byte `redis:"p"` | |
186 | + B bool `redis:"b"` | |
187 | + Bt bool | |
188 | + Bf bool | |
189 | + s0 | |
190 | + Sd durationScan `redis:"sd"` | |
191 | + Sdp *durationScan `redis:"sdp"` | |
192 | +} | |
193 | + | |
194 | +var scanStructTests = []struct { | |
195 | + title string | |
196 | + reply []string | |
197 | + value interface{} | |
198 | +}{ | |
199 | + {"basic", | |
200 | + []string{ | |
201 | + "i", "-1234", | |
202 | + "u", "5678", | |
203 | + "s", "hello", | |
204 | + "p", "world", | |
205 | + "b", "t", | |
206 | + "Bt", "1", | |
207 | + "Bf", "0", | |
208 | + "X", "123", | |
209 | + "y", "456", | |
210 | + "sd", "1m", | |
211 | + "sdp", "1m", | |
212 | + }, | |
213 | + &s1{ | |
214 | + I: -1234, | |
215 | + U: 5678, | |
216 | + S: "hello", | |
217 | + P: []byte("world"), | |
218 | + B: true, | |
219 | + Bt: true, | |
220 | + Bf: false, | |
221 | + s0: s0{X: 123, Y: 456}, | |
222 | + Sd: durationScan{Duration: time.Minute}, | |
223 | + Sdp: &durationScan{Duration: time.Minute}, | |
224 | + }, | |
225 | + }, | |
226 | +} | |
227 | + | |
228 | +func TestScanStruct(t *testing.T) { | |
229 | + for _, tt := range scanStructTests { | |
230 | + | |
231 | + var reply []interface{} | |
232 | + for _, v := range tt.reply { | |
233 | + reply = append(reply, []byte(v)) | |
234 | + } | |
235 | + | |
236 | + value := reflect.New(reflect.ValueOf(tt.value).Type().Elem()) | |
237 | + | |
238 | + if err := redis.ScanStruct(reply, value.Interface()); err != nil { | |
239 | + t.Fatalf("ScanStruct(%s) returned error %v", tt.title, err) | |
240 | + } | |
241 | + | |
242 | + if !reflect.DeepEqual(value.Interface(), tt.value) { | |
243 | + t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value) | |
244 | + } | |
245 | + } | |
246 | +} | |
247 | + | |
248 | +func TestBadScanStructArgs(t *testing.T) { | |
249 | + x := []interface{}{"A", "b"} | |
250 | + test := func(v interface{}) { | |
251 | + if err := redis.ScanStruct(x, v); err == nil { | |
252 | + t.Errorf("Expect error for ScanStruct(%T, %T)", x, v) | |
253 | + } | |
254 | + } | |
255 | + | |
256 | + test(nil) | |
257 | + | |
258 | + var v0 *struct{} | |
259 | + test(v0) | |
260 | + | |
261 | + var v1 int | |
262 | + test(&v1) | |
263 | + | |
264 | + x = x[:1] | |
265 | + v2 := struct{ A string }{} | |
266 | + test(&v2) | |
267 | +} | |
268 | + | |
269 | +var scanSliceTests = []struct { | |
270 | + src []interface{} | |
271 | + fieldNames []string | |
272 | + ok bool | |
273 | + dest interface{} | |
274 | +}{ | |
275 | + { | |
276 | + []interface{}{[]byte("1"), nil, []byte("-1")}, | |
277 | + nil, | |
278 | + true, | |
279 | + []int{1, 0, -1}, | |
280 | + }, | |
281 | + { | |
282 | + []interface{}{[]byte("1"), nil, []byte("2")}, | |
283 | + nil, | |
284 | + true, | |
285 | + []uint{1, 0, 2}, | |
286 | + }, | |
287 | + { | |
288 | + []interface{}{[]byte("-1")}, | |
289 | + nil, | |
290 | + false, | |
291 | + []uint{1}, | |
292 | + }, | |
293 | + { | |
294 | + []interface{}{[]byte("hello"), nil, []byte("world")}, | |
295 | + nil, | |
296 | + true, | |
297 | + [][]byte{[]byte("hello"), nil, []byte("world")}, | |
298 | + }, | |
299 | + { | |
300 | + []interface{}{[]byte("hello"), nil, []byte("world")}, | |
301 | + nil, | |
302 | + true, | |
303 | + []string{"hello", "", "world"}, | |
304 | + }, | |
305 | + { | |
306 | + []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, | |
307 | + nil, | |
308 | + true, | |
309 | + []struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, | |
310 | + }, | |
311 | + { | |
312 | + []interface{}{[]byte("a1"), []byte("b1")}, | |
313 | + nil, | |
314 | + false, | |
315 | + []struct{ A, B, C string }{{"a1", "b1", ""}}, | |
316 | + }, | |
317 | + { | |
318 | + []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, | |
319 | + nil, | |
320 | + true, | |
321 | + []*struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, | |
322 | + }, | |
323 | + { | |
324 | + []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, | |
325 | + []string{"A", "B"}, | |
326 | + true, | |
327 | + []struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}}, | |
328 | + }, | |
329 | + { | |
330 | + []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, | |
331 | + nil, | |
332 | + false, | |
333 | + []struct{}{}, | |
334 | + }, | |
335 | +} | |
336 | + | |
337 | +func TestScanSlice(t *testing.T) { | |
338 | + for _, tt := range scanSliceTests { | |
339 | + | |
340 | + typ := reflect.ValueOf(tt.dest).Type() | |
341 | + dest := reflect.New(typ) | |
342 | + | |
343 | + err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...) | |
344 | + if tt.ok != (err == nil) { | |
345 | + t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err) | |
346 | + continue | |
347 | + } | |
348 | + if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) { | |
349 | + t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest) | |
350 | + } | |
351 | + } | |
352 | +} | |
353 | + | |
354 | +func ExampleScanSlice() { | |
355 | + c, err := dial() | |
356 | + if err != nil { | |
357 | + fmt.Println(err) | |
358 | + return | |
359 | + } | |
360 | + defer c.Close() | |
361 | + | |
362 | + c.Send("HMSET", "album:1", "title", "Red", "rating", 5) | |
363 | + c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1) | |
364 | + c.Send("HMSET", "album:3", "title", "Beat", "rating", 4) | |
365 | + c.Send("LPUSH", "albums", "1") | |
366 | + c.Send("LPUSH", "albums", "2") | |
367 | + c.Send("LPUSH", "albums", "3") | |
368 | + values, err := redis.Values(c.Do("SORT", "albums", | |
369 | + "BY", "album:*->rating", | |
370 | + "GET", "album:*->title", | |
371 | + "GET", "album:*->rating")) | |
372 | + if err != nil { | |
373 | + fmt.Println(err) | |
374 | + return | |
375 | + } | |
376 | + | |
377 | + var albums []struct { | |
378 | + Title string | |
379 | + Rating int | |
380 | + } | |
381 | + if err := redis.ScanSlice(values, &albums); err != nil { | |
382 | + fmt.Println(err) | |
383 | + return | |
384 | + } | |
385 | + fmt.Printf("%v\n", albums) | |
386 | + // Output: | |
387 | + // [{Earthbound 1} {Beat 4} {Red 5}] | |
388 | +} | |
389 | + | |
390 | +var argsTests = []struct { | |
391 | + title string | |
392 | + actual redis.Args | |
393 | + expected redis.Args | |
394 | +}{ | |
395 | + {"struct ptr", | |
396 | + redis.Args{}.AddFlat(&struct { | |
397 | + I int `redis:"i"` | |
398 | + U uint `redis:"u"` | |
399 | + S string `redis:"s"` | |
400 | + P []byte `redis:"p"` | |
401 | + M map[string]string `redis:"m"` | |
402 | + Bt bool | |
403 | + Bf bool | |
404 | + }{ | |
405 | + -1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false, | |
406 | + }), | |
407 | + redis.Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false}, | |
408 | + }, | |
409 | + {"struct", | |
410 | + redis.Args{}.AddFlat(struct{ I int }{123}), | |
411 | + redis.Args{"I", 123}, | |
412 | + }, | |
413 | + {"slice", | |
414 | + redis.Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2), | |
415 | + redis.Args{1, "a", "b", "c", 2}, | |
416 | + }, | |
417 | + {"struct omitempty", | |
418 | + redis.Args{}.AddFlat(&struct { | |
419 | + I int `redis:"i,omitempty"` | |
420 | + U uint `redis:"u,omitempty"` | |
421 | + S string `redis:"s,omitempty"` | |
422 | + P []byte `redis:"p,omitempty"` | |
423 | + M map[string]string `redis:"m,omitempty"` | |
424 | + Bt bool `redis:"Bt,omitempty"` | |
425 | + Bf bool `redis:"Bf,omitempty"` | |
426 | + }{ | |
427 | + 0, 0, "", []byte{}, map[string]string{}, true, false, | |
428 | + }), | |
429 | + redis.Args{"Bt", true}, | |
430 | + }, | |
431 | +} | |
432 | + | |
433 | +func TestArgs(t *testing.T) { | |
434 | + for _, tt := range argsTests { | |
435 | + if !reflect.DeepEqual(tt.actual, tt.expected) { | |
436 | + t.Fatalf("%s is %v, want %v", tt.title, tt.actual, tt.expected) | |
437 | + } | |
438 | + } | |
439 | +} | |
440 | + | |
441 | +func ExampleArgs() { | |
442 | + c, err := dial() | |
443 | + if err != nil { | |
444 | + fmt.Println(err) | |
445 | + return | |
446 | + } | |
447 | + defer c.Close() | |
448 | + | |
449 | + var p1, p2 struct { | |
450 | + Title string `redis:"title"` | |
451 | + Author string `redis:"author"` | |
452 | + Body string `redis:"body"` | |
453 | + } | |
454 | + | |
455 | + p1.Title = "Example" | |
456 | + p1.Author = "Gary" | |
457 | + p1.Body = "Hello" | |
458 | + | |
459 | + if _, err := c.Do("HMSET", redis.Args{}.Add("id1").AddFlat(&p1)...); err != nil { | |
460 | + fmt.Println(err) | |
461 | + return | |
462 | + } | |
463 | + | |
464 | + m := map[string]string{ | |
465 | + "title": "Example2", | |
466 | + "author": "Steve", | |
467 | + "body": "Map", | |
468 | + } | |
469 | + | |
470 | + if _, err := c.Do("HMSET", redis.Args{}.Add("id2").AddFlat(m)...); err != nil { | |
471 | + fmt.Println(err) | |
472 | + return | |
473 | + } | |
474 | + | |
475 | + for _, id := range []string{"id1", "id2"} { | |
476 | + | |
477 | + v, err := redis.Values(c.Do("HGETALL", id)) | |
478 | + if err != nil { | |
479 | + fmt.Println(err) | |
480 | + return | |
481 | + } | |
482 | + | |
483 | + if err := redis.ScanStruct(v, &p2); err != nil { | |
484 | + fmt.Println(err) | |
485 | + return | |
486 | + } | |
487 | + | |
488 | + fmt.Printf("%+v\n", p2) | |
489 | + } | |
490 | + | |
491 | + // Output: | |
492 | + // {Title:Example Author:Gary Body:Hello} | |
493 | + // {Title:Example2 Author:Steve Body:Map} | |
494 | +} | ... | ... |
... | ... | @@ -0,0 +1,91 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "crypto/sha1" | |
19 | + "encoding/hex" | |
20 | + "io" | |
21 | + "strings" | |
22 | +) | |
23 | + | |
24 | +// Script encapsulates the source, hash and key count for a Lua script. See | |
25 | +// http://redis.io/commands/eval for information on scripts in Redis. | |
26 | +type Script struct { | |
27 | + keyCount int | |
28 | + src string | |
29 | + hash string | |
30 | +} | |
31 | + | |
32 | +// NewScript returns a new script object. If keyCount is greater than or equal | |
33 | +// to zero, then the count is automatically inserted in the EVAL command | |
34 | +// argument list. If keyCount is less than zero, then the application supplies | |
35 | +// the count as the first value in the keysAndArgs argument to the Do, Send and | |
36 | +// SendHash methods. | |
37 | +func NewScript(keyCount int, src string) *Script { | |
38 | + h := sha1.New() | |
39 | + io.WriteString(h, src) | |
40 | + return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))} | |
41 | +} | |
42 | + | |
43 | +func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} { | |
44 | + var args []interface{} | |
45 | + if s.keyCount < 0 { | |
46 | + args = make([]interface{}, 1+len(keysAndArgs)) | |
47 | + args[0] = spec | |
48 | + copy(args[1:], keysAndArgs) | |
49 | + } else { | |
50 | + args = make([]interface{}, 2+len(keysAndArgs)) | |
51 | + args[0] = spec | |
52 | + args[1] = s.keyCount | |
53 | + copy(args[2:], keysAndArgs) | |
54 | + } | |
55 | + return args | |
56 | +} | |
57 | + | |
58 | +// Hash returns the script hash. | |
59 | +func (s *Script) Hash() string { | |
60 | + return s.hash | |
61 | +} | |
62 | + | |
63 | +// Do evaluates the script. Under the covers, Do optimistically evaluates the | |
64 | +// script using the EVALSHA command. If the command fails because the script is | |
65 | +// not loaded, then Do evaluates the script using the EVAL command (thus | |
66 | +// causing the script to load). | |
67 | +func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) { | |
68 | + v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...) | |
69 | + if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") { | |
70 | + v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...) | |
71 | + } | |
72 | + return v, err | |
73 | +} | |
74 | + | |
75 | +// SendHash evaluates the script without waiting for the reply. The script is | |
76 | +// evaluated with the EVALSHA command. The application must ensure that the | |
77 | +// script is loaded by a previous call to Send, Do or Load methods. | |
78 | +func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error { | |
79 | + return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...) | |
80 | +} | |
81 | + | |
82 | +// Send evaluates the script without waiting for the reply. | |
83 | +func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error { | |
84 | + return c.Send("EVAL", s.args(s.src, keysAndArgs)...) | |
85 | +} | |
86 | + | |
87 | +// Load loads the script without evaluating it. | |
88 | +func (s *Script) Load(c Conn) error { | |
89 | + _, err := c.Do("SCRIPT", "LOAD", s.src) | |
90 | + return err | |
91 | +} | ... | ... |
... | ... | @@ -0,0 +1,100 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "fmt" | |
19 | + "reflect" | |
20 | + "testing" | |
21 | + "time" | |
22 | + | |
23 | + "github.com/garyburd/redigo/redis" | |
24 | +) | |
25 | + | |
26 | +var ( | |
27 | + // These variables are declared at package level to remove distracting | |
28 | + // details from the examples. | |
29 | + c redis.Conn | |
30 | + reply interface{} | |
31 | + err error | |
32 | +) | |
33 | + | |
34 | +func ExampleScript() { | |
35 | + // Initialize a package-level variable with a script. | |
36 | + var getScript = redis.NewScript(1, `return redis.call('get', KEYS[1])`) | |
37 | + | |
38 | + // In a function, use the script Do method to evaluate the script. The Do | |
39 | + // method optimistically uses the EVALSHA command. If the script is not | |
40 | + // loaded, then the Do method falls back to the EVAL command. | |
41 | + reply, err = getScript.Do(c, "foo") | |
42 | +} | |
43 | + | |
44 | +func TestScript(t *testing.T) { | |
45 | + c, err := redis.DialDefaultServer() | |
46 | + if err != nil { | |
47 | + t.Fatalf("error connection to database, %v", err) | |
48 | + } | |
49 | + defer c.Close() | |
50 | + | |
51 | + // To test fall back in Do, we make script unique by adding comment with current time. | |
52 | + script := fmt.Sprintf("--%d\nreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", time.Now().UnixNano()) | |
53 | + s := redis.NewScript(2, script) | |
54 | + reply := []interface{}{[]byte("key1"), []byte("key2"), []byte("arg1"), []byte("arg2")} | |
55 | + | |
56 | + v, err := s.Do(c, "key1", "key2", "arg1", "arg2") | |
57 | + if err != nil { | |
58 | + t.Errorf("s.Do(c, ...) returned %v", err) | |
59 | + } | |
60 | + | |
61 | + if !reflect.DeepEqual(v, reply) { | |
62 | + t.Errorf("s.Do(c, ..); = %v, want %v", v, reply) | |
63 | + } | |
64 | + | |
65 | + err = s.Load(c) | |
66 | + if err != nil { | |
67 | + t.Errorf("s.Load(c) returned %v", err) | |
68 | + } | |
69 | + | |
70 | + err = s.SendHash(c, "key1", "key2", "arg1", "arg2") | |
71 | + if err != nil { | |
72 | + t.Errorf("s.SendHash(c, ...) returned %v", err) | |
73 | + } | |
74 | + | |
75 | + err = c.Flush() | |
76 | + if err != nil { | |
77 | + t.Errorf("c.Flush() returned %v", err) | |
78 | + } | |
79 | + | |
80 | + v, err = c.Receive() | |
81 | + if !reflect.DeepEqual(v, reply) { | |
82 | + t.Errorf("s.SendHash(c, ..); c.Receive() = %v, want %v", v, reply) | |
83 | + } | |
84 | + | |
85 | + err = s.Send(c, "key1", "key2", "arg1", "arg2") | |
86 | + if err != nil { | |
87 | + t.Errorf("s.Send(c, ...) returned %v", err) | |
88 | + } | |
89 | + | |
90 | + err = c.Flush() | |
91 | + if err != nil { | |
92 | + t.Errorf("c.Flush() returned %v", err) | |
93 | + } | |
94 | + | |
95 | + v, err = c.Receive() | |
96 | + if !reflect.DeepEqual(v, reply) { | |
97 | + t.Errorf("s.Send(c, ..); c.Receive() = %v, want %v", v, reply) | |
98 | + } | |
99 | + | |
100 | +} | ... | ... |
... | ... | @@ -0,0 +1,183 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis | |
16 | + | |
17 | +import ( | |
18 | + "bufio" | |
19 | + "errors" | |
20 | + "flag" | |
21 | + "fmt" | |
22 | + "io" | |
23 | + "io/ioutil" | |
24 | + "os" | |
25 | + "os/exec" | |
26 | + "strconv" | |
27 | + "strings" | |
28 | + "sync" | |
29 | + "testing" | |
30 | + "time" | |
31 | +) | |
32 | + | |
33 | +func SetNowFunc(f func() time.Time) { | |
34 | + nowFunc = f | |
35 | +} | |
36 | + | |
37 | +var ( | |
38 | + ErrNegativeInt = errNegativeInt | |
39 | + | |
40 | + serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary") | |
41 | + serverAddress = flag.String("redis-address", "127.0.0.1", "The address of the server") | |
42 | + serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers") | |
43 | + serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`") | |
44 | + serverLog = ioutil.Discard | |
45 | + | |
46 | + defaultServerMu sync.Mutex | |
47 | + defaultServer *Server | |
48 | + defaultServerErr error | |
49 | +) | |
50 | + | |
51 | +type Server struct { | |
52 | + name string | |
53 | + cmd *exec.Cmd | |
54 | + done chan struct{} | |
55 | +} | |
56 | + | |
57 | +func NewServer(name string, args ...string) (*Server, error) { | |
58 | + s := &Server{ | |
59 | + name: name, | |
60 | + cmd: exec.Command(*serverPath, args...), | |
61 | + done: make(chan struct{}), | |
62 | + } | |
63 | + | |
64 | + r, err := s.cmd.StdoutPipe() | |
65 | + if err != nil { | |
66 | + return nil, err | |
67 | + } | |
68 | + | |
69 | + err = s.cmd.Start() | |
70 | + if err != nil { | |
71 | + return nil, err | |
72 | + } | |
73 | + | |
74 | + ready := make(chan error, 1) | |
75 | + go s.watch(r, ready) | |
76 | + | |
77 | + select { | |
78 | + case err = <-ready: | |
79 | + case <-time.After(time.Second * 10): | |
80 | + err = errors.New("timeout waiting for server to start") | |
81 | + } | |
82 | + | |
83 | + if err != nil { | |
84 | + s.Stop() | |
85 | + return nil, err | |
86 | + } | |
87 | + | |
88 | + return s, nil | |
89 | +} | |
90 | + | |
91 | +func (s *Server) watch(r io.Reader, ready chan error) { | |
92 | + fmt.Fprintf(serverLog, "%d START %s \n", s.cmd.Process.Pid, s.name) | |
93 | + var listening bool | |
94 | + var text string | |
95 | + scn := bufio.NewScanner(r) | |
96 | + for scn.Scan() { | |
97 | + text = scn.Text() | |
98 | + fmt.Fprintf(serverLog, "%s\n", text) | |
99 | + if !listening { | |
100 | + if strings.Contains(text, " * Ready to accept connections") || | |
101 | + strings.Contains(text, " * The server is now ready to accept connections on port") { | |
102 | + listening = true | |
103 | + ready <- nil | |
104 | + } | |
105 | + } | |
106 | + } | |
107 | + if !listening { | |
108 | + ready <- fmt.Errorf("server exited: %s", text) | |
109 | + } | |
110 | + s.cmd.Wait() | |
111 | + fmt.Fprintf(serverLog, "%d STOP %s \n", s.cmd.Process.Pid, s.name) | |
112 | + close(s.done) | |
113 | +} | |
114 | + | |
115 | +func (s *Server) Stop() { | |
116 | + s.cmd.Process.Signal(os.Interrupt) | |
117 | + <-s.done | |
118 | +} | |
119 | + | |
120 | +// stopDefaultServer stops the server created by DialDefaultServer. | |
121 | +func stopDefaultServer() { | |
122 | + defaultServerMu.Lock() | |
123 | + defer defaultServerMu.Unlock() | |
124 | + if defaultServer != nil { | |
125 | + defaultServer.Stop() | |
126 | + defaultServer = nil | |
127 | + } | |
128 | +} | |
129 | + | |
130 | +// DefaultServerAddr starts the test server if not already started and returns | |
131 | +// the address of that server. | |
132 | +func DefaultServerAddr() (string, error) { | |
133 | + defaultServerMu.Lock() | |
134 | + defer defaultServerMu.Unlock() | |
135 | + addr := fmt.Sprintf("%v:%d", *serverAddress, *serverBasePort) | |
136 | + if defaultServer != nil || defaultServerErr != nil { | |
137 | + return addr, defaultServerErr | |
138 | + } | |
139 | + defaultServer, defaultServerErr = NewServer( | |
140 | + "default", | |
141 | + "--port", strconv.Itoa(*serverBasePort), | |
142 | + "--bind", *serverAddress, | |
143 | + "--save", "", | |
144 | + "--appendonly", "no") | |
145 | + return addr, defaultServerErr | |
146 | +} | |
147 | + | |
148 | +// DialDefaultServer starts the test server if not already started and dials a | |
149 | +// connection to the server. | |
150 | +func DialDefaultServer() (Conn, error) { | |
151 | + addr, err := DefaultServerAddr() | |
152 | + if err != nil { | |
153 | + return nil, err | |
154 | + } | |
155 | + c, err := Dial("tcp", addr, DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second)) | |
156 | + if err != nil { | |
157 | + return nil, err | |
158 | + } | |
159 | + c.Do("FLUSHDB") | |
160 | + return c, nil | |
161 | +} | |
162 | + | |
163 | +func TestMain(m *testing.M) { | |
164 | + os.Exit(func() int { | |
165 | + flag.Parse() | |
166 | + | |
167 | + var f *os.File | |
168 | + if *serverLogName != "" { | |
169 | + var err error | |
170 | + f, err = os.OpenFile(*serverLogName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) | |
171 | + if err != nil { | |
172 | + fmt.Fprintf(os.Stderr, "Error opening redis-log: %v\n", err) | |
173 | + return 1 | |
174 | + } | |
175 | + defer f.Close() | |
176 | + serverLog = f | |
177 | + } | |
178 | + | |
179 | + defer stopDefaultServer() | |
180 | + | |
181 | + return m.Run() | |
182 | + }()) | |
183 | +} | ... | ... |
src/github.com/garyburd/redigo/redis/zpop_example_test.go
0 → 100644
... | ... | @@ -0,0 +1,114 @@ |
1 | +// Copyright 2013 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redis_test | |
16 | + | |
17 | +import ( | |
18 | + "fmt" | |
19 | + | |
20 | + "github.com/garyburd/redigo/redis" | |
21 | +) | |
22 | + | |
23 | +// zpop pops a value from the ZSET key using WATCH/MULTI/EXEC commands. | |
24 | +func zpop(c redis.Conn, key string) (result string, err error) { | |
25 | + | |
26 | + defer func() { | |
27 | + // Return connection to normal state on error. | |
28 | + if err != nil { | |
29 | + c.Do("DISCARD") | |
30 | + } | |
31 | + }() | |
32 | + | |
33 | + // Loop until transaction is successful. | |
34 | + for { | |
35 | + if _, err := c.Do("WATCH", key); err != nil { | |
36 | + return "", err | |
37 | + } | |
38 | + | |
39 | + members, err := redis.Strings(c.Do("ZRANGE", key, 0, 0)) | |
40 | + if err != nil { | |
41 | + return "", err | |
42 | + } | |
43 | + if len(members) != 1 { | |
44 | + return "", redis.ErrNil | |
45 | + } | |
46 | + | |
47 | + c.Send("MULTI") | |
48 | + c.Send("ZREM", key, members[0]) | |
49 | + queued, err := c.Do("EXEC") | |
50 | + if err != nil { | |
51 | + return "", err | |
52 | + } | |
53 | + | |
54 | + if queued != nil { | |
55 | + result = members[0] | |
56 | + break | |
57 | + } | |
58 | + } | |
59 | + | |
60 | + return result, nil | |
61 | +} | |
62 | + | |
63 | +// zpopScript pops a value from a ZSET. | |
64 | +var zpopScript = redis.NewScript(1, ` | |
65 | + local r = redis.call('ZRANGE', KEYS[1], 0, 0) | |
66 | + if r ~= nil then | |
67 | + r = r[1] | |
68 | + redis.call('ZREM', KEYS[1], r) | |
69 | + end | |
70 | + return r | |
71 | +`) | |
72 | + | |
73 | +// This example implements ZPOP as described at | |
74 | +// http://redis.io/topics/transactions using WATCH/MULTI/EXEC and scripting. | |
75 | +func Example_zpop() { | |
76 | + c, err := dial() | |
77 | + if err != nil { | |
78 | + fmt.Println(err) | |
79 | + return | |
80 | + } | |
81 | + defer c.Close() | |
82 | + | |
83 | + // Add test data using a pipeline. | |
84 | + | |
85 | + for i, member := range []string{"red", "blue", "green"} { | |
86 | + c.Send("ZADD", "zset", i, member) | |
87 | + } | |
88 | + if _, err := c.Do(""); err != nil { | |
89 | + fmt.Println(err) | |
90 | + return | |
91 | + } | |
92 | + | |
93 | + // Pop using WATCH/MULTI/EXEC | |
94 | + | |
95 | + v, err := zpop(c, "zset") | |
96 | + if err != nil { | |
97 | + fmt.Println(err) | |
98 | + return | |
99 | + } | |
100 | + fmt.Println(v) | |
101 | + | |
102 | + // Pop using a script. | |
103 | + | |
104 | + v, err = redis.String(zpopScript.Do(c, "zset")) | |
105 | + if err != nil { | |
106 | + fmt.Println(err) | |
107 | + return | |
108 | + } | |
109 | + fmt.Println(v) | |
110 | + | |
111 | + // Output: | |
112 | + // red | |
113 | + // blue | |
114 | +} | ... | ... |
... | ... | @@ -0,0 +1,152 @@ |
1 | +// Copyright 2014 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redisx | |
16 | + | |
17 | +import ( | |
18 | + "errors" | |
19 | + "sync" | |
20 | + | |
21 | + "github.com/garyburd/redigo/internal" | |
22 | + "github.com/garyburd/redigo/redis" | |
23 | +) | |
24 | + | |
25 | +// ConnMux multiplexes one or more connections to a single underlying | |
26 | +// connection. The ConnMux connections do not support concurrency, commands | |
27 | +// that associate server side state with the connection or commands that put | |
28 | +// the connection in a special mode. | |
29 | +type ConnMux struct { | |
30 | + c redis.Conn | |
31 | + | |
32 | + sendMu sync.Mutex | |
33 | + sendID uint | |
34 | + | |
35 | + recvMu sync.Mutex | |
36 | + recvID uint | |
37 | + recvWait map[uint]chan struct{} | |
38 | +} | |
39 | + | |
40 | +func NewConnMux(c redis.Conn) *ConnMux { | |
41 | + return &ConnMux{c: c, recvWait: make(map[uint]chan struct{})} | |
42 | +} | |
43 | + | |
44 | +// Get gets a connection. The application must close the returned connection. | |
45 | +func (p *ConnMux) Get() redis.Conn { | |
46 | + c := &muxConn{p: p} | |
47 | + c.ids = c.buf[:0] | |
48 | + return c | |
49 | +} | |
50 | + | |
51 | +// Close closes the underlying connection. | |
52 | +func (p *ConnMux) Close() error { | |
53 | + return p.c.Close() | |
54 | +} | |
55 | + | |
56 | +type muxConn struct { | |
57 | + p *ConnMux | |
58 | + ids []uint | |
59 | + buf [8]uint | |
60 | +} | |
61 | + | |
62 | +func (c *muxConn) send(flush bool, cmd string, args ...interface{}) error { | |
63 | + if internal.LookupCommandInfo(cmd).Set != 0 { | |
64 | + return errors.New("command not supported by mux pool") | |
65 | + } | |
66 | + p := c.p | |
67 | + p.sendMu.Lock() | |
68 | + id := p.sendID | |
69 | + c.ids = append(c.ids, id) | |
70 | + p.sendID++ | |
71 | + err := p.c.Send(cmd, args...) | |
72 | + if flush { | |
73 | + err = p.c.Flush() | |
74 | + } | |
75 | + p.sendMu.Unlock() | |
76 | + return err | |
77 | +} | |
78 | + | |
79 | +func (c *muxConn) Send(cmd string, args ...interface{}) error { | |
80 | + return c.send(false, cmd, args...) | |
81 | +} | |
82 | + | |
83 | +func (c *muxConn) Flush() error { | |
84 | + p := c.p | |
85 | + p.sendMu.Lock() | |
86 | + err := p.c.Flush() | |
87 | + p.sendMu.Unlock() | |
88 | + return err | |
89 | +} | |
90 | + | |
91 | +func (c *muxConn) Receive() (interface{}, error) { | |
92 | + if len(c.ids) == 0 { | |
93 | + return nil, errors.New("mux pool underflow") | |
94 | + } | |
95 | + | |
96 | + id := c.ids[0] | |
97 | + c.ids = c.ids[1:] | |
98 | + if len(c.ids) == 0 { | |
99 | + c.ids = c.buf[:0] | |
100 | + } | |
101 | + | |
102 | + p := c.p | |
103 | + p.recvMu.Lock() | |
104 | + if p.recvID != id { | |
105 | + ch := make(chan struct{}) | |
106 | + p.recvWait[id] = ch | |
107 | + p.recvMu.Unlock() | |
108 | + <-ch | |
109 | + p.recvMu.Lock() | |
110 | + if p.recvID != id { | |
111 | + panic("out of sync") | |
112 | + } | |
113 | + } | |
114 | + | |
115 | + v, err := p.c.Receive() | |
116 | + | |
117 | + id++ | |
118 | + p.recvID = id | |
119 | + ch, ok := p.recvWait[id] | |
120 | + if ok { | |
121 | + delete(p.recvWait, id) | |
122 | + } | |
123 | + p.recvMu.Unlock() | |
124 | + if ok { | |
125 | + ch <- struct{}{} | |
126 | + } | |
127 | + | |
128 | + return v, err | |
129 | +} | |
130 | + | |
131 | +func (c *muxConn) Close() error { | |
132 | + var err error | |
133 | + if len(c.ids) == 0 { | |
134 | + return nil | |
135 | + } | |
136 | + c.Flush() | |
137 | + for _ = range c.ids { | |
138 | + _, err = c.Receive() | |
139 | + } | |
140 | + return err | |
141 | +} | |
142 | + | |
143 | +func (c *muxConn) Do(cmd string, args ...interface{}) (interface{}, error) { | |
144 | + if err := c.send(true, cmd, args...); err != nil { | |
145 | + return nil, err | |
146 | + } | |
147 | + return c.Receive() | |
148 | +} | |
149 | + | |
150 | +func (c *muxConn) Err() error { | |
151 | + return c.p.c.Err() | |
152 | +} | ... | ... |
... | ... | @@ -0,0 +1,259 @@ |
1 | +// Copyright 2014 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +package redisx_test | |
16 | + | |
17 | +import ( | |
18 | + "net/textproto" | |
19 | + "sync" | |
20 | + "testing" | |
21 | + | |
22 | + "github.com/garyburd/redigo/internal/redistest" | |
23 | + "github.com/garyburd/redigo/redis" | |
24 | + "github.com/garyburd/redigo/redisx" | |
25 | +) | |
26 | + | |
27 | +func TestConnMux(t *testing.T) { | |
28 | + c, err := redistest.Dial() | |
29 | + if err != nil { | |
30 | + t.Fatalf("error connection to database, %v", err) | |
31 | + } | |
32 | + m := redisx.NewConnMux(c) | |
33 | + defer m.Close() | |
34 | + | |
35 | + c1 := m.Get() | |
36 | + c2 := m.Get() | |
37 | + c1.Send("ECHO", "hello") | |
38 | + c2.Send("ECHO", "world") | |
39 | + c1.Flush() | |
40 | + c2.Flush() | |
41 | + s, err := redis.String(c1.Receive()) | |
42 | + if err != nil { | |
43 | + t.Fatal(err) | |
44 | + } | |
45 | + if s != "hello" { | |
46 | + t.Fatalf("echo returned %q, want %q", s, "hello") | |
47 | + } | |
48 | + s, err = redis.String(c2.Receive()) | |
49 | + if err != nil { | |
50 | + t.Fatal(err) | |
51 | + } | |
52 | + if s != "world" { | |
53 | + t.Fatalf("echo returned %q, want %q", s, "world") | |
54 | + } | |
55 | + c1.Close() | |
56 | + c2.Close() | |
57 | +} | |
58 | + | |
59 | +func TestConnMuxClose(t *testing.T) { | |
60 | + c, err := redistest.Dial() | |
61 | + if err != nil { | |
62 | + t.Fatalf("error connection to database, %v", err) | |
63 | + } | |
64 | + m := redisx.NewConnMux(c) | |
65 | + defer m.Close() | |
66 | + | |
67 | + c1 := m.Get() | |
68 | + c2 := m.Get() | |
69 | + | |
70 | + if err := c1.Send("ECHO", "hello"); err != nil { | |
71 | + t.Fatal(err) | |
72 | + } | |
73 | + if err := c1.Close(); err != nil { | |
74 | + t.Fatal(err) | |
75 | + } | |
76 | + | |
77 | + if err := c2.Send("ECHO", "world"); err != nil { | |
78 | + t.Fatal(err) | |
79 | + } | |
80 | + if err := c2.Flush(); err != nil { | |
81 | + t.Fatal(err) | |
82 | + } | |
83 | + | |
84 | + s, err := redis.String(c2.Receive()) | |
85 | + if err != nil { | |
86 | + t.Fatal(err) | |
87 | + } | |
88 | + if s != "world" { | |
89 | + t.Fatalf("echo returned %q, want %q", s, "world") | |
90 | + } | |
91 | + c2.Close() | |
92 | +} | |
93 | + | |
94 | +func BenchmarkConn(b *testing.B) { | |
95 | + b.StopTimer() | |
96 | + c, err := redistest.Dial() | |
97 | + if err != nil { | |
98 | + b.Fatalf("error connection to database, %v", err) | |
99 | + } | |
100 | + defer c.Close() | |
101 | + b.StartTimer() | |
102 | + | |
103 | + for i := 0; i < b.N; i++ { | |
104 | + if _, err := c.Do("PING"); err != nil { | |
105 | + b.Fatal(err) | |
106 | + } | |
107 | + } | |
108 | +} | |
109 | + | |
110 | +func BenchmarkConnMux(b *testing.B) { | |
111 | + b.StopTimer() | |
112 | + c, err := redistest.Dial() | |
113 | + if err != nil { | |
114 | + b.Fatalf("error connection to database, %v", err) | |
115 | + } | |
116 | + m := redisx.NewConnMux(c) | |
117 | + defer m.Close() | |
118 | + | |
119 | + b.StartTimer() | |
120 | + | |
121 | + for i := 0; i < b.N; i++ { | |
122 | + c := m.Get() | |
123 | + if _, err := c.Do("PING"); err != nil { | |
124 | + b.Fatal(err) | |
125 | + } | |
126 | + c.Close() | |
127 | + } | |
128 | +} | |
129 | + | |
130 | +func BenchmarkPool(b *testing.B) { | |
131 | + b.StopTimer() | |
132 | + | |
133 | + p := redis.Pool{Dial: redistest.Dial, MaxIdle: 1} | |
134 | + defer p.Close() | |
135 | + | |
136 | + // Fill the pool. | |
137 | + c := p.Get() | |
138 | + if err := c.Err(); err != nil { | |
139 | + b.Fatal(err) | |
140 | + } | |
141 | + c.Close() | |
142 | + | |
143 | + b.StartTimer() | |
144 | + | |
145 | + for i := 0; i < b.N; i++ { | |
146 | + c := p.Get() | |
147 | + if _, err := c.Do("PING"); err != nil { | |
148 | + b.Fatal(err) | |
149 | + } | |
150 | + c.Close() | |
151 | + } | |
152 | +} | |
153 | + | |
154 | +const numConcurrent = 10 | |
155 | + | |
156 | +func BenchmarkConnMuxConcurrent(b *testing.B) { | |
157 | + b.StopTimer() | |
158 | + c, err := redistest.Dial() | |
159 | + if err != nil { | |
160 | + b.Fatalf("error connection to database, %v", err) | |
161 | + } | |
162 | + defer c.Close() | |
163 | + | |
164 | + m := redisx.NewConnMux(c) | |
165 | + | |
166 | + var wg sync.WaitGroup | |
167 | + wg.Add(numConcurrent) | |
168 | + | |
169 | + b.StartTimer() | |
170 | + | |
171 | + for i := 0; i < numConcurrent; i++ { | |
172 | + go func() { | |
173 | + defer wg.Done() | |
174 | + for i := 0; i < b.N; i++ { | |
175 | + c := m.Get() | |
176 | + if _, err := c.Do("PING"); err != nil { | |
177 | + b.Fatal(err) | |
178 | + } | |
179 | + c.Close() | |
180 | + } | |
181 | + }() | |
182 | + } | |
183 | + wg.Wait() | |
184 | +} | |
185 | + | |
186 | +func BenchmarkPoolConcurrent(b *testing.B) { | |
187 | + b.StopTimer() | |
188 | + | |
189 | + p := redis.Pool{Dial: redistest.Dial, MaxIdle: numConcurrent} | |
190 | + defer p.Close() | |
191 | + | |
192 | + // Fill the pool. | |
193 | + conns := make([]redis.Conn, numConcurrent) | |
194 | + for i := range conns { | |
195 | + c := p.Get() | |
196 | + if err := c.Err(); err != nil { | |
197 | + b.Fatal(err) | |
198 | + } | |
199 | + conns[i] = c | |
200 | + } | |
201 | + for _, c := range conns { | |
202 | + c.Close() | |
203 | + } | |
204 | + | |
205 | + var wg sync.WaitGroup | |
206 | + wg.Add(numConcurrent) | |
207 | + | |
208 | + b.StartTimer() | |
209 | + | |
210 | + for i := 0; i < numConcurrent; i++ { | |
211 | + go func() { | |
212 | + defer wg.Done() | |
213 | + for i := 0; i < b.N; i++ { | |
214 | + c := p.Get() | |
215 | + if _, err := c.Do("PING"); err != nil { | |
216 | + b.Fatal(err) | |
217 | + } | |
218 | + c.Close() | |
219 | + } | |
220 | + }() | |
221 | + } | |
222 | + wg.Wait() | |
223 | +} | |
224 | + | |
225 | +func BenchmarkPipelineConcurrency(b *testing.B) { | |
226 | + b.StopTimer() | |
227 | + c, err := redistest.Dial() | |
228 | + if err != nil { | |
229 | + b.Fatalf("error connection to database, %v", err) | |
230 | + } | |
231 | + defer c.Close() | |
232 | + | |
233 | + var wg sync.WaitGroup | |
234 | + wg.Add(numConcurrent) | |
235 | + | |
236 | + var pipeline textproto.Pipeline | |
237 | + | |
238 | + b.StartTimer() | |
239 | + | |
240 | + for i := 0; i < numConcurrent; i++ { | |
241 | + go func() { | |
242 | + defer wg.Done() | |
243 | + for i := 0; i < b.N; i++ { | |
244 | + id := pipeline.Next() | |
245 | + pipeline.StartRequest(id) | |
246 | + c.Send("PING") | |
247 | + c.Flush() | |
248 | + pipeline.EndRequest(id) | |
249 | + pipeline.StartResponse(id) | |
250 | + _, err := c.Receive() | |
251 | + if err != nil { | |
252 | + b.Fatal(err) | |
253 | + } | |
254 | + pipeline.EndResponse(id) | |
255 | + } | |
256 | + }() | |
257 | + } | |
258 | + wg.Wait() | |
259 | +} | ... | ... |
... | ... | @@ -0,0 +1,17 @@ |
1 | +// Copyright 2012 Gary Burd | |
2 | +// | |
3 | +// Licensed under the Apache License, Version 2.0 (the "License"): you may | |
4 | +// not use this file except in compliance with the License. You may obtain | |
5 | +// a copy of the License at | |
6 | +// | |
7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
8 | +// | |
9 | +// Unless required by applicable law or agreed to in writing, software | |
10 | +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
11 | +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
12 | +// License for the specific language governing permissions and limitations | |
13 | +// under the License. | |
14 | + | |
15 | +// Package redisx contains experimental features for Redigo. Features in this | |
16 | +// package may be modified or deleted at any time. | |
17 | +package redisx // import "github.com/garyburd/redigo/redisx" | ... | ... |
test.txt