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.

history.go 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. // Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package history
  4. import (
  5. "github.com/oragono/oragono/irc/utils"
  6. "sync"
  7. "time"
  8. )
  9. type ItemType uint
  10. const (
  11. uninitializedItem ItemType = iota
  12. Privmsg
  13. Notice
  14. Join
  15. Part
  16. Kick
  17. Quit
  18. Mode
  19. Tagmsg
  20. Nick
  21. Topic
  22. Invite
  23. )
  24. const (
  25. initialAutoSize = 32
  26. )
  27. // Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
  28. type Item struct {
  29. Type ItemType
  30. Nick string
  31. // this is the uncasefolded account name, if there's no account it should be set to "*"
  32. AccountName string
  33. // for non-privmsg items, we may stuff some other data in here
  34. Message utils.SplitMessage
  35. Tags map[string]string
  36. Params [1]string
  37. // for a DM, this is the casefolded nickname of the other party (whether this is
  38. // an incoming or outgoing message). this lets us emulate the "query buffer" functionality
  39. // required by CHATHISTORY:
  40. CfCorrespondent string
  41. }
  42. // HasMsgid tests whether a message has the message id `msgid`.
  43. func (item *Item) HasMsgid(msgid string) bool {
  44. return item.Message.Msgid == msgid
  45. }
  46. type Predicate func(item *Item) (matches bool)
  47. func Reverse(results []Item) {
  48. for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
  49. results[i], results[j] = results[j], results[i]
  50. }
  51. }
  52. // Buffer is a ring buffer holding message/event history for a channel or user
  53. type Buffer struct {
  54. sync.RWMutex
  55. // ring buffer, see irc/whowas.go for conventions
  56. buffer []Item
  57. start int
  58. end int
  59. maximumSize int
  60. window time.Duration
  61. lastDiscarded time.Time
  62. nowFunc func() time.Time
  63. }
  64. func NewHistoryBuffer(size int, window time.Duration) (result *Buffer) {
  65. result = new(Buffer)
  66. result.Initialize(size, window)
  67. return
  68. }
  69. func (hist *Buffer) Initialize(size int, window time.Duration) {
  70. hist.buffer = make([]Item, hist.initialSize(size, window))
  71. hist.start = -1
  72. hist.end = -1
  73. hist.window = window
  74. hist.maximumSize = size
  75. hist.nowFunc = time.Now
  76. }
  77. // compute the initial size for the buffer, taking into account autoresize
  78. func (hist *Buffer) initialSize(size int, window time.Duration) (result int) {
  79. result = size
  80. if window != 0 {
  81. result = initialAutoSize
  82. if size < result {
  83. result = size // min(initialAutoSize, size)
  84. }
  85. }
  86. return
  87. }
  88. // Add adds a history item to the buffer
  89. func (list *Buffer) Add(item Item) {
  90. if item.Message.Time.IsZero() {
  91. item.Message.Time = time.Now().UTC()
  92. }
  93. list.Lock()
  94. defer list.Unlock()
  95. if len(list.buffer) == 0 {
  96. return
  97. }
  98. list.maybeExpand()
  99. var pos int
  100. if list.start == -1 { // empty
  101. pos = 0
  102. list.start = 0
  103. list.end = 1 % len(list.buffer)
  104. } else if list.start != list.end { // partially full
  105. pos = list.end
  106. list.end = (list.end + 1) % len(list.buffer)
  107. } else if list.start == list.end { // full
  108. pos = list.end
  109. list.end = (list.end + 1) % len(list.buffer)
  110. list.start = list.end // advance start as well, overwriting first entry
  111. // record the timestamp of the overwritten item
  112. if list.lastDiscarded.Before(list.buffer[pos].Message.Time) {
  113. list.lastDiscarded = list.buffer[pos].Message.Time
  114. }
  115. }
  116. list.buffer[pos] = item
  117. }
  118. func (list *Buffer) lookup(msgid string) (result Item, found bool) {
  119. predicate := func(item *Item) bool {
  120. return item.HasMsgid(msgid)
  121. }
  122. results := list.matchInternal(predicate, false, 1)
  123. if len(results) != 0 {
  124. return results[0], true
  125. }
  126. return
  127. }
  128. // Between returns all history items with a time `after` <= time <= `before`,
  129. // with an indication of whether the results are complete or are missing items
  130. // because some of that period was discarded. A zero value of `before` is considered
  131. // higher than all other times.
  132. func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int) (results []Item, complete bool, err error) {
  133. var ascending bool
  134. defer func() {
  135. if !ascending {
  136. Reverse(results)
  137. }
  138. }()
  139. list.RLock()
  140. defer list.RUnlock()
  141. if len(list.buffer) == 0 {
  142. return
  143. }
  144. after := start.Time
  145. if start.Msgid != "" {
  146. item, found := list.lookup(start.Msgid)
  147. if !found {
  148. return
  149. }
  150. after = item.Message.Time
  151. }
  152. before := end.Time
  153. if end.Msgid != "" {
  154. item, found := list.lookup(end.Msgid)
  155. if !found {
  156. return
  157. }
  158. before = item.Message.Time
  159. }
  160. after, before, ascending = MinMaxAsc(after, before, cutoff)
  161. complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
  162. satisfies := func(item *Item) bool {
  163. return (after.IsZero() || item.Message.Time.After(after)) &&
  164. (before.IsZero() || item.Message.Time.Before(before)) &&
  165. (pred == nil || pred(item))
  166. }
  167. return list.matchInternal(satisfies, ascending, limit), complete, nil
  168. }
  169. // implements history.Sequence, emulating a single history buffer (for a channel,
  170. // a single user's DMs, or a DM conversation)
  171. type bufferSequence struct {
  172. list *Buffer
  173. pred Predicate
  174. cutoff time.Time
  175. }
  176. func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequence {
  177. var pred Predicate
  178. if correspondent != "" {
  179. pred = func(item *Item) bool {
  180. return item.CfCorrespondent == correspondent
  181. }
  182. }
  183. return &bufferSequence{
  184. list: list,
  185. pred: pred,
  186. cutoff: cutoff,
  187. }
  188. }
  189. func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, complete bool, err error) {
  190. return seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
  191. }
  192. func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
  193. return GenericAround(seq, start, limit)
  194. }
  195. // you must be holding the read lock to call this
  196. func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
  197. if list.start == -1 || len(list.buffer) == 0 {
  198. return
  199. }
  200. var pos, stop int
  201. if ascending {
  202. pos = list.start
  203. stop = list.prev(list.end)
  204. } else {
  205. pos = list.prev(list.end)
  206. stop = list.start
  207. }
  208. for {
  209. if predicate(&list.buffer[pos]) {
  210. results = append(results, list.buffer[pos])
  211. }
  212. if pos == stop || (limit != 0 && len(results) == limit) {
  213. break
  214. }
  215. if ascending {
  216. pos = list.next(pos)
  217. } else {
  218. pos = list.prev(pos)
  219. }
  220. }
  221. return
  222. }
  223. // Delete deletes messages matching some predicate.
  224. func (list *Buffer) Delete(predicate Predicate) (count int) {
  225. list.Lock()
  226. defer list.Unlock()
  227. if list.start == -1 || len(list.buffer) == 0 {
  228. return
  229. }
  230. pos := list.start
  231. stop := list.prev(list.end)
  232. for {
  233. if predicate(&list.buffer[pos]) {
  234. list.buffer[pos] = Item{}
  235. count++
  236. }
  237. if pos == stop {
  238. break
  239. }
  240. pos = list.next(pos)
  241. }
  242. return
  243. }
  244. // latest returns the items most recently added, up to `limit`. If `limit` is 0,
  245. // it returns all items.
  246. func (list *Buffer) latest(limit int) (results []Item) {
  247. results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit)
  248. return
  249. }
  250. // LastDiscarded returns the latest time of any entry that was evicted
  251. // from the ring buffer.
  252. func (list *Buffer) LastDiscarded() time.Time {
  253. list.RLock()
  254. defer list.RUnlock()
  255. return list.lastDiscarded
  256. }
  257. func (list *Buffer) prev(index int) int {
  258. switch index {
  259. case 0:
  260. return len(list.buffer) - 1
  261. default:
  262. return index - 1
  263. }
  264. }
  265. func (list *Buffer) next(index int) int {
  266. switch index {
  267. case len(list.buffer) - 1:
  268. return 0
  269. default:
  270. return index + 1
  271. }
  272. }
  273. func (list *Buffer) maybeExpand() {
  274. if list.window == 0 {
  275. return // autoresize is disabled
  276. }
  277. length := list.length()
  278. if length < len(list.buffer) {
  279. return // we have spare capacity already
  280. }
  281. if len(list.buffer) == list.maximumSize {
  282. return // cannot expand any further
  283. }
  284. wouldDiscard := list.buffer[list.start].Message.Time
  285. if list.window < list.nowFunc().Sub(wouldDiscard) {
  286. return // oldest element is old enough to overwrite
  287. }
  288. newSize := utils.RoundUpToPowerOfTwo(length + 1)
  289. if list.maximumSize < newSize {
  290. newSize = list.maximumSize
  291. }
  292. list.resize(newSize)
  293. }
  294. // Resize shrinks or expands the buffer
  295. func (list *Buffer) Resize(maximumSize int, window time.Duration) {
  296. list.Lock()
  297. defer list.Unlock()
  298. if list.maximumSize == maximumSize && list.window == window {
  299. return // no-op
  300. }
  301. list.maximumSize = maximumSize
  302. list.window = window
  303. // three cases where we need to preemptively resize:
  304. // (1) we are not autoresizing
  305. // (2) the buffer is currently larger than maximumSize and needs to be shrunk
  306. // (3) the buffer is currently smaller than the recommended initial size
  307. // (including the case where it is currently disabled and needs to be enabled)
  308. // TODO make it possible to shrink the buffer so that it only contains `window`
  309. if window == 0 || maximumSize < len(list.buffer) {
  310. list.resize(maximumSize)
  311. } else {
  312. initialSize := list.initialSize(maximumSize, window)
  313. if len(list.buffer) < initialSize {
  314. list.resize(initialSize)
  315. }
  316. }
  317. }
  318. func (list *Buffer) resize(size int) {
  319. newbuffer := make([]Item, size)
  320. if list.start == -1 {
  321. // indices are already correct and nothing needs to be copied
  322. } else if size == 0 {
  323. // this is now the empty list
  324. list.start = -1
  325. list.end = -1
  326. } else {
  327. currentLength := list.length()
  328. start := list.start
  329. end := list.end
  330. // if we're truncating, keep the latest entries, not the earliest
  331. if size < currentLength {
  332. start = list.end - size
  333. if start < 0 {
  334. start += len(list.buffer)
  335. }
  336. // update lastDiscarded for discarded entries
  337. for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
  338. if list.lastDiscarded.Before(list.buffer[i].Message.Time) {
  339. list.lastDiscarded = list.buffer[i].Message.Time
  340. }
  341. }
  342. }
  343. if start < end {
  344. copied := copy(newbuffer, list.buffer[start:end])
  345. list.start = 0
  346. list.end = copied % size
  347. } else {
  348. lenInitial := len(list.buffer) - start
  349. copied := copy(newbuffer, list.buffer[start:])
  350. copied += copy(newbuffer[lenInitial:], list.buffer[:end])
  351. list.start = 0
  352. list.end = copied % size
  353. }
  354. }
  355. list.buffer = newbuffer
  356. }
  357. func (hist *Buffer) length() int {
  358. if hist.start == -1 {
  359. return 0
  360. } else if hist.start < hist.end {
  361. return hist.end - hist.start
  362. } else {
  363. return len(hist.buffer) - (hist.start - hist.end)
  364. }
  365. }