-
Notifications
You must be signed in to change notification settings - Fork 10
/
memcached.go
159 lines (138 loc) · 4.66 KB
/
memcached.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
157
158
159
package main
import (
"bytes"
"strconv"
"strings"
)
const (
ERR_NONE = iota
ERR_NO_CMD
ERR_INVALID_CMD
ERR_TRUNCATED
ERR_INCOMPLETE_CMD
ERR_BAD_BYTES
)
/* A map to translate errors that may arise to the name of the stat that
* should be reported back when the error occurs. An entry should be added
* for all non-none errors that can be returned. */
var ERR_TO_STAT = map[int]string{
ERR_NO_CMD: "no_cmd",
ERR_INVALID_CMD: "invalid_cmd",
ERR_TRUNCATED: "truncated",
ERR_INCOMPLETE_CMD: "incomplete_cmd",
ERR_BAD_BYTES: "bad_bytes",
}
// processSingleKeyNoData processes a "get", "incr", or "decr" command, all
// of which only allow for a single key to be passed and have no value field.
//
// On the wire, "get" looks like:
//
// get key\r\n
//
// And "incr" and "decr" look like:
//
// cmd key value [noreply]\r\n
//
// Where "noreply" is an optional field that indicates whether the server
// should return a response.
func processSingleKeyNoData(first_line string, remainder []byte) (keys []string, processed_remainder []byte, cmd_err int) {
// Get the key
// ... the command should at least consist of "cmd foo", where "foo" is the key
split_data := strings.Split(first_line, " ")
if len(split_data) <= 1 {
return []string{}, remainder, ERR_INCOMPLETE_CMD
}
key := split_data[1]
if key == "" {
return []string{}, remainder, ERR_INCOMPLETE_CMD
}
// Return parsed data
return []string{key}, remainder, ERR_NONE
}
// processSingleKeyWithData processes a "set", "add", "replace", "append", or
// "prepend" command, all of which only allow for a single key and have a
// corresponding value field.
//
// On the wire, these commands look like:
//
// cmd key flags exptime bytes [noreply]\r\n
// <data block of `bytes` length>\r\n
//
// Where "noreply" is an optional field that indicates whether the server
// should return a response.
func processSingleKeyWithData(first_line string, remainder []byte) (keys []string, processed_remainder []byte, cmd_err int) {
// Get the key
split_data := strings.Split(first_line, " ")
if len(split_data) != 5 && len(split_data) != 6 {
return []string{}, remainder, ERR_INCOMPLETE_CMD
}
key, bytes_str := split_data[1], split_data[4]
// Parse length of stored value
base := 10
// ... the max memcached object size is 1MB, so a 32 bit int will suffice
bitSize := 32
bytes, err := strconv.ParseInt(bytes_str, base, bitSize)
if err != nil {
return []string{}, []byte{}, ERR_INVALID_CMD
}
// Make sure we got a full command
// ... bytes + 2 to account for trailing "\r\n"
next_command_idx := bytes + 2
if int64(len(remainder)) < next_command_idx {
return []string{}, []byte{}, ERR_TRUNCATED
}
// Return parsed data
return []string{key}, remainder[next_command_idx:], ERR_NONE
}
// processMultiKeyNoData processes a "gets" command, which allows for
// multiple keys and has no value field.
//
// On the wire, "gets" looks like:
//
// gets key1 key2 key3\r\n
func processMultiKeyNoData(first_line string, remainder []byte) (keys []string, processed_remainder []byte, cmd_err int) {
// Get the key(s)
// ... the command should at least consist of "cmd foo", where "foo" is the key
split_data := strings.Split(first_line, " ")
if len(split_data) <= 1 {
return []string{}, remainder, ERR_INCOMPLETE_CMD
}
keys = split_data[1:]
// Return parsed data
return keys, remainder, ERR_NONE
}
var CMD_PROCESSORS = map[string]func(first_line string, remainder []byte) (keys []string, processed_remainder []byte, cmd_err int){
"get": processSingleKeyNoData,
"gets": processMultiKeyNoData,
"set": processSingleKeyWithData,
"add": processSingleKeyWithData,
"replace": processSingleKeyWithData,
"append": processSingleKeyWithData,
"prepend": processSingleKeyWithData,
"incr": processSingleKeyNoData,
"decr": processSingleKeyNoData,
}
// parseCommand parses a command and list of keys the command is operating on from
// a sequence of application-level data bytes.
func parseCommand(app_data []byte) (cmd string, keys []string, remainder []byte, cmd_err int) {
// Parse out the command
space_i := bytes.IndexByte(app_data, byte(' '))
if space_i == -1 {
return "", []string{}, []byte{}, ERR_NO_CMD
}
// Find the first newline
newline_i := bytes.Index(app_data, []byte("\r\n"))
if newline_i == -1 {
return "", []string{}, []byte{}, ERR_TRUNCATED
}
// Validate command
first_line := string(app_data[:newline_i])
split_data := strings.Split(first_line, " ")
cmd = split_data[0]
if fn, ok := CMD_PROCESSORS[cmd]; ok {
keys, remainder, cmd_err = fn(first_line, app_data[newline_i+2:])
} else {
return "", []string{}, []byte{}, ERR_INVALID_CMD
}
return cmd, keys, remainder, cmd_err
}