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  }