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.

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