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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. // Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  2. // released under the MIT license
  3. package history
  4. import (
  5. "github.com/ergochat/ergo/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 `json:"CfCorrespondent,omitempty"`
  41. IsBot bool `json:"IsBot,omitempty"`
  42. }
  43. // HasMsgid tests whether a message has the message id `msgid`.
  44. func (item *Item) HasMsgid(msgid string) bool {
  45. return item.Message.Msgid == msgid
  46. }
  47. func (item *Item) IsExcluded(excludeFlags ExcludeFlags) bool {
  48. switch item.Type {
  49. case Tagmsg:
  50. return excludeFlags&ExcludeTagmsg != 0
  51. case Join, Part, Quit:
  52. return excludeFlags&ExcludeJoins != 0
  53. default:
  54. return false
  55. }
  56. }
  57. type Predicate func(item *Item) (matches bool)
  58. func Reverse(results []Item) {
  59. for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
  60. results[i], results[j] = results[j], results[i]
  61. }
  62. }
  63. // Buffer is a ring buffer holding message/event history for a channel or user
  64. type Buffer struct {
  65. sync.RWMutex
  66. // ring buffer, see irc/whowas.go for conventions
  67. buffer []Item
  68. start int
  69. end int
  70. maximumSize int
  71. window time.Duration
  72. lastDiscarded time.Time
  73. nowFunc func() time.Time
  74. }
  75. func NewHistoryBuffer(size int, window time.Duration) (result *Buffer) {
  76. result = new(Buffer)
  77. result.Initialize(size, window)
  78. return
  79. }
  80. func (hist *Buffer) Initialize(size int, window time.Duration) {
  81. hist.buffer = make([]Item, hist.initialSize(size, window))
  82. hist.start = -1
  83. hist.end = -1
  84. hist.window = window
  85. hist.maximumSize = size
  86. hist.nowFunc = time.Now
  87. }
  88. // compute the initial size for the buffer, taking into account autoresize
  89. func (hist *Buffer) initialSize(size int, window time.Duration) (result int) {
  90. result = size
  91. if window != 0 {
  92. result = initialAutoSize
  93. if size < result {
  94. result = size // min(initialAutoSize, size)
  95. }
  96. }
  97. return
  98. }
  99. // Add adds a history item to the buffer
  100. func (list *Buffer) Add(item Item) {
  101. if item.Message.Time.IsZero() {
  102. item.Message.Time = time.Now().UTC()
  103. }
  104. list.Lock()
  105. defer list.Unlock()
  106. if len(list.buffer) == 0 {
  107. return
  108. }
  109. list.maybeExpand()
  110. var pos int
  111. if list.start == -1 { // empty
  112. pos = 0
  113. list.start = 0
  114. list.end = 1 % len(list.buffer)
  115. } else if list.start != list.end { // partially full
  116. pos = list.end
  117. list.end = (list.end + 1) % len(list.buffer)
  118. } else if list.start == list.end { // full
  119. pos = list.end
  120. list.end = (list.end + 1) % len(list.buffer)
  121. list.start = list.end // advance start as well, overwriting first entry
  122. // record the timestamp of the overwritten item
  123. if list.lastDiscarded.Before(list.buffer[pos].Message.Time) {
  124. list.lastDiscarded = list.buffer[pos].Message.Time
  125. }
  126. }
  127. list.buffer[pos] = item
  128. }
  129. func (list *Buffer) lookup(msgid string) (result Item, found bool) {
  130. predicate := func(item *Item) bool {
  131. return item.HasMsgid(msgid)
  132. }
  133. results := list.matchInternal(predicate, false, 1)
  134. if len(results) != 0 {
  135. return results[0], true
  136. }
  137. return
  138. }
  139. // Between returns all history items with a time `after` <= time <= `before`,
  140. // with an indication of whether the results are complete or are missing items
  141. // because some of that period was discarded. A zero value of `before` is considered
  142. // higher than all other times.
  143. func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int, excludeFlags ExcludeFlags) (results []Item, complete bool, err error) {
  144. var ascending bool
  145. defer func() {
  146. if !ascending {
  147. Reverse(results)
  148. }
  149. }()
  150. list.RLock()
  151. defer list.RUnlock()
  152. if len(list.buffer) == 0 {
  153. return
  154. }
  155. after := start.Time
  156. if start.Msgid != "" {
  157. item, found := list.lookup(start.Msgid)
  158. if !found {
  159. return
  160. }
  161. after = item.Message.Time
  162. }
  163. before := end.Time
  164. if end.Msgid != "" {
  165. item, found := list.lookup(end.Msgid)
  166. if !found {
  167. return
  168. }
  169. before = item.Message.Time
  170. }
  171. after, before, ascending = MinMaxAsc(after, before, cutoff)
  172. complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
  173. satisfies := func(item *Item) bool {
  174. return (after.IsZero() || item.Message.Time.After(after)) &&
  175. (before.IsZero() || item.Message.Time.Before(before)) &&
  176. (pred == nil || pred(item)) &&
  177. !item.IsExcluded(excludeFlags)
  178. }
  179. return list.matchInternal(satisfies, ascending, limit), complete, nil
  180. }
  181. // returns all correspondents, in reverse time order
  182. func (list *Buffer) allCorrespondents() (results []TargetListing) {
  183. seen := make(utils.StringSet)
  184. list.RLock()
  185. defer list.RUnlock()
  186. if list.start == -1 || len(list.buffer) == 0 {
  187. return
  188. }
  189. // XXX traverse in reverse order, so we get the latest timestamp
  190. // of any message sent to/from the correspondent
  191. pos := list.prev(list.end)
  192. stop := list.start
  193. for {
  194. if !seen.Has(list.buffer[pos].CfCorrespondent) {
  195. seen.Add(list.buffer[pos].CfCorrespondent)
  196. results = append(results, TargetListing{
  197. CfName: list.buffer[pos].CfCorrespondent,
  198. Time: list.buffer[pos].Message.Time,
  199. })
  200. }
  201. if pos == stop {
  202. break
  203. }
  204. pos = list.prev(pos)
  205. }
  206. return
  207. }
  208. // list DM correspondents, as one input to CHATHISTORY TARGETS
  209. func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []TargetListing, err error) {
  210. after := start.Time
  211. before := end.Time
  212. after, before, ascending := MinMaxAsc(after, before, cutoff)
  213. correspondents := list.allCorrespondents()
  214. if len(correspondents) == 0 {
  215. return
  216. }
  217. // XXX allCorrespondents returns results in reverse order,
  218. // so if we're ascending, we actually go backwards
  219. var i int
  220. if ascending {
  221. i = len(correspondents) - 1
  222. } else {
  223. i = 0
  224. }
  225. for 0 <= i && i < len(correspondents) && (limit == 0 || len(results) < limit) {
  226. if (after.IsZero() || correspondents[i].Time.After(after)) &&
  227. (before.IsZero() || correspondents[i].Time.Before(before)) {
  228. results = append(results, correspondents[i])
  229. }
  230. if ascending {
  231. i--
  232. } else {
  233. i++
  234. }
  235. }
  236. if !ascending {
  237. ReverseCorrespondents(results)
  238. }
  239. return
  240. }
  241. // implements history.Sequence, emulating a single history buffer (for a channel,
  242. // a single user's DMs, or a DM conversation)
  243. type bufferSequence struct {
  244. list *Buffer
  245. pred Predicate
  246. cutoff time.Time
  247. flags ExcludeFlags
  248. }
  249. func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time, flags ExcludeFlags) Sequence {
  250. var pred Predicate
  251. if correspondent != "" {
  252. pred = func(item *Item) bool {
  253. return item.CfCorrespondent == correspondent
  254. }
  255. }
  256. return &bufferSequence{
  257. list: list,
  258. pred: pred,
  259. cutoff: cutoff,
  260. flags: flags,
  261. }
  262. }
  263. func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, err error) {
  264. results, _, err = seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit, seq.flags)
  265. return
  266. }
  267. func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
  268. return GenericAround(seq, start, limit)
  269. }
  270. func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
  271. return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
  272. }
  273. func (seq *bufferSequence) Cutoff() time.Time {
  274. return seq.cutoff
  275. }
  276. func (seq *bufferSequence) Ephemeral() bool {
  277. return true
  278. }
  279. // you must be holding the read lock to call this
  280. func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
  281. if list.start == -1 || len(list.buffer) == 0 {
  282. return
  283. }
  284. var pos, stop int
  285. if ascending {
  286. pos = list.start
  287. stop = list.prev(list.end)
  288. } else {
  289. pos = list.prev(list.end)
  290. stop = list.start
  291. }
  292. for {
  293. if predicate(&list.buffer[pos]) {
  294. results = append(results, list.buffer[pos])
  295. }
  296. if pos == stop || (limit != 0 && len(results) == limit) {
  297. break
  298. }
  299. if ascending {
  300. pos = list.next(pos)
  301. } else {
  302. pos = list.prev(pos)
  303. }
  304. }
  305. return
  306. }
  307. // Delete deletes messages matching some predicate.
  308. func (list *Buffer) Delete(predicate Predicate) (count int) {
  309. list.Lock()
  310. defer list.Unlock()
  311. if list.start == -1 || len(list.buffer) == 0 {
  312. return
  313. }
  314. pos := list.start
  315. stop := list.prev(list.end)
  316. for {
  317. if predicate(&list.buffer[pos]) {
  318. list.buffer[pos] = Item{}
  319. count++
  320. }
  321. if pos == stop {
  322. break
  323. }
  324. pos = list.next(pos)
  325. }
  326. return
  327. }
  328. // latest returns the items most recently added, up to `limit`. If `limit` is 0,
  329. // it returns all items.
  330. func (list *Buffer) latest(limit int) (results []Item) {
  331. results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit, 0)
  332. return
  333. }
  334. // LastDiscarded returns the latest time of any entry that was evicted
  335. // from the ring buffer.
  336. func (list *Buffer) LastDiscarded() time.Time {
  337. list.RLock()
  338. defer list.RUnlock()
  339. return list.lastDiscarded
  340. }
  341. func (list *Buffer) prev(index int) int {
  342. switch index {
  343. case 0:
  344. return len(list.buffer) - 1
  345. default:
  346. return index - 1
  347. }
  348. }
  349. func (list *Buffer) next(index int) int {
  350. switch index {
  351. case len(list.buffer) - 1:
  352. return 0
  353. default:
  354. return index + 1
  355. }
  356. }
  357. func (list *Buffer) maybeExpand() {
  358. if list.window == 0 {
  359. return // autoresize is disabled
  360. }
  361. length := list.length()
  362. if length < len(list.buffer) {
  363. return // we have spare capacity already
  364. }
  365. if len(list.buffer) == list.maximumSize {
  366. return // cannot expand any further
  367. }
  368. wouldDiscard := list.buffer[list.start].Message.Time
  369. if list.window < list.nowFunc().Sub(wouldDiscard) {
  370. return // oldest element is old enough to overwrite
  371. }
  372. newSize := utils.RoundUpToPowerOfTwo(length + 1)
  373. if list.maximumSize < newSize {
  374. newSize = list.maximumSize
  375. }
  376. list.resize(newSize)
  377. }
  378. // Resize shrinks or expands the buffer
  379. func (list *Buffer) Resize(maximumSize int, window time.Duration) {
  380. list.Lock()
  381. defer list.Unlock()
  382. if list.maximumSize == maximumSize && list.window == window {
  383. return // no-op
  384. }
  385. list.maximumSize = maximumSize
  386. list.window = window
  387. // three cases where we need to preemptively resize:
  388. // (1) we are not autoresizing
  389. // (2) the buffer is currently larger than maximumSize and needs to be shrunk
  390. // (3) the buffer is currently smaller than the recommended initial size
  391. // (including the case where it is currently disabled and needs to be enabled)
  392. // TODO make it possible to shrink the buffer so that it only contains `window`
  393. if window == 0 || maximumSize < len(list.buffer) {
  394. list.resize(maximumSize)
  395. } else {
  396. initialSize := list.initialSize(maximumSize, window)
  397. if len(list.buffer) < initialSize {
  398. list.resize(initialSize)
  399. }
  400. }
  401. }
  402. func (list *Buffer) resize(size int) {
  403. newbuffer := make([]Item, size)
  404. if list.start == -1 {
  405. // indices are already correct and nothing needs to be copied
  406. } else if size == 0 {
  407. // this is now the empty list
  408. list.start = -1
  409. list.end = -1
  410. } else {
  411. currentLength := list.length()
  412. start := list.start
  413. end := list.end
  414. // if we're truncating, keep the latest entries, not the earliest
  415. if size < currentLength {
  416. start = list.end - size
  417. if start < 0 {
  418. start += len(list.buffer)
  419. }
  420. // update lastDiscarded for discarded entries
  421. for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
  422. if list.lastDiscarded.Before(list.buffer[i].Message.Time) {
  423. list.lastDiscarded = list.buffer[i].Message.Time
  424. }
  425. }
  426. }
  427. if start < end {
  428. copied := copy(newbuffer, list.buffer[start:end])
  429. list.start = 0
  430. list.end = copied % size
  431. } else {
  432. lenInitial := len(list.buffer) - start
  433. copied := copy(newbuffer, list.buffer[start:])
  434. copied += copy(newbuffer[lenInitial:], list.buffer[:end])
  435. list.start = 0
  436. list.end = copied % size
  437. }
  438. }
  439. list.buffer = newbuffer
  440. }
  441. func (hist *Buffer) length() int {
  442. if hist.start == -1 {
  443. return 0
  444. } else if hist.start < hist.end {
  445. return hist.end - hist.start
  446. } else {
  447. return len(hist.buffer) - (hist.start - hist.end)
  448. }
  449. }