/
utils.go
156 lines (131 loc) · 4.4 KB
/
utils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package store
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/pkg/errors"
"github.com/Brightscout/mattermost-plugin-azure-devops/server/constants"
)
var ErrNotFound = errors.New("not found")
// Ensure makes sure the initial value for a key is set to the value provided, if it does not already exist
// Returns the value set for the key in kv-store and error
func (s *Store) Ensure(key string, newValue []byte) ([]byte, error) {
value, err := s.Load(key)
switch err {
case nil:
return value, nil
case ErrNotFound:
break
default:
return nil, err
}
if err = s.Store(key, newValue); err != nil {
return nil, err
}
// Load again in case we lost the race to another server
value, err = s.Load(key)
if err != nil {
return newValue, nil
}
return value, nil
}
// LoadJSON loads a json value stored in the KVStore using StoreJSON
// unmarshalling it to an interface using json.Unmarshal
func (s *Store) LoadJSON(key string, v interface{}) (returnErr error) {
data, err := s.Load(key)
if err != nil {
return err
}
if data == nil {
return nil
}
return json.Unmarshal(data, v)
}
// StoreJSON stores a json value from an interface to KVStore after marshaling it using json.Marshal
func (s *Store) StoreJSON(key string, v interface{}) (returnErr error) {
data, err := json.Marshal(v)
if err != nil {
return err
}
return s.Store(key, data)
}
// AtomicModifyWithOptions modifies the value for a key in KVStore, only if the initial value is not changed while attempting to modify it.
// To avoid race conditions, we retry the modification multiple times after waiting for a fixed wait interval.
// input: kv store key and a modify function which takes initial value and returns final value with PluginKVSetOptions and error.
// returns: nil for a successful update and error if the update is unsuccessful or the retry limit reached.
func (s *Store) AtomicModifyWithOptions(key string, modify func(initialValue []byte) ([]byte, *model.PluginKVSetOptions, error)) error {
currentAttempt := 0
for {
initialBytes, appErr := s.Load(key)
if appErr != nil && appErr != ErrNotFound {
return errors.Wrap(appErr, "unable to read initial value")
}
newValue, opts, err := modify(initialBytes)
if err != nil {
return errors.Wrap(err, "modification error")
}
// No modifications have been done. No reason to hit the plugin API.
if bytes.Equal(initialBytes, newValue) {
return nil
}
if opts == nil {
opts = &model.PluginKVSetOptions{}
}
opts.Atomic = true
opts.OldValue = initialBytes
success, setError := s.StoreWithOptions(key, newValue, *opts)
if setError != nil {
return errors.Wrap(setError, "problem writing value")
}
if success {
return nil
}
currentAttempt++
if currentAttempt >= constants.AtomicRetryLimit {
return errors.New("reached write attempt limit")
}
time.Sleep(constants.AtomicRetryWait)
}
}
// AtomicModify calls AtomicModifyWithOptions with nil PluginKVSetOptions
// to atomically modify a value in KVStore and set it for an indefinite time
// See AtomicModifyWithOptions for more info
func (s *Store) AtomicModify(key string, modify func(initialValue []byte) ([]byte, error)) error {
return s.AtomicModifyWithOptions(key, func(initialValue []byte) ([]byte, *model.PluginKVSetOptions, error) {
dataInByte, err := modify(initialValue)
return dataInByte, nil, err
})
}
func GetProjectListMapKey() string {
return GetKeyHash(constants.ProjectPrefix)
}
func GetProjectKey(projectID, mattermostUserID string) string {
return fmt.Sprintf(constants.ProjectKey, projectID, mattermostUserID)
}
func GetOAuthKey(mattermostUserID string) string {
return fmt.Sprintf(constants.OAuthPrefix, mattermostUserID)
}
func GetSubscriptionListMapKey() string {
return GetKeyHash(constants.SubscriptionPrefix)
}
func GetSubscriptionKey(mattermostUserID, projectID, channelID, eventType string) string {
return fmt.Sprintf("%s_%s_%s_%s", mattermostUserID, projectID, channelID, eventType)
}
// GetKeyHash can be used to create a hash from a string
func GetKeyHash(key string) string {
hash := sha256.New()
_, _ = hash.Write([]byte(key))
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
func IsValidUserKey(key string) (string, bool) {
res := strings.Split(key, "_")
if len(res) == 2 && res[0] == constants.UserIDPrefix {
return res[1], true
}
return "", false
}