github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/persist/fs/commitlog/read_write_prop_test.go (about)

     1  // +build big
     2  //
     3  // Copyright (c) 2018 Uber Technologies, Inc.
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in
    13  // all copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    21  // THE SOFTWARE.
    22  
    23  package commitlog
    24  
    25  import (
    26  	"bytes"
    27  	"errors"
    28  	"fmt"
    29  	"io/ioutil"
    30  	"os"
    31  	"path"
    32  	"reflect"
    33  	"sync"
    34  	"testing"
    35  	"time"
    36  
    37  	"github.com/m3db/m3/src/x/pool"
    38  	"github.com/m3db/m3/src/x/serialize"
    39  
    40  	"github.com/m3db/m3/src/dbnode/ts"
    41  	"github.com/m3db/m3/src/x/context"
    42  	"github.com/m3db/m3/src/x/ident"
    43  	xos "github.com/m3db/m3/src/x/os"
    44  	xtest "github.com/m3db/m3/src/x/test"
    45  	xtime "github.com/m3db/m3/src/x/time"
    46  
    47  	"github.com/leanovate/gopter"
    48  	"github.com/leanovate/gopter/commands"
    49  	"github.com/leanovate/gopter/gen"
    50  	"github.com/stretchr/testify/require"
    51  )
    52  
    53  const (
    54  	defaultTestFlushInterval = time.Millisecond
    55  )
    56  
    57  type seriesWritesAndReadPosition struct {
    58  	writes       []generatedWrite
    59  	readPosition int
    60  }
    61  
    62  func TestCommitLogReadWrite(t *testing.T) {
    63  	baseTestPath, err := ioutil.TempDir("", "commit-log-test-base-dir")
    64  	require.NoError(t, err)
    65  	defer os.RemoveAll(baseTestPath)
    66  
    67  	opts := testOpts.SetStrategy(StrategyWriteBehind)
    68  	fsOpts := opts.FilesystemOptions().SetFilePathPrefix(baseTestPath)
    69  	opts = opts.SetFilesystemOptions(fsOpts).SetFlushInterval(time.Millisecond)
    70  
    71  	cl, err := NewCommitLog(opts)
    72  	require.NoError(t, err)
    73  	require.NoError(t, cl.Open())
    74  
    75  	params := gopter.DefaultGenParameters()
    76  	writeResult := gen.SliceOfN(100, genWrite())(params)
    77  	writesInterface, ok := writeResult.Retrieve()
    78  	require.True(t, ok)
    79  	writes, ok := writesInterface.([]generatedWrite)
    80  	require.True(t, ok)
    81  
    82  	ctx := context.NewBackground()
    83  	for _, w := range writes {
    84  		require.NoError(t, cl.Write(ctx, w.series, w.datapoint, w.unit, w.annotation))
    85  	}
    86  	ctx.Close()
    87  	require.NoError(t, cl.Close())
    88  
    89  	i := 0
    90  	iterOpts := IteratorOpts{
    91  		CommitLogOptions:    opts,
    92  		FileFilterPredicate: ReadAllPredicate(),
    93  	}
    94  	iter, corruptFiles, err := NewIterator(iterOpts)
    95  	require.NoError(t, err)
    96  	require.True(t, len(corruptFiles) == 0)
    97  	defer iter.Close()
    98  
    99  	// Convert the writes to be in-order, but keyed by series ID because the
   100  	// commitlog reader only guarantees the same order on disk within a
   101  	// given series
   102  	writesBySeries := map[string]seriesWritesAndReadPosition{}
   103  	for _, write := range writes {
   104  		seriesWrites := writesBySeries[write.series.ID.String()]
   105  		if seriesWrites.writes == nil {
   106  			seriesWrites.writes = []generatedWrite{}
   107  		}
   108  		seriesWrites.writes = append(seriesWrites.writes, write)
   109  		writesBySeries[write.series.ID.String()] = seriesWrites
   110  	}
   111  
   112  	for ; iter.Next(); i++ {
   113  		logEntry := iter.Current()
   114  		var (
   115  			series    = logEntry.Series
   116  			datapoint = logEntry.Datapoint
   117  		)
   118  
   119  		require.NoError(t, iter.Err())
   120  		seriesWrites := writesBySeries[series.ID.String()]
   121  		write := seriesWrites.writes[seriesWrites.readPosition]
   122  
   123  		require.Equal(t, write.series.ID.String(), series.ID.String())
   124  		require.True(t, bytes.Equal(write.series.EncodedTags, series.EncodedTags))
   125  		require.Equal(t, write.series.Namespace.String(), series.Namespace.String())
   126  		require.Equal(t, write.series.Shard, series.Shard)
   127  		require.Equal(t, write.datapoint.Value, datapoint.Value)
   128  		require.Equal(t, write.datapoint.TimestampNanos, datapoint.TimestampNanos)
   129  
   130  		seriesWrites.readPosition++
   131  		writesBySeries[series.ID.String()] = seriesWrites
   132  	}
   133  	require.Equal(t, len(writes), i)
   134  }
   135  
   136  // TestCommitLogPropTest property tests the commitlog by performing various
   137  // operations (Open, Write, Close) in various orders, and then ensuring that
   138  // all the data can be read back. In addition, in some runs it will arbitrarily
   139  // (based on a randomly generated probability) corrupt any bytes written to disk by
   140  // the commitlog to ensure that the commitlog reader is resilient to arbitrarily
   141  // corrupted files and will not deadlock / panic.
   142  func TestCommitLogPropTest(t *testing.T) {
   143  	// Temporarily reduce size of buffered channels to increase chance of
   144  	// catching deadlock issues.
   145  	var (
   146  		oldDecoderInBufChanSize  = decoderInBufChanSize
   147  		oldDecoderOutBufChanSize = decoderOutBufChanSize
   148  	)
   149  	defer func() {
   150  		decoderInBufChanSize = oldDecoderInBufChanSize
   151  		decoderOutBufChanSize = oldDecoderOutBufChanSize
   152  	}()
   153  	decoderInBufChanSize = 0
   154  	decoderOutBufChanSize = 0
   155  
   156  	basePath, err := ioutil.TempDir("", "commit-log-tests")
   157  	require.NoError(t, err)
   158  	defer os.RemoveAll(basePath)
   159  
   160  	var (
   161  		seed       = time.Now().Unix()
   162  		parameters = gopter.DefaultTestParametersWithSeed(seed)
   163  		reporter   = gopter.NewFormatedReporter(true, 160, os.Stdout)
   164  	)
   165  	parameters.MinSuccessfulTests = 20
   166  	properties := gopter.NewProperties(parameters)
   167  
   168  	comms := clCommandFunctor(t, basePath, seed)
   169  	properties.Property("CommitLog System", commands.Prop(comms))
   170  	if !properties.Run(reporter) {
   171  		t.Errorf("failed with initial seed: %d", seed)
   172  	}
   173  }
   174  
   175  // clCommandFunctor is a var which implements the command.Commands interface,
   176  // i.e. is responsible for creating/destroying the system under test and generating
   177  // commands and initial states (clState)
   178  func clCommandFunctor(t *testing.T, basePath string, seed int64) *commands.ProtoCommands {
   179  	return &commands.ProtoCommands{
   180  		NewSystemUnderTestFunc: func(initialState commands.State) commands.SystemUnderTest {
   181  			return initialState
   182  		},
   183  		DestroySystemUnderTestFunc: func(sut commands.SystemUnderTest) {
   184  			state := sut.(*clState)
   185  			if cl := state.cLog; cl != nil {
   186  				cl.Close()
   187  			}
   188  			os.RemoveAll(state.opts.FilesystemOptions().FilePathPrefix())
   189  		},
   190  		InitialStateGen: genState(t, basePath, seed),
   191  		InitialPreConditionFunc: func(state commands.State) bool {
   192  			if state == nil {
   193  				return false
   194  			}
   195  			_, ok := state.(*clState)
   196  			return ok
   197  		},
   198  		GenCommandFunc: func(state commands.State) gopter.Gen {
   199  			return gen.OneGenOf(
   200  				genOpenCommand,
   201  				genCloseCommand,
   202  				genWriteBehindCommand,
   203  				genActiveLogsCommand,
   204  				genRotateLogsCommand,
   205  			)
   206  		},
   207  	}
   208  }
   209  
   210  // operations on *clState
   211  
   212  var genOpenCommand = gen.Const(&commands.ProtoCommand{
   213  	Name: "Open",
   214  	PreConditionFunc: func(state commands.State) bool {
   215  		return !state.(*clState).open
   216  	},
   217  	RunFunc: func(q commands.SystemUnderTest) commands.Result {
   218  		var err error
   219  		s := q.(*clState)
   220  		s.cLog, err = NewCommitLog(s.opts)
   221  		if err != nil {
   222  			return err
   223  		}
   224  
   225  		if s.shouldCorrupt {
   226  			cLog := s.cLog.(*commitLog)
   227  			cLog.newCommitLogWriterFn = func(flushFn flushFn, opts Options) commitLogWriter {
   228  				wIface := newCommitLogWriter(flushFn, opts)
   229  				w := wIface.(*writer)
   230  				w.chunkWriter = newCorruptingChunkWriter(
   231  					w.chunkWriter.(*fsChunkWriter),
   232  					s.corruptionProbability,
   233  					s.seed,
   234  				)
   235  				return w
   236  			}
   237  		}
   238  		err = s.cLog.Open()
   239  		if err != nil {
   240  			return err
   241  		}
   242  		s.open = true
   243  		return nil
   244  	},
   245  	NextStateFunc: func(state commands.State) commands.State {
   246  		s := state.(*clState)
   247  		s.open = true
   248  		return s
   249  	},
   250  	PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult {
   251  		if result == nil {
   252  			return &gopter.PropResult{Status: gopter.PropTrue}
   253  		}
   254  		return &gopter.PropResult{
   255  			Status: gopter.PropError,
   256  			Error:  result.(error),
   257  		}
   258  	},
   259  })
   260  
   261  var genCloseCommand = gen.Const(&commands.ProtoCommand{
   262  	Name: "Close",
   263  	PreConditionFunc: func(state commands.State) bool {
   264  		return state.(*clState).open
   265  	},
   266  	RunFunc: func(q commands.SystemUnderTest) commands.Result {
   267  		s := q.(*clState)
   268  		err := s.cLog.Close()
   269  		if err != nil {
   270  			return err
   271  		}
   272  		s.open = false
   273  		return nil
   274  	},
   275  	NextStateFunc: func(state commands.State) commands.State {
   276  		s := state.(*clState)
   277  		s.open = false
   278  		return s
   279  	},
   280  	PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult {
   281  		if result != nil {
   282  			return &gopter.PropResult{
   283  				Status: gopter.PropError,
   284  				Error:  result.(error),
   285  			}
   286  		}
   287  		s := state.(*clState)
   288  		err := s.writesArePresent(s.pendingWrites...)
   289  		if err != nil {
   290  			return &gopter.PropResult{
   291  				Status: gopter.PropError,
   292  				Error:  err.(error),
   293  			}
   294  		}
   295  		return &gopter.PropResult{Status: gopter.PropTrue}
   296  	},
   297  })
   298  
   299  var genWriteBehindCommand = gen.SliceOfN(10, genWrite()).
   300  	Map(func(writes []generatedWrite) commands.Command {
   301  		return &commands.ProtoCommand{
   302  			Name: "WriteBehind",
   303  			PreConditionFunc: func(state commands.State) bool {
   304  				return state.(*clState).open
   305  			},
   306  			RunFunc: func(q commands.SystemUnderTest) commands.Result {
   307  				s := q.(*clState)
   308  				ctx := context.NewBackground()
   309  				defer ctx.Close()
   310  
   311  				for _, w := range writes {
   312  					err := s.cLog.Write(ctx, w.series, w.datapoint, w.unit, w.annotation)
   313  					if err != nil {
   314  						return err
   315  					}
   316  				}
   317  
   318  				return nil
   319  			},
   320  			NextStateFunc: func(state commands.State) commands.State {
   321  				s := state.(*clState)
   322  				s.pendingWrites = append(s.pendingWrites, writes...)
   323  				return s
   324  			},
   325  			PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult {
   326  				if result == nil {
   327  					return &gopter.PropResult{Status: gopter.PropTrue}
   328  				}
   329  				return &gopter.PropResult{
   330  					Status: gopter.PropError,
   331  					Error:  result.(error),
   332  				}
   333  			},
   334  		}
   335  	})
   336  
   337  var genActiveLogsCommand = gen.Const(&commands.ProtoCommand{
   338  	Name: "ActiveLogs",
   339  	PreConditionFunc: func(state commands.State) bool {
   340  		return true
   341  	},
   342  	RunFunc: func(q commands.SystemUnderTest) commands.Result {
   343  		s := q.(*clState)
   344  
   345  		if s.cLog == nil {
   346  			return nil
   347  		}
   348  
   349  		logs, err := s.cLog.ActiveLogs()
   350  		if !s.open {
   351  			if err != errCommitLogClosed {
   352  				return errors.New("did not receive commit log closed error")
   353  			}
   354  			return nil
   355  		}
   356  
   357  		if err != nil {
   358  			return err
   359  		}
   360  
   361  		if len(logs) != 2 {
   362  			return fmt.Errorf("ActiveLogs did not return exactly two log files: %v", logs)
   363  		}
   364  
   365  		return nil
   366  	},
   367  	NextStateFunc: func(state commands.State) commands.State {
   368  		s := state.(*clState)
   369  		return s
   370  	},
   371  	PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult {
   372  		if result == nil {
   373  			return &gopter.PropResult{Status: gopter.PropTrue}
   374  		}
   375  		return &gopter.PropResult{
   376  			Status: gopter.PropError,
   377  			Error:  result.(error),
   378  		}
   379  	},
   380  })
   381  
   382  var genRotateLogsCommand = gen.Const(&commands.ProtoCommand{
   383  	Name: "RotateLogs",
   384  	PreConditionFunc: func(state commands.State) bool {
   385  		return true
   386  	},
   387  	RunFunc: func(q commands.SystemUnderTest) commands.Result {
   388  		s := q.(*clState)
   389  
   390  		if s.cLog == nil {
   391  			return nil
   392  		}
   393  
   394  		_, err := s.cLog.RotateLogs()
   395  		if !s.open {
   396  			if err != errCommitLogClosed {
   397  				return errors.New("did not receive commit log closed error")
   398  			}
   399  			return nil
   400  		}
   401  
   402  		if err != nil {
   403  			return err
   404  		}
   405  
   406  		return nil
   407  	},
   408  	NextStateFunc: func(state commands.State) commands.State {
   409  		s := state.(*clState)
   410  		return s
   411  	},
   412  	PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult {
   413  		if result == nil {
   414  			return &gopter.PropResult{Status: gopter.PropTrue}
   415  		}
   416  		return &gopter.PropResult{
   417  			Status: gopter.PropError,
   418  			Error:  result.(error),
   419  		}
   420  	},
   421  })
   422  
   423  // clState holds the expected state (i.e. its the commands.State), and we use it as the SystemUnderTest
   424  type clState struct {
   425  	basePath      string
   426  	opts          Options
   427  	open          bool
   428  	cLog          CommitLog
   429  	pendingWrites []generatedWrite
   430  	// Whether the test should corrupt the commit log.
   431  	shouldCorrupt bool
   432  	// If the test should corrupt the commit log, what is the probability of
   433  	// corruption for any given write.
   434  	corruptionProbability float64
   435  	// Seed for use with all RNGs so that runs are reproducible.
   436  	seed int64
   437  }
   438  
   439  // generator for commit log write
   440  func genState(t *testing.T, basePath string, seed int64) gopter.Gen {
   441  	return gopter.CombineGens(gen.Identifier(), gen.Bool(), gen.Float64Range(0, 1)).
   442  		MapResult(func(r *gopter.GenResult) *gopter.GenResult {
   443  			iface, ok := r.Retrieve()
   444  			if !ok {
   445  				return gopter.NewEmptyResult(reflect.PtrTo(reflect.TypeOf(clState{})))
   446  			}
   447  			p, ok := iface.([]interface{})
   448  			if !ok {
   449  				return gopter.NewEmptyResult(reflect.PtrTo(reflect.TypeOf(clState{})))
   450  			}
   451  
   452  			var (
   453  				initPath              = path.Join(basePath, p[0].(string))
   454  				shouldCorrupt         = p[1].(bool)
   455  				corruptionProbability = p[2].(float64)
   456  				result                = newInitState(
   457  					t, initPath, shouldCorrupt, corruptionProbability, seed)
   458  			)
   459  			return gopter.NewGenResult(result, gopter.NoShrinker)
   460  		})
   461  }
   462  
   463  func newInitState(
   464  	t *testing.T,
   465  	dir string,
   466  	shouldCorrupt bool,
   467  	corruptionProbability float64,
   468  	seed int64,
   469  ) *clState {
   470  	opts := testOpts.
   471  		SetStrategy(StrategyWriteBehind).
   472  		SetFlushInterval(defaultTestFlushInterval).
   473  		// Need to set this to a relatively low value otherwise the test will
   474  		// time out because its allocating so much memory for the byte pools
   475  		// in the commit log reader.
   476  		SetFlushSize(1024)
   477  	fsOpts := opts.FilesystemOptions().SetFilePathPrefix(dir)
   478  	opts = opts.SetFilesystemOptions(fsOpts)
   479  	return &clState{
   480  		basePath:              dir,
   481  		opts:                  opts,
   482  		shouldCorrupt:         shouldCorrupt,
   483  		corruptionProbability: corruptionProbability,
   484  		seed:                  seed,
   485  	}
   486  }
   487  
   488  func (s *clState) writesArePresent(writes ...generatedWrite) error {
   489  	writesOnDisk := make(map[string]map[xtime.UnixNano]generatedWrite)
   490  	iterOpts := IteratorOpts{
   491  		CommitLogOptions:    s.opts,
   492  		FileFilterPredicate: ReadAllPredicate(),
   493  	}
   494  	// Based on the corruption type this could return some corrupt files
   495  	// or it could not, so we don't check it.
   496  	iter, _, err := NewIterator(iterOpts)
   497  	if err != nil {
   498  		if s.shouldCorrupt {
   499  			return nil
   500  		}
   501  		return err
   502  	}
   503  
   504  	defer iter.Close()
   505  
   506  	count := 0
   507  	for iter.Next() {
   508  		logEntry := iter.Current()
   509  		var (
   510  			series     = logEntry.Series
   511  			datapoint  = logEntry.Datapoint
   512  			unit       = logEntry.Unit
   513  			annotation = logEntry.Annotation
   514  			idString   = series.ID.String()
   515  		)
   516  
   517  		seriesMap, ok := writesOnDisk[idString]
   518  		if !ok {
   519  			seriesMap = make(map[xtime.UnixNano]generatedWrite)
   520  			writesOnDisk[idString] = seriesMap
   521  		}
   522  		seriesMap[datapoint.TimestampNanos] = generatedWrite{
   523  			series:     series,
   524  			datapoint:  datapoint,
   525  			unit:       unit,
   526  			annotation: annotation,
   527  		}
   528  		count++
   529  	}
   530  
   531  	if s.shouldCorrupt {
   532  		return nil
   533  	}
   534  
   535  	if err := iter.Err(); err != nil {
   536  		return fmt.Errorf("failed after reading %d datapoints: %v", count, err)
   537  	}
   538  
   539  	missingErr := fmt.Errorf("writesOnDisk: %+v, writes: %+v", writesOnDisk, writes)
   540  	for _, w := range writes {
   541  		idString := w.series.ID.String()
   542  		seriesMap, ok := writesOnDisk[idString]
   543  		if !ok {
   544  			return missingErr
   545  		}
   546  		gw, ok := seriesMap[w.datapoint.TimestampNanos]
   547  		if !ok {
   548  			return missingErr
   549  		}
   550  		if !gw.series.Namespace.Equal(w.series.Namespace) ||
   551  			gw.series.Shard != w.series.Shard ||
   552  			gw.datapoint.Value != w.datapoint.Value {
   553  			return missingErr
   554  		}
   555  	}
   556  	return nil
   557  }
   558  
   559  type generatedWrite struct {
   560  	series     ts.Series
   561  	datapoint  ts.Datapoint
   562  	unit       xtime.Unit
   563  	annotation ts.Annotation
   564  }
   565  
   566  func (w generatedWrite) String() string {
   567  	return fmt.Sprintf("ID = %v, Datapoint = %+v", w.series.ID.String(), w.datapoint)
   568  }
   569  
   570  // generator for commit log write
   571  func genWrite() gopter.Gen {
   572  	testTagEncodingPool := serialize.NewTagEncoderPool(serialize.NewTagEncoderOptions(),
   573  		pool.NewObjectPoolOptions().SetSize(1))
   574  	testTagEncodingPool.Init()
   575  
   576  	return gopter.CombineGens(
   577  		gen.Identifier(),
   578  		gen.TimeRange(time.Now(), 15*time.Minute),
   579  		gen.Float64(),
   580  		gen.Identifier(),
   581  		gen.UInt32(),
   582  		gen.Identifier(),
   583  		gen.Identifier(),
   584  		gen.Identifier(),
   585  		gen.Identifier(),
   586  		gen.Bool(),
   587  	).Map(func(val []interface{}) generatedWrite {
   588  		id := val[0].(string)
   589  		t := val[1].(time.Time)
   590  		v := val[2].(float64)
   591  		ns := val[3].(string)
   592  		shard := val[4].(uint32)
   593  		tags := map[string]string{
   594  			val[5].(string): val[6].(string),
   595  			val[7].(string): val[8].(string),
   596  		}
   597  		encodeTags := val[9].(bool)
   598  
   599  		var (
   600  			seriesTags        ident.Tags
   601  			seriesEncodedTags []byte
   602  		)
   603  		for k, v := range tags {
   604  			seriesTags.Append(ident.Tag{
   605  				Name:  ident.StringID(k),
   606  				Value: ident.StringID(v),
   607  			})
   608  		}
   609  
   610  		if encodeTags {
   611  			encoder := testTagEncodingPool.Get()
   612  			if err := encoder.Encode(ident.NewTagsIterator(seriesTags)); err != nil {
   613  				panic(err)
   614  			}
   615  			data, ok := encoder.Data()
   616  			if !ok {
   617  				panic("could not encode tags")
   618  			}
   619  
   620  			// Set encoded tags so the "fast" path is activated.
   621  			seriesEncodedTags = data.Bytes()
   622  		}
   623  
   624  		return generatedWrite{
   625  			series: ts.Series{
   626  				ID:          ident.StringID(id),
   627  				EncodedTags: seriesEncodedTags,
   628  				Namespace:   ident.StringID(ns),
   629  				Shard:       shard,
   630  				UniqueIndex: uniqueID(ns, id),
   631  			},
   632  			datapoint: ts.Datapoint{
   633  				TimestampNanos: xtime.ToUnixNano(t),
   634  				Value:          v,
   635  			},
   636  			unit: xtime.Nanosecond,
   637  		}
   638  	})
   639  }
   640  
   641  type globalMetricIdx struct {
   642  	sync.Mutex
   643  
   644  	idx uint64
   645  	// NB(prateek): we use a map from ns -> series id (string) -> unique uint64, to
   646  	// ensure we assign a unique value to each series/ns combination. Further, we
   647  	// also ensure the value is consistent. i.e. repeated generations of the same
   648  	// id/ns retrieve the same uint64 as earlier.
   649  	idToIdx map[string]map[string]uint64
   650  }
   651  
   652  var metricIdx = globalMetricIdx{
   653  	idToIdx: make(map[string]map[string]uint64),
   654  }
   655  
   656  func uniqueID(ns, s string) uint64 {
   657  	metricIdx.Lock()
   658  	defer metricIdx.Unlock()
   659  
   660  	nsMap, ok := metricIdx.idToIdx[ns]
   661  	if !ok {
   662  		nsMap = make(map[string]uint64)
   663  		metricIdx.idToIdx[ns] = nsMap
   664  	}
   665  
   666  	i, ok := nsMap[s]
   667  	if ok {
   668  		return i
   669  	}
   670  
   671  	idx := metricIdx.idx
   672  	metricIdx.idx++
   673  
   674  	nsMap[s] = idx
   675  	return idx
   676  }
   677  
   678  // corruptingChunkWriter implements the chunkWriter interface and can corrupt all writes issued
   679  // to it based on a configurable probability.
   680  type corruptingChunkWriter struct {
   681  	chunkWriter           *fsChunkWriter
   682  	corruptionProbability float64
   683  	seed                  int64
   684  }
   685  
   686  func newCorruptingChunkWriter(
   687  	chunkWriter *fsChunkWriter,
   688  	corruptionProbability float64,
   689  	seed int64,
   690  ) chunkWriter {
   691  	return &corruptingChunkWriter{
   692  		chunkWriter:           chunkWriter,
   693  		corruptionProbability: corruptionProbability,
   694  		seed:                  seed,
   695  	}
   696  }
   697  
   698  func (c *corruptingChunkWriter) reset(f xos.File) {
   699  	c.chunkWriter.fd = xtest.NewCorruptingFile(
   700  		f, c.corruptionProbability, c.seed)
   701  }
   702  
   703  func (c *corruptingChunkWriter) Write(p []byte) (int, error) {
   704  	return c.chunkWriter.Write(p)
   705  }
   706  
   707  func (c *corruptingChunkWriter) close() error {
   708  	return c.chunkWriter.close()
   709  }
   710  
   711  func (c *corruptingChunkWriter) isOpen() bool {
   712  	return c.chunkWriter.isOpen()
   713  }
   714  
   715  func (c *corruptingChunkWriter) sync() error {
   716  	return c.chunkWriter.sync()
   717  }
   718  
   719  func (c *corruptingChunkWriter) setOnFlush(f func(err error)) {
   720  	c.chunkWriter.setOnFlush(f)
   721  }