初始化项目
This commit is contained in:
2
trunk/goutil/coroutine-timer/.gitignore
vendored
Normal file
2
trunk/goutil/coroutine-timer/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Log/*
|
||||
logs/*
|
||||
47
trunk/goutil/coroutine-timer/cmd.go
Normal file
47
trunk/goutil/coroutine-timer/cmd.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package coroutine_timer
|
||||
|
||||
const (
|
||||
// 添加
|
||||
cmd_add = 1
|
||||
|
||||
// 删除
|
||||
cmd_del = 2
|
||||
)
|
||||
|
||||
// cmdModel
|
||||
// @description: 命令对象
|
||||
type cmdModel struct {
|
||||
// cmd 指令
|
||||
cmd int
|
||||
|
||||
// paramObj 指令参数
|
||||
paramObj interface{}
|
||||
|
||||
// resObj 指令返回对象
|
||||
resObj interface{}
|
||||
|
||||
// err 指令返回的错误
|
||||
err error
|
||||
|
||||
// waitChan 等待channel
|
||||
waitChan chan struct{}
|
||||
}
|
||||
|
||||
// newCmdModel
|
||||
// @description: 创建cmd模型对象
|
||||
// parameter:
|
||||
// @c:cmd命令
|
||||
// @po:参数
|
||||
// return:
|
||||
// @*cmdModel:
|
||||
func newCmdModel(c int, po interface{}) *cmdModel {
|
||||
result := &cmdModel{
|
||||
cmd: c,
|
||||
paramObj: po,
|
||||
resObj: nil,
|
||||
err: nil,
|
||||
waitChan: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
73
trunk/goutil/coroutine-timer/model.go
Normal file
73
trunk/goutil/coroutine-timer/model.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package coroutine_timer
|
||||
|
||||
// timersModel
|
||||
// @description: timer卡槽对象
|
||||
type timersModel struct {
|
||||
timers map[string]*timerObj
|
||||
}
|
||||
|
||||
// newTimersModel
|
||||
// @description: 构造timer卡槽对象
|
||||
// parameter:
|
||||
// return:
|
||||
// @*timersModel:
|
||||
func newTimersModel() *timersModel {
|
||||
return &timersModel{timers: map[string]*timerObj{}}
|
||||
}
|
||||
|
||||
// addTimer
|
||||
// @description: 添加定时器
|
||||
// parameter:
|
||||
// @receiver this:
|
||||
// @t:
|
||||
// return:
|
||||
func (this *timersModel) addTimer(t *timerObj) {
|
||||
this.timers[t.id] = t
|
||||
}
|
||||
|
||||
// delTimer
|
||||
// @description: 删除定时器
|
||||
// parameter:
|
||||
// @receiver this:
|
||||
// @id:
|
||||
// return:
|
||||
func (this *timersModel) delTimer(id string) {
|
||||
delete(this.timers, id)
|
||||
}
|
||||
|
||||
// exist
|
||||
// @description: 判断id是否存在
|
||||
// parameter:
|
||||
// @receiver this:
|
||||
// @id:
|
||||
// return:
|
||||
// @exist:
|
||||
func (this *timersModel) exist(id string) (exist bool) {
|
||||
_, exist = this.timers[id]
|
||||
return
|
||||
}
|
||||
|
||||
// getAllTimers
|
||||
// @description: 获取所有定时器
|
||||
// parameter:
|
||||
// @receiver this:
|
||||
// return:
|
||||
// @map[string]*timerObj:
|
||||
func (this *timersModel) getAllTimers() map[string]*timerObj {
|
||||
return this.timers
|
||||
}
|
||||
|
||||
// getAllTimers2
|
||||
// @description: 获取所有定时器
|
||||
// parameter:
|
||||
// @receiver this:
|
||||
// return:
|
||||
// @result:
|
||||
func (this *timersModel) getAllTimers2() (result []*timerObj) {
|
||||
result = make([]*timerObj, 0, len(this.timers))
|
||||
for _, v := range this.timers {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
20
trunk/goutil/coroutine-timer/readme.md
Normal file
20
trunk/goutil/coroutine-timer/readme.md
Normal file
@@ -0,0 +1,20 @@
|
||||
coroutine-timer支持如下工作:
|
||||
定时触发设定的回调,最小精度秒级
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 增加回调
|
||||
|
||||
> 1. 导入包
|
||||
> 2. 调用AddTimerx添加定时回调,传入相关参数
|
||||
|
||||
ps:
|
||||
> 1. AddTimer1,AddTimer2,AddTimer3是内部自动生成的id,内部保证唯一性。外部如果后续要删除该添加的timer,需要持有返回的id信息
|
||||
> 2. AddTimer4 需要外部传入id,外部需要保证id的唯一性。并且这个方法会在内部校验id是否已经存在,所以性能上会比其他AddTimer方法慢
|
||||
|
||||
### 删除回调
|
||||
|
||||
```go
|
||||
DeleteTimer(id)
|
||||
```
|
||||
|
||||
409
trunk/goutil/coroutine-timer/timer-mgr.go
Normal file
409
trunk/goutil/coroutine-timer/timer-mgr.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package coroutine_timer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"goutil/logUtil"
|
||||
"goutil/stringUtil"
|
||||
)
|
||||
|
||||
const (
|
||||
// 启动暂停时间
|
||||
con_STAR_SLEEP_NUM = 3
|
||||
|
||||
// 秒级定时器卡槽数量
|
||||
con_SECOND_SLOT_NUM = 60
|
||||
|
||||
//分钟级定时器卡槽数量
|
||||
con_MINUTES_SLOT_NUM = 60
|
||||
)
|
||||
|
||||
var (
|
||||
// 秒级定时器下标
|
||||
secIndex = 0
|
||||
|
||||
// 秒级定时器当前开始时间
|
||||
secondStarTime int64
|
||||
|
||||
// 秒级定时器槽
|
||||
secondsTimers [con_SECOND_SLOT_NUM]*timersModel
|
||||
|
||||
// 分钟级定时器下标
|
||||
minIndex = 0
|
||||
|
||||
// 分钟级定时器当前开始时间
|
||||
minStarTime int64
|
||||
|
||||
// 分钟级定时器槽
|
||||
minutesTimers [con_MINUTES_SLOT_NUM]*timersModel
|
||||
|
||||
// 其他定时器存放槽
|
||||
otherTimers *timersModel
|
||||
|
||||
// 操作通道
|
||||
cmdChan chan *cmdModel
|
||||
)
|
||||
|
||||
func init() {
|
||||
for i := 0; i < con_SECOND_SLOT_NUM; i++ {
|
||||
secondsTimers[i] = newTimersModel()
|
||||
}
|
||||
for i := 0; i < con_MINUTES_SLOT_NUM; i++ {
|
||||
minutesTimers[i] = newTimersModel()
|
||||
}
|
||||
|
||||
otherTimers = newTimersModel()
|
||||
cmdChan = make(chan *cmdModel, 1000)
|
||||
secondStarTime = time.Now().Unix()
|
||||
minStarTime = secondStarTime + con_SECOND_SLOT_NUM
|
||||
|
||||
go chanHandler()
|
||||
}
|
||||
|
||||
// AddTimer
|
||||
// @description: 添加定时回调
|
||||
// parameter:
|
||||
//
|
||||
// @afterSecond:延后多少时间执行
|
||||
// @exfun:执行方法
|
||||
// @obj:执行传入的参数
|
||||
//
|
||||
// return:
|
||||
//
|
||||
// @string:
|
||||
func AddTimer(afterSecond int, exfun func(interface{}), obj interface{}) string {
|
||||
tick := time.Now().Unix() + int64(afterSecond)
|
||||
return AddTimer3(tick, exfun, obj)
|
||||
}
|
||||
|
||||
// AddTimer2
|
||||
// @description: 添加定时回调
|
||||
// parameter:
|
||||
//
|
||||
// @t:执行时间点
|
||||
// @exfun:执行方法
|
||||
// @obj:执行传入的参数
|
||||
//
|
||||
// return:
|
||||
//
|
||||
// @string:
|
||||
func AddTimer2(t time.Time, exfun func(interface{}), obj interface{}) string {
|
||||
tick := t.Unix()
|
||||
return AddTimer3(tick, exfun, obj)
|
||||
}
|
||||
|
||||
// AddTimer3
|
||||
// @description: 添加定时回调
|
||||
// parameter:
|
||||
//
|
||||
// @tick:执行时间点
|
||||
// @exfun:执行方法
|
||||
// @obj:执行传入的参数
|
||||
//
|
||||
// return:
|
||||
//
|
||||
// @newId:
|
||||
func AddTimer3(tick int64, exfun func(interface{}), obj interface{}) (newId string) {
|
||||
newId = stringUtil.GetNewUUID()
|
||||
newObj := newTimerObj(newId, tick, exfun, obj)
|
||||
|
||||
cnm := newCmdModel(cmd_add, newObj)
|
||||
cmdChan <- cnm
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddTimer4
|
||||
// @description: 添加定时回调(此方法会在内部校验id,所以性能会比其他AddTimer方法低)
|
||||
// parameter:
|
||||
//
|
||||
// @id:定时id(外部需要自行保证id唯一)
|
||||
// @tick:执行时间点
|
||||
// @exfun:执行方法
|
||||
// @obj:执行传入的参数
|
||||
//
|
||||
// return:
|
||||
//
|
||||
// @err:
|
||||
func AddTimer4(id string, tick int64, exfun func(interface{}), obj interface{}) (err error) {
|
||||
newObj := newTimerObj(id, tick, exfun, obj)
|
||||
newObj.needCheckId = true
|
||||
|
||||
// 加入处理队列
|
||||
cnm := newCmdModel(cmd_add, newObj)
|
||||
cmdChan <- cnm
|
||||
|
||||
// 等待处理结束
|
||||
<-cnm.waitChan
|
||||
|
||||
// 返回处理结果
|
||||
err = cnm.err
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteTimer
|
||||
// @description: 删除定时器
|
||||
// parameter:
|
||||
//
|
||||
// @id:
|
||||
//
|
||||
// return:
|
||||
func DeleteTimer(id string) {
|
||||
cnm := newCmdModel(cmd_del, id)
|
||||
cmdChan <- cnm
|
||||
}
|
||||
|
||||
// chanHandler
|
||||
// @description: channel处理
|
||||
// parameter:
|
||||
// return:
|
||||
func chanHandler() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logUtil.ErrorLog("coroutine-timer.excute err:%s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 暂停一下再处理,避免启动立即处理,其他数据还没准备好
|
||||
time.Sleep(con_STAR_SLEEP_NUM * time.Second)
|
||||
|
||||
at := time.After(time.Second * 1)
|
||||
for {
|
||||
select {
|
||||
case cm := <-cmdChan:
|
||||
switch cm.cmd {
|
||||
case cmd_add:
|
||||
cmdAdd(cm)
|
||||
case cmd_del:
|
||||
cmdDel(cm)
|
||||
}
|
||||
case <-at:
|
||||
// byron:需要处理时间后调导致跳时间的问题:调整后应该马上执行的
|
||||
|
||||
// 计算需要执行的次数
|
||||
n := time.Now().Unix() - secondStarTime - int64(secIndex)
|
||||
if n > 0 {
|
||||
|
||||
// 执行对应次数的方法 --- 正常应该只执行1此,调时间后,此处会追时间
|
||||
var i int64
|
||||
for i = 0; i < n; i++ {
|
||||
cmdRun()
|
||||
}
|
||||
}
|
||||
|
||||
at = time.After(time.Second * 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cmdAdd
|
||||
// @description: 添加定时器
|
||||
// parameter:
|
||||
//
|
||||
// @cm:
|
||||
//
|
||||
// return:
|
||||
func cmdAdd(cm *cmdModel) {
|
||||
newObj := cm.paramObj.(*timerObj)
|
||||
if newObj.needCheckId && checkTimerExist(newObj.id) {
|
||||
cm.err = fmt.Errorf("已经存在id=%s的timer", newObj.id)
|
||||
cm.waitChan <- struct{}{}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果执行时间比当前时间小,则放入最近的调度卡槽,以便尽快执行
|
||||
tick := newObj.tick
|
||||
if tick <= (secondStarTime + int64(secIndex)) {
|
||||
tick = (secondStarTime + int64(secIndex)) + 1
|
||||
}
|
||||
|
||||
// 落在秒钟级别定时器上
|
||||
if tick < (secondStarTime + con_SECOND_SLOT_NUM) {
|
||||
index := (int)(tick - secondStarTime)
|
||||
secondsTimers[index].addTimer(newObj)
|
||||
cm.waitChan <- struct{}{}
|
||||
return
|
||||
}
|
||||
|
||||
// 落在分钟级别定时器上
|
||||
if tick < (minStarTime + con_MINUTES_SLOT_NUM*con_SECOND_SLOT_NUM) {
|
||||
index := (int)(tick-minStarTime) / con_SECOND_SLOT_NUM
|
||||
minutesTimers[index].addTimer(newObj)
|
||||
cm.waitChan <- struct{}{}
|
||||
return
|
||||
}
|
||||
|
||||
//落在小时级别定时器上
|
||||
otherTimers.addTimer(newObj)
|
||||
|
||||
// 返回操作完成
|
||||
cm.waitChan <- struct{}{}
|
||||
}
|
||||
|
||||
// cmdDel
|
||||
// @description: 删除timer
|
||||
// parameter:
|
||||
//
|
||||
// @cm:
|
||||
//
|
||||
// return:
|
||||
func cmdDel(cm *cmdModel) {
|
||||
id := cm.paramObj.(string)
|
||||
|
||||
// 移除秒级别定时器
|
||||
for _, item := range secondsTimers {
|
||||
item.delTimer(id)
|
||||
}
|
||||
|
||||
// 移除分种级定时器
|
||||
for _, item := range minutesTimers {
|
||||
item.delTimer(id)
|
||||
}
|
||||
|
||||
// 移除时钟级定时器
|
||||
otherTimers.delTimer(id)
|
||||
|
||||
// 返回操作完成
|
||||
cm.waitChan <- struct{}{}
|
||||
}
|
||||
|
||||
// cmdRun
|
||||
// @description: 运行定时器
|
||||
// parameter:
|
||||
// return:
|
||||
func cmdRun() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logUtil.ErrorLog("coroutine-timer.inExcute err:%s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 执行秒级定时器
|
||||
timers := getSencondTimers()
|
||||
if len(timers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range timers {
|
||||
go safeRun(t)
|
||||
}
|
||||
}
|
||||
|
||||
// checkTimerExist
|
||||
// @description: 校验timer是否存在
|
||||
// parameter:
|
||||
//
|
||||
// @id:id
|
||||
//
|
||||
// return:
|
||||
//
|
||||
// @bool:
|
||||
func checkTimerExist(id string) bool {
|
||||
// 秒级别定时器检测
|
||||
for _, item := range secondsTimers {
|
||||
if item.exist(id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 分种级定时器检测
|
||||
for _, item := range minutesTimers {
|
||||
if item.exist(id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 时钟级定时器检测
|
||||
return otherTimers.exist(id)
|
||||
}
|
||||
|
||||
// getSencondTimers
|
||||
// @description: 获取秒级定时器
|
||||
// parameter:
|
||||
// return:
|
||||
//
|
||||
// @result:
|
||||
func getSencondTimers() (result []*timerObj) {
|
||||
// 获取对应slot里面的定时对象
|
||||
result = secondsTimers[secIndex].getAllTimers2()
|
||||
secondsTimers[secIndex] = newTimersModel()
|
||||
secIndex++
|
||||
|
||||
// 如果达到最大,则重新填装新的调度对象
|
||||
if secIndex == con_SECOND_SLOT_NUM {
|
||||
secIndex = 0
|
||||
secondStarTime = secondStarTime + con_SECOND_SLOT_NUM
|
||||
minTaskList := getMinutesTasks()
|
||||
for _, t := range minTaskList {
|
||||
index := t.tick - secondStarTime
|
||||
secondsTimers[index].addTimer(t)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getMinutesTasks
|
||||
// @description: 获取分钟级定时器
|
||||
// parameter:
|
||||
// return:
|
||||
//
|
||||
// @result:
|
||||
func getMinutesTasks() (result []*timerObj) {
|
||||
// 获取对应slot里面的定时对象
|
||||
result = minutesTimers[minIndex].getAllTimers2()
|
||||
minutesTimers[minIndex] = newTimersModel()
|
||||
minIndex++
|
||||
|
||||
// 如果达到最大,则重新填装新的调度对象
|
||||
if minIndex == con_MINUTES_SLOT_NUM {
|
||||
reInputMin()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// reInputMin
|
||||
// @description: 重新填入分钟级定时器
|
||||
// parameter:
|
||||
// return:
|
||||
func reInputMin() {
|
||||
minIndex = 0
|
||||
minStarTime = minStarTime + con_MINUTES_SLOT_NUM*con_SECOND_SLOT_NUM
|
||||
|
||||
delMap := make(map[string]struct{})
|
||||
for _, t := range otherTimers.getAllTimers() {
|
||||
index := (t.tick - minStarTime) / con_SECOND_SLOT_NUM
|
||||
if index > math.MaxInt || index >= con_MINUTES_SLOT_NUM {
|
||||
continue
|
||||
}
|
||||
minutesTimers[index].addTimer(t)
|
||||
delMap[t.id] = struct{}{}
|
||||
}
|
||||
|
||||
if len(delMap) > 0 {
|
||||
for k := range delMap {
|
||||
otherTimers.delTimer(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// safeRun
|
||||
// @description: 安全运行定时器回调
|
||||
// parameter:
|
||||
//
|
||||
// @t:
|
||||
//
|
||||
// return:
|
||||
func safeRun(t *timerObj) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logUtil.ErrorLog("coroutine-timer.safeRun id:%s err:%s", t.id, err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.excuteAction(t.paramObj)
|
||||
}
|
||||
40
trunk/goutil/coroutine-timer/timer-obj.go
Normal file
40
trunk/goutil/coroutine-timer/timer-obj.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package coroutine_timer
|
||||
|
||||
// timerObj
|
||||
// @description: 定时调度对象
|
||||
type timerObj struct {
|
||||
// id 调度id
|
||||
id string
|
||||
|
||||
// tick 执行时间
|
||||
tick int64
|
||||
|
||||
// excuteAction 执行方法
|
||||
excuteAction func(interface{})
|
||||
|
||||
// paramObj 携带的参数
|
||||
paramObj interface{}
|
||||
|
||||
// needCheckId 是否需要校验id
|
||||
needCheckId bool
|
||||
}
|
||||
|
||||
// newTimerObj
|
||||
// @description: 构造调度对象
|
||||
// parameter:
|
||||
// @_id:id
|
||||
// @t:调度时间
|
||||
// @ea:调度方法
|
||||
// @pm:调度参数
|
||||
// return:
|
||||
// @*timerObj:
|
||||
func newTimerObj(_id string, t int64, ea func(interface{}), pm interface{}) *timerObj {
|
||||
result := &timerObj{
|
||||
id: _id,
|
||||
tick: t,
|
||||
excuteAction: ea,
|
||||
paramObj: pm,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
114
trunk/goutil/coroutine-timer/timer_test.go
Normal file
114
trunk/goutil/coroutine-timer/timer_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package coroutine_timer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"goutil/mathUtil"
|
||||
"goutil/stringUtil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func Test_Method1(t *testing.T) {
|
||||
imap := make(map[int]struct{})
|
||||
var lockObj sync.Mutex
|
||||
|
||||
cb := func(obj interface{}) {
|
||||
i := obj.(int)
|
||||
|
||||
lockObj.Lock()
|
||||
defer lockObj.Unlock()
|
||||
|
||||
if _, exist := imap[i]; exist == false {
|
||||
t.Error(i, "应该删除,不应该回调 Test_Method1")
|
||||
}
|
||||
|
||||
delete(imap, i)
|
||||
}
|
||||
|
||||
for i := 0; i < 20000; i++ {
|
||||
tick := i % 20
|
||||
isdel := false
|
||||
if tick > 1 {
|
||||
isdel = mathUtil.GetRand().GetRandInt(100) < 50
|
||||
}
|
||||
if isdel == false {
|
||||
lockObj.Lock()
|
||||
imap[i] = struct{}{}
|
||||
lockObj.Unlock()
|
||||
}
|
||||
id := AddTimer(tick, cb, i)
|
||||
if isdel {
|
||||
DeleteTimer(id)
|
||||
}
|
||||
}
|
||||
|
||||
newN := 10000000
|
||||
newId := stringUtil.GetNewUUID()
|
||||
|
||||
lockObj.Lock()
|
||||
imap[newN] = struct{}{}
|
||||
lockObj.Unlock()
|
||||
|
||||
err := AddTimer4(newId, 3, cb, newN)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = AddTimer4(newId, 3, cb, newN)
|
||||
if err == nil {
|
||||
t.Error("未检测到重复id")
|
||||
}
|
||||
|
||||
for {
|
||||
if len(imap) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t.Log("剩余回调次数:", len(imap))
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Method2(t *testing.T) {
|
||||
imap := make(map[int64]struct{})
|
||||
var lockObj sync.Mutex
|
||||
|
||||
cb := func(obj interface{}) {
|
||||
i := obj.(int64)
|
||||
n := time.Now().Unix()
|
||||
x := n - i
|
||||
// 此处因为启动有暂停5s,所以启动后最近的执行偏差在5s内
|
||||
if x > 6 || x < -6 {
|
||||
t.Errorf("错误的时间执行了回调函数 tick:%v now:%v", i, n)
|
||||
}
|
||||
|
||||
lockObj.Lock()
|
||||
defer lockObj.Unlock()
|
||||
|
||||
if _, exist := imap[i]; exist == false {
|
||||
t.Error(i, "应该删除,不应该回调 Test_Method2")
|
||||
}
|
||||
|
||||
delete(imap, i)
|
||||
}
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
tick := time.Now().Unix() + int64(i)
|
||||
imap[tick] = struct{}{}
|
||||
AddTimer3(tick, cb, tick)
|
||||
}
|
||||
|
||||
for {
|
||||
if len(imap) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t.Log("剩余回调次数:", len(imap))
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user