123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- // Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
- // released under the MIT license
-
- package history
-
- import (
- "github.com/ergochat/ergo/irc/utils"
- "sync"
- "time"
- )
-
- type ItemType uint
-
- const (
- uninitializedItem ItemType = iota
- Privmsg
- Notice
- Join
- Part
- Kick
- Quit
- Mode
- Tagmsg
- Nick
- Topic
- Invite
- )
-
- const (
- initialAutoSize = 32
- )
-
- // Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
- type Item struct {
- Type ItemType
-
- Nick string
- // this is the uncasefolded account name, if there's no account it should be set to "*"
- AccountName string
- // for non-privmsg items, we may stuff some other data in here
- Message utils.SplitMessage
- Tags map[string]string
- Params [1]string
- // for a DM, this is the casefolded nickname of the other party (whether this is
- // an incoming or outgoing message). this lets us emulate the "query buffer" functionality
- // required by CHATHISTORY:
- CfCorrespondent string `json:"CfCorrespondent,omitempty"`
- IsBot bool `json:"IsBot,omitempty"`
- }
-
- // HasMsgid tests whether a message has the message id `msgid`.
- func (item *Item) HasMsgid(msgid string) bool {
- return item.Message.Msgid == msgid
- }
-
- type Predicate func(item *Item) (matches bool)
-
- func Reverse(results []Item) {
- for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
- results[i], results[j] = results[j], results[i]
- }
- }
-
- // Buffer is a ring buffer holding message/event history for a channel or user
- type Buffer struct {
- sync.RWMutex
-
- // ring buffer, see irc/whowas.go for conventions
- buffer []Item
- start int
- end int
- maximumSize int
- window time.Duration
-
- lastDiscarded time.Time
-
- nowFunc func() time.Time
- }
-
- func NewHistoryBuffer(size int, window time.Duration) (result *Buffer) {
- result = new(Buffer)
- result.Initialize(size, window)
- return
- }
-
- func (hist *Buffer) Initialize(size int, window time.Duration) {
- hist.buffer = make([]Item, hist.initialSize(size, window))
- hist.start = -1
- hist.end = -1
- hist.window = window
- hist.maximumSize = size
- hist.nowFunc = time.Now
- }
-
- // compute the initial size for the buffer, taking into account autoresize
- func (hist *Buffer) initialSize(size int, window time.Duration) (result int) {
- result = size
- if window != 0 {
- result = initialAutoSize
- if size < result {
- result = size // min(initialAutoSize, size)
- }
- }
- return
- }
-
- // Add adds a history item to the buffer
- func (list *Buffer) Add(item Item) {
- if item.Message.Time.IsZero() {
- item.Message.Time = time.Now().UTC()
- }
-
- list.Lock()
- defer list.Unlock()
-
- if len(list.buffer) == 0 {
- return
- }
-
- list.maybeExpand()
-
- var pos int
- if list.start == -1 { // empty
- pos = 0
- list.start = 0
- list.end = 1 % len(list.buffer)
- } else if list.start != list.end { // partially full
- pos = list.end
- list.end = (list.end + 1) % len(list.buffer)
- } else if list.start == list.end { // full
- pos = list.end
- list.end = (list.end + 1) % len(list.buffer)
- list.start = list.end // advance start as well, overwriting first entry
- // record the timestamp of the overwritten item
- if list.lastDiscarded.Before(list.buffer[pos].Message.Time) {
- list.lastDiscarded = list.buffer[pos].Message.Time
- }
- }
-
- list.buffer[pos] = item
- }
-
- func (list *Buffer) lookup(msgid string) (result Item, found bool) {
- predicate := func(item *Item) bool {
- return item.HasMsgid(msgid)
- }
- results := list.matchInternal(predicate, false, 1)
- if len(results) != 0 {
- return results[0], true
- }
- return
- }
-
- // Between returns all history items with a time `after` <= time <= `before`,
- // with an indication of whether the results are complete or are missing items
- // because some of that period was discarded. A zero value of `before` is considered
- // higher than all other times.
- func (list *Buffer) betweenHelper(start, end Selector, cutoff time.Time, pred Predicate, limit int) (results []Item, complete bool, err error) {
- var ascending bool
-
- defer func() {
- if !ascending {
- Reverse(results)
- }
- }()
-
- list.RLock()
- defer list.RUnlock()
-
- if len(list.buffer) == 0 {
- return
- }
-
- after := start.Time
- if start.Msgid != "" {
- item, found := list.lookup(start.Msgid)
- if !found {
- return
- }
- after = item.Message.Time
- }
- before := end.Time
- if end.Msgid != "" {
- item, found := list.lookup(end.Msgid)
- if !found {
- return
- }
- before = item.Message.Time
- }
-
- after, before, ascending = MinMaxAsc(after, before, cutoff)
-
- complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
-
- satisfies := func(item *Item) bool {
- return (after.IsZero() || item.Message.Time.After(after)) &&
- (before.IsZero() || item.Message.Time.Before(before)) &&
- (pred == nil || pred(item))
- }
-
- return list.matchInternal(satisfies, ascending, limit), complete, nil
- }
-
- // returns all correspondents, in reverse time order
- func (list *Buffer) allCorrespondents() (results []TargetListing) {
- seen := make(utils.StringSet)
-
- list.RLock()
- defer list.RUnlock()
- if list.start == -1 || len(list.buffer) == 0 {
- return
- }
-
- // XXX traverse in reverse order, so we get the latest timestamp
- // of any message sent to/from the correspondent
- pos := list.prev(list.end)
- stop := list.start
-
- for {
- if !seen.Has(list.buffer[pos].CfCorrespondent) {
- seen.Add(list.buffer[pos].CfCorrespondent)
- results = append(results, TargetListing{
- CfName: list.buffer[pos].CfCorrespondent,
- Time: list.buffer[pos].Message.Time,
- })
- }
-
- if pos == stop {
- break
- }
- pos = list.prev(pos)
- }
- return
- }
-
- // list DM correspondents, as one input to CHATHISTORY TARGETS
- func (list *Buffer) listCorrespondents(start, end Selector, cutoff time.Time, limit int) (results []TargetListing, err error) {
- after := start.Time
- before := end.Time
- after, before, ascending := MinMaxAsc(after, before, cutoff)
-
- correspondents := list.allCorrespondents()
- if len(correspondents) == 0 {
- return
- }
-
- // XXX allCorrespondents returns results in reverse order,
- // so if we're ascending, we actually go backwards
- var i int
- if ascending {
- i = len(correspondents) - 1
- } else {
- i = 0
- }
-
- for 0 <= i && i < len(correspondents) && (limit == 0 || len(results) < limit) {
- if (after.IsZero() || correspondents[i].Time.After(after)) &&
- (before.IsZero() || correspondents[i].Time.Before(before)) {
- results = append(results, correspondents[i])
- }
-
- if ascending {
- i--
- } else {
- i++
- }
- }
-
- if !ascending {
- ReverseCorrespondents(results)
- }
-
- return
- }
-
- // implements history.Sequence, emulating a single history buffer (for a channel,
- // a single user's DMs, or a DM conversation)
- type bufferSequence struct {
- list *Buffer
- pred Predicate
- cutoff time.Time
- }
-
- func (list *Buffer) MakeSequence(correspondent string, cutoff time.Time) Sequence {
- var pred Predicate
- if correspondent != "" {
- pred = func(item *Item) bool {
- return item.CfCorrespondent == correspondent
- }
- }
- return &bufferSequence{
- list: list,
- pred: pred,
- cutoff: cutoff,
- }
- }
-
- func (seq *bufferSequence) Between(start, end Selector, limit int) (results []Item, err error) {
- results, _, err = seq.list.betweenHelper(start, end, seq.cutoff, seq.pred, limit)
- return
- }
-
- func (seq *bufferSequence) Around(start Selector, limit int) (results []Item, err error) {
- return GenericAround(seq, start, limit)
- }
-
- func (seq *bufferSequence) ListCorrespondents(start, end Selector, limit int) (results []TargetListing, err error) {
- return seq.list.listCorrespondents(start, end, seq.cutoff, limit)
- }
-
- func (seq *bufferSequence) Cutoff() time.Time {
- return seq.cutoff
- }
-
- func (seq *bufferSequence) Ephemeral() bool {
- return true
- }
-
- // you must be holding the read lock to call this
- func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
- if list.start == -1 || len(list.buffer) == 0 {
- return
- }
-
- var pos, stop int
- if ascending {
- pos = list.start
- stop = list.prev(list.end)
- } else {
- pos = list.prev(list.end)
- stop = list.start
- }
-
- for {
- if predicate(&list.buffer[pos]) {
- results = append(results, list.buffer[pos])
- }
- if pos == stop || (limit != 0 && len(results) == limit) {
- break
- }
- if ascending {
- pos = list.next(pos)
- } else {
- pos = list.prev(pos)
- }
- }
-
- return
- }
-
- // Delete deletes messages matching some predicate.
- func (list *Buffer) Delete(predicate Predicate) (count int) {
- list.Lock()
- defer list.Unlock()
-
- if list.start == -1 || len(list.buffer) == 0 {
- return
- }
-
- pos := list.start
- stop := list.prev(list.end)
-
- for {
- if predicate(&list.buffer[pos]) {
- list.buffer[pos] = Item{}
- count++
- }
- if pos == stop {
- break
- }
- pos = list.next(pos)
- }
-
- return
- }
-
- // latest returns the items most recently added, up to `limit`. If `limit` is 0,
- // it returns all items.
- func (list *Buffer) latest(limit int) (results []Item) {
- results, _, _ = list.betweenHelper(Selector{}, Selector{}, time.Time{}, nil, limit)
- return
- }
-
- // LastDiscarded returns the latest time of any entry that was evicted
- // from the ring buffer.
- func (list *Buffer) LastDiscarded() time.Time {
- list.RLock()
- defer list.RUnlock()
-
- return list.lastDiscarded
- }
-
- func (list *Buffer) prev(index int) int {
- switch index {
- case 0:
- return len(list.buffer) - 1
- default:
- return index - 1
- }
- }
-
- func (list *Buffer) next(index int) int {
- switch index {
- case len(list.buffer) - 1:
- return 0
- default:
- return index + 1
- }
- }
-
- func (list *Buffer) maybeExpand() {
- if list.window == 0 {
- return // autoresize is disabled
- }
-
- length := list.length()
- if length < len(list.buffer) {
- return // we have spare capacity already
- }
-
- if len(list.buffer) == list.maximumSize {
- return // cannot expand any further
- }
-
- wouldDiscard := list.buffer[list.start].Message.Time
- if list.window < list.nowFunc().Sub(wouldDiscard) {
- return // oldest element is old enough to overwrite
- }
-
- newSize := utils.RoundUpToPowerOfTwo(length + 1)
- if list.maximumSize < newSize {
- newSize = list.maximumSize
- }
- list.resize(newSize)
- }
-
- // Resize shrinks or expands the buffer
- func (list *Buffer) Resize(maximumSize int, window time.Duration) {
- list.Lock()
- defer list.Unlock()
-
- if list.maximumSize == maximumSize && list.window == window {
- return // no-op
- }
-
- list.maximumSize = maximumSize
- list.window = window
-
- // three cases where we need to preemptively resize:
- // (1) we are not autoresizing
- // (2) the buffer is currently larger than maximumSize and needs to be shrunk
- // (3) the buffer is currently smaller than the recommended initial size
- // (including the case where it is currently disabled and needs to be enabled)
- // TODO make it possible to shrink the buffer so that it only contains `window`
- if window == 0 || maximumSize < len(list.buffer) {
- list.resize(maximumSize)
- } else {
- initialSize := list.initialSize(maximumSize, window)
- if len(list.buffer) < initialSize {
- list.resize(initialSize)
- }
- }
- }
-
- func (list *Buffer) resize(size int) {
- newbuffer := make([]Item, size)
-
- if list.start == -1 {
- // indices are already correct and nothing needs to be copied
- } else if size == 0 {
- // this is now the empty list
- list.start = -1
- list.end = -1
- } else {
- currentLength := list.length()
- start := list.start
- end := list.end
- // if we're truncating, keep the latest entries, not the earliest
- if size < currentLength {
- start = list.end - size
- if start < 0 {
- start += len(list.buffer)
- }
- // update lastDiscarded for discarded entries
- for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
- if list.lastDiscarded.Before(list.buffer[i].Message.Time) {
- list.lastDiscarded = list.buffer[i].Message.Time
- }
- }
- }
- if start < end {
- copied := copy(newbuffer, list.buffer[start:end])
- list.start = 0
- list.end = copied % size
- } else {
- lenInitial := len(list.buffer) - start
- copied := copy(newbuffer, list.buffer[start:])
- copied += copy(newbuffer[lenInitial:], list.buffer[:end])
- list.start = 0
- list.end = copied % size
- }
- }
-
- list.buffer = newbuffer
- }
-
- func (hist *Buffer) length() int {
- if hist.start == -1 {
- return 0
- } else if hist.start < hist.end {
- return hist.end - hist.start
- } else {
- return len(hist.buffer) - (hist.start - hist.end)
- }
- }
|