You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

261 lines
6.7 KiB

new events package query parser use parser compiler to generate query parser I used https://github.com/pointlander/peg which has a nice API and seems to be the most popular Golang compiler parser using PEG on Github. More about PEG: - https://en.wikipedia.org/wiki/Parsing_expression_grammar - https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics - https://github.com/PhilippeSigaud/Pegged/wiki/Grammar-Examples rename implement query match function match function uncomment test lines add more test cases for query#Matches fix int case rename events to pubsub add comment about cache assertReceive helper to not block on receive in tests fix bug with multiple conditions uncomment benchmark first results: ``` Benchmark10Clients-2 1000 1305493 ns/op 3957519 B/op 355 allocs/op Benchmark100Clients-2 100 12278304 ns/op 39571751 B/op 3505 allocs/op Benchmark1000Clients-2 10 124120909 ns/op 395714004 B/op 35005 allocs/op ``` 124ms to publish message to 1000 clients. A lot. use AST from query.peg.go separate pubsub and query packages by using Query interface in pubsub wrote docs and refactor code updates from Frey's review refactor type assertion to use type switch cleanup during shutdown subscriber should create output channel, not the server overflow strategies, server buffer capacity context as the first argument for Publish log error introduce Option type update NewServer comment move helpers into pubsub_test increase assertReceive timeout add query.MustParse add more false tests for parser add more false tests for query.Matches parse numbers as int64 / float64 try our best to convert from other types add number to panic output add more comments save commit introduce client argument as first argument to Subscribe > Why we do not specify buffer size on the output channel in Subscribe? The choice of buffer size of N here depends on knowing the number of messages server will receive and the number of messages downstream subscribers will consume. This is fragile: if we publish an additional message, or if one of the downstream subscribers reads any fewer messages, we will again have blocked goroutines. save commit remove reference counting fix test test client resubscribe test UnsubscribeAll client options [pubsub/query] fuzzy testing do not print msg as it creates data race!
7 years ago
new events package query parser use parser compiler to generate query parser I used https://github.com/pointlander/peg which has a nice API and seems to be the most popular Golang compiler parser using PEG on Github. More about PEG: - https://en.wikipedia.org/wiki/Parsing_expression_grammar - https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics - https://github.com/PhilippeSigaud/Pegged/wiki/Grammar-Examples rename implement query match function match function uncomment test lines add more test cases for query#Matches fix int case rename events to pubsub add comment about cache assertReceive helper to not block on receive in tests fix bug with multiple conditions uncomment benchmark first results: ``` Benchmark10Clients-2 1000 1305493 ns/op 3957519 B/op 355 allocs/op Benchmark100Clients-2 100 12278304 ns/op 39571751 B/op 3505 allocs/op Benchmark1000Clients-2 10 124120909 ns/op 395714004 B/op 35005 allocs/op ``` 124ms to publish message to 1000 clients. A lot. use AST from query.peg.go separate pubsub and query packages by using Query interface in pubsub wrote docs and refactor code updates from Frey's review refactor type assertion to use type switch cleanup during shutdown subscriber should create output channel, not the server overflow strategies, server buffer capacity context as the first argument for Publish log error introduce Option type update NewServer comment move helpers into pubsub_test increase assertReceive timeout add query.MustParse add more false tests for parser add more false tests for query.Matches parse numbers as int64 / float64 try our best to convert from other types add number to panic output add more comments save commit introduce client argument as first argument to Subscribe > Why we do not specify buffer size on the output channel in Subscribe? The choice of buffer size of N here depends on knowing the number of messages server will receive and the number of messages downstream subscribers will consume. This is fragile: if we publish an additional message, or if one of the downstream subscribers reads any fewer messages, we will again have blocked goroutines. save commit remove reference counting fix test test client resubscribe test UnsubscribeAll client options [pubsub/query] fuzzy testing do not print msg as it creates data race!
7 years ago
new events package query parser use parser compiler to generate query parser I used https://github.com/pointlander/peg which has a nice API and seems to be the most popular Golang compiler parser using PEG on Github. More about PEG: - https://en.wikipedia.org/wiki/Parsing_expression_grammar - https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics - https://github.com/PhilippeSigaud/Pegged/wiki/Grammar-Examples rename implement query match function match function uncomment test lines add more test cases for query#Matches fix int case rename events to pubsub add comment about cache assertReceive helper to not block on receive in tests fix bug with multiple conditions uncomment benchmark first results: ``` Benchmark10Clients-2 1000 1305493 ns/op 3957519 B/op 355 allocs/op Benchmark100Clients-2 100 12278304 ns/op 39571751 B/op 3505 allocs/op Benchmark1000Clients-2 10 124120909 ns/op 395714004 B/op 35005 allocs/op ``` 124ms to publish message to 1000 clients. A lot. use AST from query.peg.go separate pubsub and query packages by using Query interface in pubsub wrote docs and refactor code updates from Frey's review refactor type assertion to use type switch cleanup during shutdown subscriber should create output channel, not the server overflow strategies, server buffer capacity context as the first argument for Publish log error introduce Option type update NewServer comment move helpers into pubsub_test increase assertReceive timeout add query.MustParse add more false tests for parser add more false tests for query.Matches parse numbers as int64 / float64 try our best to convert from other types add number to panic output add more comments save commit introduce client argument as first argument to Subscribe > Why we do not specify buffer size on the output channel in Subscribe? The choice of buffer size of N here depends on knowing the number of messages server will receive and the number of messages downstream subscribers will consume. This is fragile: if we publish an additional message, or if one of the downstream subscribers reads any fewer messages, we will again have blocked goroutines. save commit remove reference counting fix test test client resubscribe test UnsubscribeAll client options [pubsub/query] fuzzy testing do not print msg as it creates data race!
7 years ago
  1. // Package query provides a parser for a custom query format:
  2. //
  3. // abci.invoice.number=22 AND abci.invoice.owner=Ivan
  4. //
  5. // See query.peg for the grammar, which is a https://en.wikipedia.org/wiki/Parsing_expression_grammar.
  6. // More: https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics
  7. //
  8. // It has a support for numbers (integer and floating point), dates and times.
  9. package query
  10. import (
  11. "fmt"
  12. "reflect"
  13. "strconv"
  14. "strings"
  15. "time"
  16. )
  17. // Query holds the query string and the query parser.
  18. type Query struct {
  19. str string
  20. parser *QueryParser
  21. }
  22. // New parses the given string and returns a query or error if the string is
  23. // invalid.
  24. func New(s string) (*Query, error) {
  25. p := &QueryParser{Buffer: fmt.Sprintf(`"%s"`, s)}
  26. p.Init()
  27. if err := p.Parse(); err != nil {
  28. return nil, err
  29. }
  30. return &Query{str: s, parser: p}, nil
  31. }
  32. // MustParse turns the given string into a query or panics; for tests or others
  33. // cases where you know the string is valid.
  34. func MustParse(s string) *Query {
  35. q, err := New(s)
  36. if err != nil {
  37. panic(fmt.Sprintf("failed to parse %s: %v", s, err))
  38. }
  39. return q
  40. }
  41. // String returns the original string.
  42. func (q *Query) String() string {
  43. return q.str
  44. }
  45. type operator uint8
  46. const (
  47. opLessEqual operator = iota
  48. opGreaterEqual
  49. opLess
  50. opGreater
  51. opEqual
  52. opContains
  53. )
  54. // Matches returns true if the query matches the given set of tags, false otherwise.
  55. //
  56. // For example, query "name=John" matches tags = {"name": "John"}. More
  57. // examples could be found in parser_test.go and query_test.go.
  58. func (q *Query) Matches(tags map[string]interface{}) bool {
  59. if len(tags) == 0 {
  60. return false
  61. }
  62. buffer, begin, end := q.parser.Buffer, 0, 0
  63. var tag string
  64. var op operator
  65. // tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7")
  66. for _, token := range q.parser.Tokens() {
  67. switch token.pegRule {
  68. case rulePegText:
  69. begin, end = int(token.begin), int(token.end)
  70. case ruletag:
  71. tag = buffer[begin:end]
  72. case rulele:
  73. op = opLessEqual
  74. case rulege:
  75. op = opGreaterEqual
  76. case rulel:
  77. op = opLess
  78. case ruleg:
  79. op = opGreater
  80. case ruleequal:
  81. op = opEqual
  82. case rulecontains:
  83. op = opContains
  84. case rulevalue:
  85. // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
  86. valueWithoutSingleQuotes := buffer[begin+1 : end-1]
  87. // see if the triplet (tag, operator, operand) matches any tag
  88. // "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" }
  89. if !match(tag, op, reflect.ValueOf(valueWithoutSingleQuotes), tags) {
  90. return false
  91. }
  92. case rulenumber:
  93. number := buffer[begin:end]
  94. if strings.Contains(number, ".") { // if it looks like a floating-point number
  95. value, err := strconv.ParseFloat(number, 64)
  96. if err != nil {
  97. panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number))
  98. }
  99. if !match(tag, op, reflect.ValueOf(value), tags) {
  100. return false
  101. }
  102. } else {
  103. value, err := strconv.ParseInt(number, 10, 64)
  104. if err != nil {
  105. panic(fmt.Sprintf("got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number))
  106. }
  107. if !match(tag, op, reflect.ValueOf(value), tags) {
  108. return false
  109. }
  110. }
  111. case ruletime:
  112. value, err := time.Parse(time.RFC3339, buffer[begin:end])
  113. if err != nil {
  114. panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end]))
  115. }
  116. if !match(tag, op, reflect.ValueOf(value), tags) {
  117. return false
  118. }
  119. case ruledate:
  120. value, err := time.Parse("2006-01-02", buffer[begin:end])
  121. if err != nil {
  122. panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)", err, buffer[begin:end]))
  123. }
  124. if !match(tag, op, reflect.ValueOf(value), tags) {
  125. return false
  126. }
  127. }
  128. }
  129. return true
  130. }
  131. // match returns true if the given triplet (tag, operator, operand) matches any tag.
  132. //
  133. // First, it looks up the tag in tags and if it finds one, tries to compare the
  134. // value from it to the operand using the operator.
  135. //
  136. // "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" }
  137. func match(tag string, op operator, operand reflect.Value, tags map[string]interface{}) bool {
  138. // look up the tag from the query in tags
  139. value, ok := tags[tag]
  140. if !ok {
  141. return false
  142. }
  143. switch operand.Kind() {
  144. case reflect.Struct: // time
  145. operandAsTime := operand.Interface().(time.Time)
  146. v, ok := value.(time.Time)
  147. if !ok { // if value from tags is not time.Time
  148. return false
  149. }
  150. switch op {
  151. case opLessEqual:
  152. return v.Before(operandAsTime) || v.Equal(operandAsTime)
  153. case opGreaterEqual:
  154. return v.Equal(operandAsTime) || v.After(operandAsTime)
  155. case opLess:
  156. return v.Before(operandAsTime)
  157. case opGreater:
  158. return v.After(operandAsTime)
  159. case opEqual:
  160. return v.Equal(operandAsTime)
  161. }
  162. case reflect.Float64:
  163. operandFloat64 := operand.Interface().(float64)
  164. var v float64
  165. // try our best to convert value from tags to float64
  166. switch vt := value.(type) {
  167. case float64:
  168. v = vt
  169. case float32:
  170. v = float64(vt)
  171. case int:
  172. v = float64(vt)
  173. case int8:
  174. v = float64(vt)
  175. case int16:
  176. v = float64(vt)
  177. case int32:
  178. v = float64(vt)
  179. case int64:
  180. v = float64(vt)
  181. default: // fail for all other types
  182. panic(fmt.Sprintf("Incomparable types: %T (%v) vs float64 (%v)", value, value, operandFloat64))
  183. }
  184. switch op {
  185. case opLessEqual:
  186. return v <= operandFloat64
  187. case opGreaterEqual:
  188. return v >= operandFloat64
  189. case opLess:
  190. return v < operandFloat64
  191. case opGreater:
  192. return v > operandFloat64
  193. case opEqual:
  194. return v == operandFloat64
  195. }
  196. case reflect.Int64:
  197. operandInt := operand.Interface().(int64)
  198. var v int64
  199. // try our best to convert value from tags to int64
  200. switch vt := value.(type) {
  201. case int64:
  202. v = vt
  203. case int8:
  204. v = int64(vt)
  205. case int16:
  206. v = int64(vt)
  207. case int32:
  208. v = int64(vt)
  209. case int:
  210. v = int64(vt)
  211. case float64:
  212. v = int64(vt)
  213. case float32:
  214. v = int64(vt)
  215. default: // fail for all other types
  216. panic(fmt.Sprintf("Incomparable types: %T (%v) vs int64 (%v)", value, value, operandInt))
  217. }
  218. switch op {
  219. case opLessEqual:
  220. return v <= operandInt
  221. case opGreaterEqual:
  222. return v >= operandInt
  223. case opLess:
  224. return v < operandInt
  225. case opGreater:
  226. return v > operandInt
  227. case opEqual:
  228. return v == operandInt
  229. }
  230. case reflect.String:
  231. v, ok := value.(string)
  232. if !ok { // if value from tags is not string
  233. return false
  234. }
  235. switch op {
  236. case opEqual:
  237. return v == operand.String()
  238. case opContains:
  239. return strings.Contains(v, operand.String())
  240. }
  241. default:
  242. panic(fmt.Sprintf("Unknown kind of operand %v", operand.Kind()))
  243. }
  244. return false
  245. }