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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -0,0 +1 @@ | ||
1 | +Ask questions at https://stackoverflow.com/questions/ask?tags=go+redis |
@@ -0,0 +1,19 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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 @@ | @@ -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