github.com/siglens/siglens@v0.0.0-20240328180423-f7ce9ae441ed/pkg/scroll/scroll.go (about) 1 /* 2 Copyright 2023. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package scroll 18 19 import ( 20 "bytes" 21 "encoding/csv" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "io" 26 "os" 27 "regexp" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/google/uuid" 34 "github.com/siglens/siglens/pkg/config" 35 segutils "github.com/siglens/siglens/pkg/segment/utils" 36 "github.com/siglens/siglens/pkg/utils" 37 log "github.com/sirupsen/logrus" 38 ) 39 40 type Scroll struct { 41 Scroll_id string 42 Results *utils.HttpServerESResponseOuter 43 Size uint64 44 TimeOut uint64 45 Expiry string 46 Offset uint64 47 Valid bool 48 } 49 50 var allScrollRecords = map[string]*Scroll{} 51 var allScrollRecordsLock sync.RWMutex 52 53 func init() { 54 go checkStaleScrollContext() 55 } 56 57 func checkStaleScrollContext() { 58 59 //TODO On init load AllScrollRecords from file 60 61 for { 62 time.Sleep(1 * time.Minute) 63 allScrollRecordsLock.Lock() 64 for scroll_id, scrollRecord := range allScrollRecords { 65 if scrollRecord == nil { 66 continue 67 } 68 if scrollRecord.Valid && isScrollExpired(scrollRecord.TimeOut) { 69 scrollRecord.Valid = false 70 err := scrollRecord.FlushScrollContextToFile() 71 if err != nil { 72 log.Errorf("checkStaleScrollContext: failed to flush scroll context %v, err=%v", scroll_id, err) 73 continue 74 } 75 log.Infof("Scroll Context Expired %v", scroll_id) 76 //delete result file with scroll_id 77 removeScrollResultFile(scroll_id) 78 } 79 } 80 allScrollRecordsLock.Unlock() 81 } 82 } 83 84 func removeScrollResultFile(scroll_id string) { 85 filename := getScrollResultsFilename(getBaseScrollDir(), scroll_id) 86 e := os.Remove(filename) 87 if e != nil { 88 log.Errorf("Error in removeScrollResultFile %v", e) 89 } 90 } 91 92 func isScrollExpired(TimeOut uint64) bool { 93 return TimeOut < segutils.GetCurrentTimeMillis() 94 } 95 96 func getBaseScrollDir() string { 97 98 var sb strings.Builder 99 sb.WriteString(config.GetRunningConfig().DataPath) 100 sb.WriteString(config.GetHostID()) 101 sb.WriteString("/scroll/") 102 basedir := sb.String() 103 return basedir 104 } 105 106 func getScrollFilename(baseDir string) string { 107 var sb strings.Builder 108 109 err := os.MkdirAll(baseDir, 0764) 110 if err != nil { 111 return "" 112 } 113 sb.WriteString(baseDir) 114 sb.WriteString("scroll.csv") 115 return sb.String() 116 } 117 118 func getScrollResultsFilename(baseDir string, scroll_id string) string { 119 var sb strings.Builder 120 121 err := os.MkdirAll(baseDir, 0764) 122 if err != nil { 123 return "" 124 } 125 sb.WriteString(baseDir) 126 sb.WriteString(scroll_id + ".csv") 127 return sb.String() 128 } 129 130 /* 131 Loads scroll from file 132 */ 133 func loadScrollContextFromFile(scroll_id string) (*Scroll, error) { 134 scrollRecord := &Scroll{} 135 filename := getScrollResultsFilename(getBaseScrollDir(), scroll_id) 136 fd, err := os.OpenFile(filename, os.O_RDWR, 0666) 137 if err != nil { 138 return nil, err 139 } 140 defer fd.Close() 141 err = json.NewDecoder(fd).Decode(scrollRecord) 142 return scrollRecord, err 143 } 144 145 func (scroll *Scroll) FlushScrollContextToFile() error { 146 147 filename := getScrollFilename(getBaseScrollDir()) 148 fd, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 149 if err != nil { 150 return err 151 } 152 defer fd.Close() 153 w := csv.NewWriter(fd) 154 var record []string 155 var records [][]string 156 157 record = append(record, scroll.Scroll_id, scroll.Expiry, fmt.Sprint(scroll.Offset), fmt.Sprint(scroll.Size), fmt.Sprint(scroll.TimeOut), fmt.Sprint(scroll.Valid)) 158 records = append(records, record) 159 160 err = w.WriteAll(records) 161 if err != nil { 162 log.Errorf("flushScrollContextToFile: write failed, filename=%v, err=%v", filename, err) 163 return err 164 } 165 w.Flush() 166 return nil 167 } 168 169 func (scrollRecord *Scroll) WriteScrollResultToFile() error { 170 filename1 := getScrollResultsFilename(getBaseScrollDir(), scrollRecord.Scroll_id) 171 fd1, err := os.OpenFile(filename1, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 172 if err != nil { 173 return err 174 } 175 defer fd1.Close() 176 jdata1, err := json.MarshalIndent(scrollRecord, "", " ") 177 if err != nil { 178 log.Errorf("WriteScrollResultToFile error: %v", err) 179 return err 180 } 181 _, err = io.Copy(fd1, bytes.NewReader(jdata1)) 182 if err != nil { 183 log.Errorf("WriteScrollResultToFile: io copy failed, err=%v", err) 184 return err 185 } 186 return nil 187 188 } 189 190 func ForcedFlushToScrollFile() { 191 log.Infof("Flushing scroll context to file") 192 allScrollRecordsLock.Lock() 193 for _, scrollRecord := range allScrollRecords { 194 err := scrollRecord.FlushScrollContextToFile() 195 if err != nil { 196 log.Errorf("Forcedflushtoscrollfile: flush failed, scrollRecord=%v, err=%v", scrollRecord, err) 197 } 198 } 199 allScrollRecordsLock.Unlock() 200 } 201 202 func GetScrollRecord(scroll_id string, timeOut string, sizeLimit uint64) *Scroll { 203 allScrollRecordsLock.Lock() 204 var scrollRecord *Scroll 205 _, present := allScrollRecords[scroll_id] 206 if !present { 207 scroll_id = uuid.New().String() 208 scrollRecord = &Scroll{Scroll_id: scroll_id, Expiry: timeOut, Offset: 0, Size: sizeLimit, Valid: true} 209 allScrollRecords[scroll_id] = scrollRecord 210 } else if present && allScrollRecords[scroll_id].Valid { 211 //read scrollRecord from file 212 scrollRecord, _ = loadScrollContextFromFile(scroll_id) 213 } 214 allScrollRecordsLock.Unlock() 215 return scrollRecord 216 } 217 218 func GetScrollTimeOut(scrollTimeout string, qid uint64) (uint64, error) { 219 var validTimeUnitRegex = regexp.MustCompile(`^([0-9])+(.*)$`) 220 scrollTime := validTimeUnitRegex.FindStringSubmatch(scrollTimeout) 221 scrollExpiry := utils.GetCurrentTimeInMs() 222 if len(scrollTime) >= 3 { 223 if scrollTimeValue, err := strconv.ParseUint(scrollTime[1], 10, 64); err == nil { 224 switch scrollTime[2] { 225 case "d": 226 log.Errorf("qid=%d, InvalidTimeUnit for scroll %v", qid, scrollTime) 227 return 0, errors.New("InvalidTimeUnit for scroll") 228 case "h": 229 scrollExpiry = scrollExpiry + scrollTimeValue*60*60*1000 230 case "m": 231 scrollExpiry = scrollExpiry + scrollTimeValue*60*1000 232 case "s": 233 scrollExpiry = scrollExpiry + scrollTimeValue*1000 234 case "ms": 235 scrollExpiry = scrollExpiry + scrollTimeValue 236 case "micros": 237 log.Errorf("qid=%d, InvalidTimeUnit for scroll %v", qid, scrollTime) 238 return 0, errors.New("InvalidTimeUnit for scroll") 239 case "nanos": 240 log.Errorf("qid=%d, InvalidTimeUnit for scroll %v", qid, scrollTime) 241 return 0, errors.New("InvalidTimeUnit for scroll") 242 default: 243 log.Errorf("qid=%d, InvalidTimeUnit for scroll %v ", qid, scrollTime) 244 return 0, errors.New("InvalidTimeUnit for scroll") 245 } 246 } else { 247 log.Errorf("qid=%d, InvalidTimeUnit for scroll %v ", qid, scrollTime) 248 return 0, errors.New("InvalidTimeUnit for scroll") 249 } 250 } else { 251 log.Errorf("qid=%d, InvalidTimeUnit for scroll %v ", qid, scrollTime) 252 return 0, errors.New("InvalidTimeUnit for scroll") 253 } 254 255 return scrollExpiry, nil 256 257 } 258 259 /* 260 Returns if the scroll id is valid. 261 262 False if does not exist or is not valid 263 */ 264 func IsScrollIdValid(scrollId string) bool { 265 allScrollRecordsLock.RLock() 266 scroll, ok := allScrollRecords[scrollId] 267 allScrollRecordsLock.RUnlock() 268 if !ok { 269 return false 270 } 271 return scroll.Valid 272 } 273 274 /* 275 Sets scroll based on scroll id 276 277 Interally protects against concurrent scroll operations 278 */ 279 func SetScrollRecord(scrollId string, scroll *Scroll) { 280 allScrollRecordsLock.Lock() 281 allScrollRecords[scrollId] = scroll 282 allScrollRecordsLock.Unlock() 283 } 284 285 /* 286 Returns the total hits of scroll. 287 288 Returns 0 if scroll does not exist 289 */ 290 func GetScrollTotalHits(scrollId string) uint64 { 291 allScrollRecordsLock.RLock() 292 scroll, ok := allScrollRecords[scrollId] 293 allScrollRecordsLock.RUnlock() 294 if !ok { 295 return 0 296 } 297 return scroll.Results.Hits.GetHits() 298 }