@@ -5,16 +5,34 @@ import "code.gitea.io/gitea/modules/timeutil" | |||
type LimitType string | |||
const ( | |||
LimitTypeTask LimitType = "TASK" | |||
LimitTypeReward LimitType = "REWARD" | |||
LimitTypeTask LimitType = "TASK" | |||
LimitTypeRewardPoint LimitType = "REWARD_POINT" | |||
) | |||
func (l LimitType) Name() string { | |||
switch l { | |||
case LimitTypeTask: | |||
return "TASK" | |||
case LimitTypeReward: | |||
return "REWARD" | |||
case LimitTypeRewardPoint: | |||
return "REWARD_POINT" | |||
default: | |||
return "" | |||
} | |||
} | |||
type LimitScope string | |||
const ( | |||
LimitScopeAllUsers LimitScope = "ALL_USERS" | |||
LimitScopeSingleUser LimitScope = "SINGLE_USER" | |||
) | |||
func (l LimitScope) Name() string { | |||
switch l { | |||
case LimitScopeAllUsers: | |||
return "ALL_USERS" | |||
case LimitScopeSingleUser: | |||
return "SINGLE_USER" | |||
default: | |||
return "" | |||
} | |||
@@ -23,19 +41,19 @@ func (l LimitType) Name() string { | |||
type LimitConfig struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
Tittle string | |||
RefreshRate string `xorm:"NOT NULL"` | |||
Scope string `xorm:"NOT NULL"` | |||
LimitNum int64 `xorm:"NOT NULL"` | |||
LimitCode string `xorm:"NOT NULL"` | |||
RefreshRate string `xorm:"NOT NULL"` | |||
Scope string `xorm:"NOT NULL"` | |||
LimitNum int64 `xorm:"NOT NULL"` | |||
LimitCode string | |||
LimitType string `xorm:"NOT NULL"` | |||
Creator int64 `xorm:"NOT NULL"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"created"` | |||
DeletedAt timeutil.TimeStamp `xorm:"deleted"` | |||
} | |||
func GetLimitConfigByLimitCode(limitCode string, limitType LimitType) ([]LimitConfig, error) { | |||
func GetLimitConfigByLimitType(limitType LimitType) ([]LimitConfig, error) { | |||
r := make([]LimitConfig, 0) | |||
err := x.Where("limit_code = ? and limit_type = ?", limitCode, limitType.Name()).Find(&r) | |||
err := x.Where(" limit_type = ?", limitType.Name()).Find(&r) | |||
if err != nil { | |||
return nil, err | |||
} else if len(r) == 0 { | |||
@@ -2,7 +2,6 @@ package models | |||
import ( | |||
"code.gitea.io/gitea/modules/timeutil" | |||
"fmt" | |||
) | |||
const ( | |||
@@ -17,8 +16,13 @@ const ( | |||
RewardTypePoint RewardType = "POINT" | |||
) | |||
func (r *RewardType) String() string { | |||
return fmt.Sprint(r) | |||
func (r RewardType) Name() string { | |||
switch r { | |||
case RewardTypePoint: | |||
return "POINT" | |||
default: | |||
return "" | |||
} | |||
} | |||
const ( | |||
@@ -11,7 +11,6 @@ type TaskAccomplishLog struct { | |||
ConfigId int64 `xorm:"NOT NULL"` | |||
TaskCode string `xorm:"NOT NULL"` | |||
UserId int64 `xorm:"INDEX NOT NULL"` | |||
SourceId string `xorm:"INDEX NOT NULL"` | |||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
} | |||
@@ -31,15 +30,7 @@ func getTaskAccomplishLog(tl *TaskAccomplishLog) (*TaskAccomplishLog, error) { | |||
return tl, nil | |||
} | |||
func GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskCode string) (*TaskAccomplishLog, error) { | |||
t := &TaskAccomplishLog{ | |||
SourceId: sourceId, | |||
TaskCode: taskCode, | |||
} | |||
return getTaskAccomplishLog(t) | |||
} | |||
func CountInTaskPeriod(configId int64, userId int64, period *PeriodResult) (int64, error) { | |||
func CountTaskAccomplishLogInTaskPeriod(configId int64, userId int64, period *PeriodResult) (int64, error) { | |||
if period == nil { | |||
return x.Where("config_id = ? and user_id = ?", configId, userId).Count(&TaskAccomplishLog{}) | |||
} else { | |||
@@ -5,8 +5,24 @@ import ( | |||
) | |||
const ( | |||
TaskTypeCreateIssueComment string = "CREATE_IS" | |||
TaskTypeNewIssue = "NEW_ISSUE" | |||
TaskTypeNewIssue = "NEW_ISSUE" | |||
TaskTypeIssueChangeStatus = "ISSUE_CHANGE_STATUS" | |||
TaskTypeCreateIssueComment = "CREATE_ISSUE_COMMENT" | |||
TaskTypeNewPullRequest = "NEW_PULL_REQUEST" | |||
TaskTypeRenameRepository = "RENAME_REPOSITORY" | |||
TaskTypeAliasRepository = "ALIAS_REPOSITORY" | |||
TaskTypeTransferRepository = "TRANSFER_REPOSITORY" | |||
TaskTypeCreateRepository = "CREATE_REPOSITORY" | |||
TaskTypeForkRepository = "FORK_REPOSITORY" | |||
TaskTypePullRequestReview = "PULL_REQUEST_REVIEW" | |||
TaskTypeCommentPull = "COMMENT_PULL" | |||
TaskTypeApprovePullRequest = "APPROVE_PULL_REQUEST" | |||
TaskTypeRejectPullRequest = "REJECT_PULL_REQUEST" | |||
TaskTypeMergePullRequest = "MERGE_PULL_REQUEST" | |||
TaskTypeSyncPushCommits = "SYNC_PUSH_COMMITS" | |||
TaskTypeSyncCreateRef = "SYNC_CREATE_REF" | |||
TaskTypeSyncDeleteRef = "SYNC_DELETE_REF" | |||
TaskTypeBindWechat = "BIND_WECHAT" | |||
) | |||
const ( | |||
@@ -345,3 +345,7 @@ func (a *actionNotifier) NotifyOtherTask(doer *models.User, repo *models.Reposit | |||
log.Error("notifyWatchers: %v", err) | |||
} | |||
} | |||
func (a *actionNotifier) NotifyWechatBind(doer *models.User) { | |||
return | |||
} |
@@ -56,4 +56,5 @@ type Notifier interface { | |||
NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) | |||
NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) | |||
NotifyWechatBind(doer *models.User) | |||
} |
@@ -158,3 +158,7 @@ func (*NullNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Reposit | |||
func (*NullNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { | |||
} | |||
func (*NullNotifier) NotifyWechatBind(doer *models.User) { | |||
} |
@@ -271,3 +271,10 @@ func NotifySyncDeleteRef(pusher *models.User, repo *models.Repository, refType, | |||
notifier.NotifySyncDeleteRef(pusher, repo, refType, refFullName) | |||
} | |||
} | |||
// NotifyWechatBind notifies wechat bind | |||
func NotifyWechatBind(doer *models.User) { | |||
for _, notifier := range notifiers { | |||
notifier.NotifyWechatBind(doer) | |||
} | |||
} |
@@ -5,7 +5,7 @@ import ( | |||
"code.gitea.io/gitea/modules/notification/base" | |||
"code.gitea.io/gitea/modules/repository" | |||
"code.gitea.io/gitea/services/task" | |||
"fmt" | |||
"strings" | |||
) | |||
type taskNotifier struct { | |||
@@ -22,64 +22,86 @@ func NewNotifier() base.Notifier { | |||
} | |||
func (t *taskNotifier) NotifyNewIssue(issue *models.Issue) { | |||
task.Accomplish(issue.Poster.ID, models.TaskTypeNewIssue, fmt.Sprint(issue.ID)) | |||
task.Accomplish(issue.Poster.ID, models.TaskTypeNewIssue) | |||
} | |||
// NotifyIssueChangeStatus notifies close or reopen issue to notifiers | |||
func (t *taskNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, closeOrReopen bool) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeIssueChangeStatus) | |||
} | |||
// NotifyCreateIssueComment notifies comment on an issue to notifiers | |||
func (t *taskNotifier) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, | |||
issue *models.Issue, comment *models.Comment) { | |||
task.Accomplish(doer.ID, models.TaskTypeCreateIssueComment, fmt.Sprint(comment.ID)) | |||
task.Accomplish(doer.ID, models.TaskTypeCreateIssueComment) | |||
} | |||
func (t *taskNotifier) NotifyNewPullRequest(pull *models.PullRequest) { | |||
task.Accomplish(pull.Issue.Poster.ID, models.TaskTypeCreateIssueComment, fmt.Sprint(pull.ID)) | |||
task.Accomplish(pull.Issue.Poster.ID, models.TaskTypeNewPullRequest) | |||
} | |||
func (t *taskNotifier) NotifyRenameRepository(doer *models.User, repo *models.Repository, oldRepoName string) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeRenameRepository) | |||
} | |||
func (t *taskNotifier) NotifyAliasRepository(doer *models.User, repo *models.Repository, oldAlias string) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeAliasRepository) | |||
} | |||
func (t *taskNotifier) NotifyTransferRepository(doer *models.User, repo *models.Repository, oldOwnerName string) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeTransferRepository) | |||
} | |||
func (t *taskNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeCreateRepository) | |||
} | |||
func (t *taskNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeForkRepository) | |||
} | |||
func (t *taskNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) { | |||
return | |||
for _, lines := range review.CodeComments { | |||
for _, comments := range lines { | |||
for _, _ = range comments { | |||
task.Accomplish(review.Reviewer.ID, models.TaskTypePullRequestReview) | |||
} | |||
} | |||
} | |||
if review.Type != models.ReviewTypeComment || strings.TrimSpace(comment.Content) != "" { | |||
switch review.Type { | |||
case models.ReviewTypeApprove: | |||
task.Accomplish(review.Reviewer.ID, models.TaskTypeApprovePullRequest) | |||
case models.ReviewTypeReject: | |||
task.Accomplish(review.Reviewer.ID, models.TaskTypeRejectPullRequest) | |||
default: | |||
task.Accomplish(review.Reviewer.ID, models.TaskTypeCommentPull) | |||
} | |||
} | |||
} | |||
func (t *taskNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User) { | |||
return | |||
task.Accomplish(doer.ID, models.TaskTypeMergePullRequest) | |||
} | |||
func (t *taskNotifier) NotifySyncPushCommits(pusher *models.User, repo *models.Repository, refName, oldCommitID, newCommitID string, commits *repository.PushCommits) { | |||
return | |||
task.Accomplish(repo.OwnerID, models.TaskTypeSyncPushCommits) | |||
} | |||
func (t *taskNotifier) NotifySyncCreateRef(doer *models.User, repo *models.Repository, refType, refFullName string) { | |||
return | |||
task.Accomplish(repo.OwnerID, models.TaskTypeSyncCreateRef) | |||
} | |||
func (t *taskNotifier) NotifySyncDeleteRef(doer *models.User, repo *models.Repository, refType, refFullName string) { | |||
return | |||
task.Accomplish(repo.OwnerID, models.TaskTypeSyncDeleteRef) | |||
} | |||
func (t *taskNotifier) NotifyOtherTask(doer *models.User, repo *models.Repository, id string, name string, optype models.ActionType) { | |||
return | |||
} | |||
func (t *taskNotifier) NotifyWechatBind(doer *models.User) { | |||
task.Accomplish(doer.ID, models.TaskTypeSyncDeleteRef) | |||
} |
@@ -7,20 +7,20 @@ import ( | |||
const LIMIT_REDIS_PREFIX = "limit" | |||
func LimitCount(userId int64, limitCode string, period *models.PeriodResult) string { | |||
if userId == 0 { | |||
func LimitCount(userId int64, limitCode string, limitType string, scope string, period *models.PeriodResult) string { | |||
if scope == models.LimitScopeAllUsers.Name() { | |||
if period == nil { | |||
return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, "count") | |||
return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType, "count") | |||
} | |||
return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") | |||
return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") | |||
} | |||
if period == nil { | |||
return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, "count") | |||
return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, limitType, "count") | |||
} | |||
return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") | |||
return KeyJoin(LIMIT_REDIS_PREFIX, "uid", fmt.Sprint(userId), limitCode, limitType, fmt.Sprint(period.StartTime.Unix()), fmt.Sprint(period.EndTime.Unix()), "count") | |||
} | |||
func LimitConfig(limitCode string, limitType models.LimitType) string { | |||
return KeyJoin(LIMIT_REDIS_PREFIX, limitCode, limitType.Name(), "config") | |||
func LimitConfig(limitType models.LimitType) string { | |||
return KeyJoin(LIMIT_REDIS_PREFIX, limitType.Name(), "config") | |||
} |
@@ -39,6 +39,7 @@ func (l *limiterRunner) Run() error { | |||
err := l.limit(l.limiters[l.index]) | |||
if err != nil { | |||
log.Info("limiter check failed,%v", err) | |||
l.Rollback(l.index) | |||
return err | |||
} | |||
l.index += 1 | |||
@@ -46,20 +47,50 @@ func (l *limiterRunner) Run() error { | |||
return nil | |||
} | |||
//Rollback rollback the usedNum from limiters[0] to limiters[index] | |||
func (l *limiterRunner) Rollback(index int) error { | |||
for i := index; i >= 0; i-- { | |||
l.rollback(l.limiters[i]) | |||
} | |||
return nil | |||
} | |||
func (l *limiterRunner) rollback(r models.LimitConfig) error { | |||
p, err := period.GetPeriod(r.RefreshRate) | |||
if err != nil { | |||
return err | |||
} | |||
redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p) | |||
redis_client.IncrBy(redisKey, -1*l.amount) | |||
return nil | |||
} | |||
func (l *limiterRunner) limit(r models.LimitConfig) error { | |||
p, err := period.GetPeriod(r.RefreshRate) | |||
if err != nil { | |||
return err | |||
} | |||
redisKey := redis_key.LimitCount(l.userId, r.LimitCode, p) | |||
redisKey := redis_key.LimitCount(l.userId, r.LimitCode, r.LimitType, r.Scope, p) | |||
usedNum, err := redis_client.IncrBy(redisKey, l.amount) | |||
//if it is the first time,set expire time | |||
if usedNum == l.amount && p != nil { | |||
//todo 验证浮点精确度 | |||
redis_client.Expire(redisKey, int64(p.LeftTime.Seconds())) | |||
if err != nil { | |||
return err | |||
} | |||
//if usedNum equals amount,it is the first operation in period or redis cache deleted | |||
//count in database to distinguish the two cases | |||
if usedNum == l.amount { | |||
n, err := l.countInPeriod(r, p) | |||
if err != nil { | |||
return err | |||
} | |||
if n > 0 { | |||
//means redis cache deleted,incr the cache with real value | |||
usedNum, err = redis_client.IncrBy(redisKey, n) | |||
} | |||
if p != nil { | |||
redis_client.Expire(redisKey, int64(p.LeftTime.Seconds())) | |||
} | |||
} | |||
if usedNum > r.LimitNum { | |||
redis_client.IncrBy(redisKey, -1*l.amount) | |||
return errors.New(fmt.Sprintf("%s:over limit", r.Tittle)) | |||
} | |||
return nil | |||
@@ -76,13 +107,37 @@ func (l *limiterRunner) LoadLimiters() error { | |||
return nil | |||
} | |||
func (l *limiterRunner) countInPeriod(r models.LimitConfig, p *models.PeriodResult) (int64, error) { | |||
switch r.LimitType { | |||
case models.LimitTypeTask.Name(): | |||
return models.CountTaskAccomplishLogInTaskPeriod(r.ID, l.userId, p) | |||
default: | |||
return 0, nil | |||
} | |||
} | |||
func CheckLimit(limitCode string, limitType models.LimitType, userId, amount int64) error { | |||
r := newLimiterRunner(limitCode, limitType, userId, amount) | |||
return r.Run() | |||
} | |||
func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitConfig, error) { | |||
redisKey := redis_key.LimitConfig(limitCode, limitType) | |||
limiters, err := GetLimitersByLimitType(limitType) | |||
if err != nil { | |||
return nil, err | |||
} | |||
result := make([]models.LimitConfig, 0) | |||
for i, v := range limiters { | |||
if v.LimitCode == "" || v.LimitCode == limitCode { | |||
result = append(result, limiters[i]) | |||
} | |||
} | |||
return result, nil | |||
} | |||
func GetLimitersByLimitType(limitType models.LimitType) ([]models.LimitConfig, error) { | |||
redisKey := redis_key.LimitConfig(limitType) | |||
val, _ := redis_client.Get(redisKey) | |||
if val != "" { | |||
if val == redis_key.EMPTY_REDIS_VAL { | |||
@@ -92,7 +147,7 @@ func GetLimiters(limitCode string, limitType models.LimitType) ([]models.LimitCo | |||
json.Unmarshal([]byte(val), &limiters) | |||
return limiters, nil | |||
} | |||
limiters, err := models.GetLimitConfigByLimitCode(limitCode, limitType) | |||
limiters, err := models.GetLimitConfigByLimitType(limitType) | |||
if err != nil { | |||
if models.IsErrRecordNotExist(err) { | |||
redis_client.Setex(redisKey, redis_key.EMPTY_REDIS_VAL, 5*time.Second) | |||
@@ -58,7 +58,7 @@ func Send(ctx models.RewardOperateContext) error { | |||
} | |||
//is limited? | |||
if operator.IsLimited(ctx) { | |||
if isLimited := operator.IsLimited(ctx); isLimited { | |||
return nil | |||
} | |||
@@ -15,7 +15,7 @@ type PointOperator struct { | |||
} | |||
func (operator *PointOperator) IsLimited(ctx models.RewardOperateContext) bool { | |||
if err := limiter.CheckLimit(ctx.Reward.Type, models.LimitTypeReward, ctx.TargetUserId, ctx.Reward.Amount); err != nil { | |||
if err := limiter.CheckLimit(ctx.SourceType, models.LimitTypeRewardPoint, ctx.TargetUserId, ctx.Reward.Amount); err != nil { | |||
return true | |||
} | |||
return false | |||
@@ -3,49 +3,23 @@ package task | |||
import ( | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/redis/redis_key" | |||
"code.gitea.io/gitea/modules/redis/redis_lock" | |||
"code.gitea.io/gitea/modules/util" | |||
"code.gitea.io/gitea/services/reward" | |||
"code.gitea.io/gitea/services/reward/limiter" | |||
"fmt" | |||
"time" | |||
) | |||
func Accomplish(userId int64, taskType string, sourceId string) { | |||
go accomplish(userId, taskType, sourceId) | |||
func Accomplish(userId int64, taskType string) { | |||
go accomplish(userId, taskType) | |||
} | |||
func accomplish(userId int64, taskType string, sourceId string) error { | |||
func accomplish(userId int64, taskType string) error { | |||
defer func() { | |||
if err := recover(); err != nil { | |||
combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) | |||
log.Error("PANIC:%v", combinedErr) | |||
} | |||
}() | |||
//lock | |||
var taskLock = redis_lock.NewDistributeLock(redis_key.TaskAccomplishLock(sourceId, taskType)) | |||
isOk, err := taskLock.Lock(3 * time.Second) | |||
if err != nil { | |||
log.Error("get taskLock error. %v", err) | |||
return err | |||
} | |||
if !isOk { | |||
log.Info("duplicated task request,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) | |||
return nil | |||
} | |||
defer taskLock.UnLock() | |||
//is handled before? | |||
isHandled, err := isHandled(taskType, sourceId) | |||
if err != nil { | |||
log.Error("Get isHandled error,%v", err) | |||
return err | |||
} | |||
if isHandled { | |||
log.Info("task has been handled,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) | |||
return nil | |||
} | |||
//get task config | |||
config, err := GetTaskConfig(taskType) | |||
@@ -54,13 +28,13 @@ func accomplish(userId int64, taskType string, sourceId string) error { | |||
return err | |||
} | |||
if config == nil { | |||
log.Info("task config not exist,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) | |||
log.Info("task config not exist,userId=%d taskType=%s", userId, taskType) | |||
return nil | |||
} | |||
//is limited? | |||
if isLimited(userId, config) { | |||
log.Info("task accomplish maximum times are reached,userId=%d taskType=%s sourceId=%s", userId, taskType, sourceId) | |||
log.Info("task accomplish maximum times are reached,userId=%d taskType=%s", userId, taskType) | |||
return nil | |||
} | |||
@@ -71,7 +45,6 @@ func accomplish(userId int64, taskType string, sourceId string) error { | |||
ConfigId: config.ID, | |||
TaskCode: config.TaskCode, | |||
UserId: userId, | |||
SourceId: sourceId, | |||
}) | |||
if err != nil { | |||
return err | |||
@@ -93,18 +66,6 @@ func accomplish(userId int64, taskType string, sourceId string) error { | |||
return nil | |||
} | |||
func isHandled(taskType string, sourceId string) (bool, error) { | |||
_, err := models.GetTaskAccomplishLogBySourceIdAndTaskCode(sourceId, taskType) | |||
if err != nil { | |||
if models.IsErrRecordNotExist(err) { | |||
return false, nil | |||
} | |||
return false, err | |||
} | |||
return true, nil | |||
} | |||
func isLimited(userId int64, config *models.TaskConfig) bool { | |||
if err := limiter.CheckLimit(config.TaskCode, models.LimitTypeTask, userId, 1); err != nil { | |||
return true | |||