go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/common/storage/memory/memory.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package memory implements in-memory Storage structures.
    16  //
    17  // It is designed for testing, and hasn't been optimized for any productio use.
    18  package memory
    19  
    20  import (
    21  	"context"
    22  	"errors"
    23  	"sync"
    24  
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"go.chromium.org/luci/logdog/api/logpb"
    28  	"go.chromium.org/luci/logdog/common/storage"
    29  	"go.chromium.org/luci/logdog/common/types"
    30  )
    31  
    32  type logStream struct {
    33  	logs        map[types.MessageIndex][]byte
    34  	latestIndex types.MessageIndex
    35  }
    36  
    37  type rec struct {
    38  	index types.MessageIndex
    39  	data  []byte
    40  }
    41  
    42  type streamKey struct {
    43  	project string
    44  	path    types.StreamPath
    45  }
    46  
    47  // Storage is an implementation of the storage.Storage interface that stores
    48  // data in memory.
    49  //
    50  // This is intended for testing, and not intended to be performant.
    51  type Storage struct {
    52  	// MaxGetCount, if not zero, is the maximum number of records to retrieve from
    53  	// a single Get request.
    54  	MaxGetCount int
    55  
    56  	stateMu sync.Mutex
    57  	streams map[streamKey]*logStream
    58  	closed  bool
    59  	err     error
    60  }
    61  
    62  var _ storage.Storage = (*Storage)(nil)
    63  
    64  // Close implements storage.Storage.
    65  func (s *Storage) Close() {
    66  	s.run(func() error {
    67  		s.closed = true
    68  		return nil
    69  	})
    70  }
    71  
    72  // ResetClose resets the storage instance, allowing it to be used another time.
    73  // The data stored in this instance is not changed.
    74  func (s *Storage) ResetClose() {
    75  	s.stateMu.Lock()
    76  	defer s.stateMu.Unlock()
    77  
    78  	s.closed = false
    79  }
    80  
    81  // Put implements storage.Storage.
    82  func (s *Storage) Put(c context.Context, req storage.PutRequest) error {
    83  	return s.run(func() error {
    84  		ls := s.getLogStreamLocked(req.Project, req.Path, true)
    85  
    86  		for i, v := range req.Values {
    87  			index := req.Index + types.MessageIndex(i)
    88  			if _, ok := ls.logs[index]; ok {
    89  				return storage.ErrExists
    90  			}
    91  
    92  			clone := make([]byte, len(v))
    93  			copy(clone, v)
    94  			ls.logs[index] = clone
    95  			if index > ls.latestIndex {
    96  				ls.latestIndex = index
    97  			}
    98  		}
    99  		return nil
   100  	})
   101  }
   102  
   103  // Expunge implements storage.Storage.
   104  func (s *Storage) Expunge(c context.Context, req storage.ExpungeRequest) error {
   105  	return s.run(func() error {
   106  		ls := s.getLogStreamLocked(req.Project, req.Path, true)
   107  		ls.logs = nil
   108  		return nil
   109  	})
   110  }
   111  
   112  // Get implements storage.Storage.
   113  func (s *Storage) Get(c context.Context, req storage.GetRequest, cb storage.GetCallback) error {
   114  	recs := []*rec(nil)
   115  	err := s.run(func() error {
   116  		ls := s.getLogStreamLocked(req.Project, req.Path, false)
   117  		if ls == nil {
   118  			return storage.ErrDoesNotExist
   119  		}
   120  
   121  		limit := len(ls.logs)
   122  		if req.Limit > 0 && req.Limit < limit {
   123  			limit = req.Limit
   124  		}
   125  		if s.MaxGetCount > 0 && s.MaxGetCount < limit {
   126  			limit = s.MaxGetCount
   127  		}
   128  
   129  		// Grab all records starting from our start index.
   130  		for idx := req.Index; idx <= ls.latestIndex; idx++ {
   131  			if le, ok := ls.logs[idx]; ok {
   132  				recs = append(recs, &rec{
   133  					index: idx,
   134  					data:  le,
   135  				})
   136  			}
   137  
   138  			if len(recs) >= limit {
   139  				break
   140  			}
   141  		}
   142  		return nil
   143  	})
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	// Punt all of the records upstream. We copy the data to prevent the
   149  	// callback from accidentally mutating it. We reuse the data buffer to try
   150  	// and catch errors when the callback retains the data.
   151  	for _, r := range recs {
   152  		var dataCopy []byte
   153  		if !req.KeysOnly {
   154  			dataCopy = make([]byte, len(r.data))
   155  			copy(dataCopy, r.data)
   156  		}
   157  		if !cb(storage.MakeEntry(dataCopy, r.index)) {
   158  			break
   159  		}
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  // Tail implements storage.Storage.
   166  func (s *Storage) Tail(c context.Context, project string, path types.StreamPath) (*storage.Entry, error) {
   167  	var r *rec
   168  
   169  	// Find the latest log, then return it.
   170  	err := s.run(func() error {
   171  		ls := s.getLogStreamLocked(project, path, false)
   172  		if ls == nil {
   173  			return storage.ErrDoesNotExist
   174  		}
   175  
   176  		r = &rec{
   177  			index: ls.latestIndex,
   178  			data:  ls.logs[ls.latestIndex],
   179  		}
   180  		return nil
   181  	})
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	return storage.MakeEntry(r.data, r.index), nil
   186  }
   187  
   188  // Count returns the number of log records for the given stream.
   189  func (s *Storage) Count(project string, path types.StreamPath) (c int) {
   190  	s.run(func() error {
   191  		if st := s.getLogStreamLocked(project, path, false); st != nil {
   192  			c = len(st.logs)
   193  		}
   194  		return nil
   195  	})
   196  	return
   197  }
   198  
   199  // SetErr sets the storage's error value. If not nil, all operations will fail
   200  // with this error.
   201  func (s *Storage) SetErr(err error) {
   202  	s.stateMu.Lock()
   203  	defer s.stateMu.Unlock()
   204  	s.err = err
   205  }
   206  
   207  func (s *Storage) run(f func() error) error {
   208  	s.stateMu.Lock()
   209  	defer s.stateMu.Unlock()
   210  
   211  	if s.err != nil {
   212  		return s.err
   213  	}
   214  	if s.closed {
   215  		return errors.New("storage is closed")
   216  	}
   217  	return f()
   218  }
   219  
   220  func (s *Storage) getLogStreamLocked(project string, path types.StreamPath, create bool) *logStream {
   221  	key := streamKey{
   222  		project: project,
   223  		path:    path,
   224  	}
   225  
   226  	ls := s.streams[key]
   227  	if ls == nil && create {
   228  		ls = &logStream{
   229  			logs:        map[types.MessageIndex][]byte{},
   230  			latestIndex: -1,
   231  		}
   232  
   233  		if s.streams == nil {
   234  			s.streams = map[streamKey]*logStream{}
   235  		}
   236  		s.streams[key] = ls
   237  	}
   238  
   239  	return ls
   240  }
   241  
   242  // PutEntries is a convenience method for ingesting logpb.Entry's into this
   243  // Storage object.
   244  func (s *Storage) PutEntries(ctx context.Context, project string, path types.StreamPath, entries ...*logpb.LogEntry) {
   245  	for _, ent := range entries {
   246  		value, err := proto.Marshal(ent)
   247  		if err != nil {
   248  			panic(err)
   249  		}
   250  		s.Put(ctx, storage.PutRequest{
   251  			Project: project,
   252  			Path:    path,
   253  			Index:   types.MessageIndex(ent.StreamIndex),
   254  			Values:  [][]byte{value},
   255  		})
   256  	}
   257  }