github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/wal/wal.go (about)

     1  package wal
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/base64"
     6  	"encoding/binary"
     7  	goerrors "errors"
     8  	"fmt"
     9  	"hash/crc32"
    10  	"io"
    11  	"log/slog"
    12  	"path/filepath"
    13  	"time"
    14  
    15  	"github.com/gnolang/gno/tm2/pkg/amino"
    16  	auto "github.com/gnolang/gno/tm2/pkg/autofile"
    17  	tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time"
    18  	"github.com/gnolang/gno/tm2/pkg/errors"
    19  	osm "github.com/gnolang/gno/tm2/pkg/os"
    20  	"github.com/gnolang/gno/tm2/pkg/service"
    21  )
    22  
    23  const (
    24  	// how often the WAL should be sync'd during period sync'ing
    25  	walDefaultFlushInterval = 2 * time.Second
    26  )
    27  
    28  var (
    29  	crc32c      = crc32.MakeTable(crc32.Castagnoli)
    30  	base64stdnp = base64.StdEncoding.WithPadding(base64.NoPadding)
    31  )
    32  
    33  // --------------------------------------------------------
    34  // types and functions for savings consensus messages
    35  
    36  type WALMessage interface {
    37  	AssertWALMessage()
    38  }
    39  
    40  // TimedWALMessage wraps WALMessage and adds Time for debugging purposes.
    41  type TimedWALMessage struct {
    42  	Time time.Time  `json:"time"`
    43  	Msg  WALMessage `json:"msg"`
    44  }
    45  
    46  // Some lines are MetaMessages to denote new heights, etc.
    47  // NOTE: The encoding must not contain the '#' character,
    48  // so arbitrary strings will not work without some kind of escaping.
    49  // TODO: consider alternative meta line schemas in the long run, or escape the
    50  // '#' character in such a way that scanning randomly from any position in a
    51  // file resumes correctly after the first and only likely corruption.
    52  type MetaMessage struct {
    53  	Height int64 `json:"h"`
    54  }
    55  
    56  // --------------------------------------------------------
    57  // Simple write-ahead logger
    58  
    59  // WAL is an interface for any write-ahead logger.
    60  type WAL interface {
    61  	// config methods
    62  	SetLogger(l *slog.Logger)
    63  
    64  	// write methods
    65  	Write(WALMessage) error
    66  	WriteSync(WALMessage) error
    67  	WriteMetaSync(MetaMessage) error
    68  	FlushAndSync() error
    69  
    70  	// search methods
    71  	SearchForHeight(height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error)
    72  
    73  	// service methods
    74  	Start() error
    75  	Stop() error
    76  	Wait()
    77  }
    78  
    79  // Write ahead logger writes msgs to disk before they are processed.
    80  // Can be used for crash-recovery and deterministic replay.
    81  // TODO: currently the wal is overwritten during replay catchup, give it a mode
    82  // so it's either reading or appending - must read to end to start appending
    83  // again.
    84  type baseWAL struct {
    85  	service.BaseService
    86  
    87  	group *auto.Group
    88  
    89  	maxSize int64
    90  	enc     *WALWriter
    91  
    92  	flushTicker   *time.Ticker
    93  	flushInterval time.Duration
    94  }
    95  
    96  var _ WAL = &baseWAL{}
    97  
    98  // NewWAL returns a new write-ahead logger based on `baseWAL`, which implements
    99  // WAL. It's flushed and synced to disk every 2s and once when stopped.
   100  // `maxSize` is the maximum allowable amino bytes of a TimedWALMessage
   101  // including the amino (byte) size prefix, but excluding any crc checks.
   102  func NewWAL(walFile string, maxSize int64, groupOptions ...func(*auto.Group)) (*baseWAL, error) {
   103  	err := osm.EnsureDir(filepath.Dir(walFile), 0o700)
   104  	if err != nil {
   105  		return nil, errors.Wrap(err, "failed to ensure WAL directory is in place")
   106  	}
   107  
   108  	group, err := auto.OpenGroup(walFile, groupOptions...)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	wal := &baseWAL{
   113  		group:         group,
   114  		maxSize:       maxSize,
   115  		enc:           NewWALWriter(group, maxSize),
   116  		flushInterval: walDefaultFlushInterval,
   117  	}
   118  	wal.BaseService = *service.NewBaseService(nil, "baseWAL", wal)
   119  	return wal, nil
   120  }
   121  
   122  // SetFlushInterval allows us to override the periodic flush interval for the WAL.
   123  func (wal *baseWAL) SetFlushInterval(i time.Duration) {
   124  	wal.flushInterval = i
   125  }
   126  
   127  func (wal *baseWAL) Group() *auto.Group {
   128  	return wal.group
   129  }
   130  
   131  func (wal *baseWAL) SetLogger(l *slog.Logger) {
   132  	wal.BaseService.Logger = l
   133  	wal.group.SetLogger(l)
   134  }
   135  
   136  func (wal *baseWAL) OnStart() error {
   137  	size := wal.group.ReadGroupInfo().TotalSize
   138  	if size == 0 {
   139  		wal.WriteMetaSync(MetaMessage{Height: 0})
   140  	}
   141  	err := wal.group.Start()
   142  	if err != nil {
   143  		return err
   144  	}
   145  	wal.flushTicker = time.NewTicker(wal.flushInterval)
   146  	go wal.processFlushTicks()
   147  	return nil
   148  }
   149  
   150  func (wal *baseWAL) processFlushTicks() {
   151  	for {
   152  		select {
   153  		case <-wal.flushTicker.C:
   154  			if err := wal.FlushAndSync(); err != nil {
   155  				wal.Logger.Error("Periodic WAL flush failed", "err", err)
   156  			}
   157  		case <-wal.Quit():
   158  			return
   159  		}
   160  	}
   161  }
   162  
   163  // FlushAndSync flushes and fsync's the underlying group's data to disk.
   164  // See auto#FlushAndSync
   165  func (wal *baseWAL) FlushAndSync() error {
   166  	return wal.group.FlushAndSync()
   167  }
   168  
   169  // Stop the underlying autofile group.
   170  // Use Wait() to ensure it's finished shutting down
   171  // before cleaning up files.
   172  func (wal *baseWAL) OnStop() {
   173  	wal.flushTicker.Stop()
   174  	wal.FlushAndSync()
   175  	wal.group.Stop()
   176  	wal.group.Close()
   177  }
   178  
   179  // Wait for the underlying autofile group to finish shutting down
   180  // so it's safe to cleanup files.
   181  func (wal *baseWAL) Wait() {
   182  	wal.group.Wait()
   183  }
   184  
   185  // Write is called in newStep and for each receive on the
   186  // peerMsgQueue and the timeoutTicker.
   187  // NOTE: does not call fsync()
   188  func (wal *baseWAL) Write(msg WALMessage) error {
   189  	if wal == nil {
   190  		return nil
   191  	}
   192  
   193  	if err := wal.enc.Write(TimedWALMessage{tmtime.Now(), msg}); err != nil {
   194  		wal.Logger.Error("Error writing msg to consensus wal. WARNING: recover may not be possible for the current height",
   195  			"err", err, "msg", msg)
   196  		return err
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  // WriteMetaSync writes the new height and finalizes the previous height.
   203  // NOTE: It is useless to implement WriteMeta() (asynchronous) because there is
   204  // usually something to do in sync after the aforementioned finalization
   205  // occurs.
   206  func (wal *baseWAL) WriteMetaSync(meta MetaMessage) error {
   207  	if wal == nil {
   208  		return nil
   209  	}
   210  
   211  	if err := wal.enc.WriteMeta(meta); err != nil {
   212  		wal.Logger.Error("Error writing height to consensus wal. WARNING: full recover may not be possible for the previous height",
   213  			"err", err)
   214  		return err
   215  	}
   216  
   217  	if err := wal.FlushAndSync(); err != nil {
   218  		wal.Logger.Error("WriteSync failed to flush consensus wal. WARNING: may result in creating alternative proposals / votes for the current height iff the node restarted",
   219  			"err", err)
   220  		return err
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  // WriteSync is called when we receive a msg from ourselves
   227  // so that we write to disk before sending signed messages.
   228  // NOTE: calls fsync()
   229  func (wal *baseWAL) WriteSync(msg WALMessage) error {
   230  	if wal == nil {
   231  		return nil
   232  	}
   233  
   234  	if err := wal.Write(msg); err != nil {
   235  		return err
   236  	}
   237  
   238  	if err := wal.FlushAndSync(); err != nil {
   239  		wal.Logger.Error("WriteSync failed to flush consensus wal. WARNING: may result in creating alternative proposals / votes for the current height iff the node restarted",
   240  			"err", err)
   241  		return err
   242  	}
   243  
   244  	return nil
   245  }
   246  
   247  type WALSearchMode int
   248  
   249  const (
   250  	WALSearchModeInvalid WALSearchMode = iota
   251  	WALSearchModeBackwards
   252  	WALSearchModeBinary
   253  )
   254  
   255  // WALSearchOptions are optional arguments to SearchForHeight.
   256  type WALSearchOptions struct {
   257  	// Mode is by default backwards via WALSearchModeBackwards (limit at 100
   258  	// blocks).
   259  	Mode WALSearchMode
   260  	// IgnoreDataCorruptionErrors set to true will result in skipping data
   261  	// corruption errors (default false).
   262  	IgnoreDataCorruptionErrors bool
   263  }
   264  
   265  // SearchForHeight scans meta lines to find the first line after height as
   266  // denoted by a meta line, and returns an auto.GroupReader.  Group reader will
   267  // be nil if found equals false.
   268  //
   269  // The result is a buffered ReadCloser.
   270  //
   271  // CONTRACT: caller must close group reader.
   272  func (wal *baseWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error) {
   273  	var (
   274  		msg  *TimedWALMessage
   275  		meta *MetaMessage
   276  		gr   *auto.GroupReader
   277  	)
   278  
   279  	// NOTE: starting from the last file in the group because we're usually
   280  	// searching for the last height. See replay.go
   281  	min, max := wal.group.MinIndex(), wal.group.MaxIndex()
   282  	wal.Logger.Info("Searching for height", "height", height, "min", min, "max", max)
   283  
   284  	var (
   285  		mode    = WALSearchModeBackwards
   286  		backoff = 0 // backwards search offset (goes 0,-1,-2,-4,-8,-16...)
   287  		idxoff  = 0 // forward index offset (for when file contains no meta lines).
   288  	)
   289  
   290  	// option overrides.
   291  	if options != nil && options.Mode != WALSearchModeInvalid {
   292  		mode = options.Mode
   293  	}
   294  
   295  OUTER_LOOP:
   296  	for min <= max {
   297  		var index int
   298  
   299  		// set index depending on mode.
   300  		switch mode {
   301  		case WALSearchModeBackwards:
   302  			index = max + backoff + idxoff
   303  			if max < index {
   304  				// (max+backoff)+ doesn't contain any height.
   305  				// adjust max & backoff accordingly.
   306  				idxoff = 0
   307  				max = max + backoff - 1
   308  				if backoff == 0 {
   309  					backoff = -1
   310  				} else {
   311  					backoff = backoff * 2 // exponential backwards
   312  				}
   313  				continue OUTER_LOOP
   314  			}
   315  			if index < min {
   316  				panic("should not happen")
   317  			}
   318  		case WALSearchModeBinary:
   319  			index = (min+max+1)/2 + idxoff
   320  			if max < index {
   321  				// ((min+max+1)/2)+ doesn't contain any height.
   322  				// adjust max & binary search accordingly.
   323  				idxoff = 0
   324  				max = (min+max+1)/2 - 1
   325  				continue OUTER_LOOP
   326  			}
   327  		}
   328  
   329  		gr, err = wal.group.NewReader(index, index+1) // read only for index.
   330  		if err != nil {
   331  			return nil, false, err
   332  		}
   333  		dec := NewWALReader(gr, wal.maxSize)
   334  
   335  	FILE_LOOP:
   336  		for {
   337  			msg, meta, err = dec.ReadMessage()
   338  			// error case
   339  			if err != nil {
   340  				if goerrors.Is(err, io.EOF) {
   341  					// adjust next index.
   342  					// @index didn't have a height declaration.
   343  					idxoff++
   344  					dec.Close()
   345  					continue OUTER_LOOP
   346  				} else if options != nil && options.IgnoreDataCorruptionErrors && IsDataCorruptionError(err) {
   347  					wal.Logger.Error("Corrupted entry. Skipping...", "err", err)
   348  					continue FILE_LOOP // skip corrupted line and ignore error.
   349  				} else {
   350  					dec.Close()
   351  					return nil, false, err
   352  				}
   353  			}
   354  			// meta case
   355  			if meta != nil {
   356  				if height < meta.Height {
   357  					// adjust next index.
   358  					// it's in an earlier position or file.
   359  					switch mode {
   360  					case WALSearchModeBackwards:
   361  						idxoff = 0
   362  						if backoff == 0 {
   363  							max--
   364  							backoff = -1
   365  						} else {
   366  							max += backoff
   367  							backoff *= 2
   368  						}
   369  						// convert to binary search if backoff is too big.
   370  						// max+backoff would work but max+(backoff*2) is smoother.
   371  						if max+(backoff*2) <= min {
   372  							wal.Logger.Info("Converting to binary search",
   373  								"height", height, "min", min,
   374  								"max", max, "backoff", backoff)
   375  							backoff = 0
   376  							mode = WALSearchModeBinary
   377  						}
   378  					case WALSearchModeBinary:
   379  						idxoff = 0
   380  						max = (min+max+1)/2 - 1
   381  					}
   382  					dec.Close()
   383  					continue OUTER_LOOP
   384  				} else if meta.Height == height { // found
   385  					wal.Logger.Info("Found", "height", height, "index", index)
   386  					// NOTE: dec itself is an io.ReadCloser for this purpose.
   387  					// NOTE: the result is buffered, specifically a bufio.Reader.
   388  					return dec, true, nil
   389  				} else { // meta.Height < height
   390  					// it comes later, maybe @index.
   391  					switch mode {
   392  					case WALSearchModeBackwards:
   393  						if backoff == 0 {
   394  							// ignore and keep reading
   395  							// NOTE: in the future we could binary search
   396  							// within a file, but for now we read sequentially.
   397  							continue FILE_LOOP
   398  						} else {
   399  							// convert to binary search with index as new min.
   400  							wal.Logger.Info("Converting to binary search with new min",
   401  								"height", height, "min", min,
   402  								"max", max, "backoff", backoff)
   403  							idxoff = 0
   404  							backoff = 0
   405  							min = index
   406  							mode = WALSearchModeBinary
   407  							dec.Close()
   408  							continue OUTER_LOOP
   409  						}
   410  					case WALSearchModeBinary:
   411  						if index < max {
   412  							// maybe in @index, but first try binary search
   413  							// between @index and max.
   414  							idxoff = 0
   415  							min = index
   416  							dec.Close()
   417  							continue OUTER_LOOP
   418  						} else { // index == max
   419  							// this is the last file, keep reading.
   420  							// NOTE: in the future we could binary search
   421  							// within a file, but for now we read sequentially.
   422  							continue FILE_LOOP
   423  						}
   424  					}
   425  				}
   426  			}
   427  			// msg case
   428  			if msg != nil {
   429  				// do nothing, we're only interested in meta lines.
   430  				// TODO: optimize by implementing ReadNextMeta(),
   431  				// which skips decoding non-meta messages.
   432  			}
   433  		}
   434  	}
   435  
   436  	return nil, false, nil
   437  }
   438  
   439  // -----------
   440  
   441  // A WALWriter writes custom-encoded WAL messages to an output stream.
   442  // Each binary WAL entry is length encoded, then crc encoded,
   443  // then base64 encoded and delimited by newlines.
   444  // The base64 encoding used is encodeStd,
   445  // `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`.
   446  //
   447  // Each WAL item is also newline delimited.
   448  //
   449  // Meta lines are prefixed with a '#' (which is not a valid base64 character)
   450  // denote meta information, such as new height.  The purpose of base64
   451  // encoding is to enable backwards traversal of items (e.g. in search of some
   452  // previous height).
   453  //
   454  // Base64 is chosen to optimize the worst-case scenario for encoding delimited
   455  // binary bytes while enabling metadata insertion and also supporting
   456  // backwards traversal of binary blobs (to enable binary search, etc). In the
   457  // future, base64 could be replaced with a similar encoding formula, and the
   458  // crc function could change too, but otherwise the structure of the WAL
   459  // should not change, including the rule that all metalines should start with
   460  // '#' and that all items be delimited with a newline. This implementation
   461  // enforces ASCII text characters, but other implementations may choose
   462  // otherwise.
   463  //
   464  // Format: base64(4 bytes CRC sum + length-prefixed amino bytes) + newline.
   465  // e.g.
   466  // ```
   467  // ABCDEFGHIJKLMNOPQRSTUV00
   468  // ABCDEFGHIJKLMNOPQRSTUV01
   469  // ABCDEFGHIJKLMNOPQRSTUV02
   470  // #{"h":"123"}
   471  // ABCDEFGHIJKLMNOPQRSTUV03
   472  // ABCDEFGHIJKLMNOPQRSTUV04
   473  type WALWriter struct {
   474  	wr      io.Writer
   475  	maxSize int64 // max WALMessage amino size excluding time/crc/base64.
   476  }
   477  
   478  // NewWALWriter returns a new encoder that writes to wr.
   479  func NewWALWriter(wr io.Writer, maxSize int64) *WALWriter {
   480  	return &WALWriter{wr, maxSize}
   481  }
   482  
   483  // Write writes the custom encoding of v to the stream, followed by a newline
   484  // byte. It returns an error if the amino-encoded size of v is greater than
   485  // maxSize. Any error encountered during the write is also returned.
   486  func (enc *WALWriter) Write(v TimedWALMessage) error {
   487  	twmBytes := amino.MustMarshalSized(v)
   488  
   489  	length := int64(len(twmBytes))
   490  	if 0 < enc.maxSize && enc.maxSize < length {
   491  		return fmt.Errorf("msg is too big: %d bytes, max: %d bytes", length, enc.maxSize)
   492  	}
   493  
   494  	totalLength := 4 + int(length)
   495  	crc := crc32.Checksum(twmBytes, crc32c)
   496  	line := make([]byte, totalLength)
   497  	binary.BigEndian.PutUint32(line[0:4], crc)
   498  	copy(line[4:], twmBytes)
   499  
   500  	line64 := base64stdnp.EncodeToString(line)
   501  	line64 += "\n"
   502  	_, err := enc.wr.Write([]byte(line64))
   503  	return err
   504  }
   505  
   506  // Meta lines are in JSON for readability.
   507  // TODO: CRC not used (yet), concatenate the CRC bytes with the JSON bytes.
   508  func (enc *WALWriter) WriteMeta(meta MetaMessage) error {
   509  	metaJSON := amino.MustMarshalJSON(meta)
   510  	metaLine := "#" + string(metaJSON) + "\n"
   511  	_, err := enc.wr.Write([]byte(metaLine))
   512  	return err
   513  }
   514  
   515  // -----------
   516  
   517  // IsDataCorruptionError returns true if data has been corrupted inside WAL.
   518  func IsDataCorruptionError(err error) bool {
   519  	_, ok := err.(DataCorruptionError)
   520  	return ok
   521  }
   522  
   523  // DataCorruptionError is an error that occurs if data on disk was corrupted.
   524  type DataCorruptionError struct {
   525  	cause error
   526  }
   527  
   528  func (e DataCorruptionError) Error() string {
   529  	return fmt.Sprintf("DataCorruptionError[%v]", e.cause)
   530  }
   531  
   532  func (e DataCorruptionError) Cause() error {
   533  	return e.cause
   534  }
   535  
   536  // A WALReader reads and decodes custom-encoded WAL messages from an input
   537  // stream. See WALWriter for the format used.
   538  //
   539  // It will also compare the checksums and make sure data size is equal to the
   540  // length from the header. If that is not the case, error will be returned.
   541  //
   542  // WALReader is itself an io.ReaderCloser, and it uses a bufio reader under the
   543  // hood, which means it will usually end up reading more bytes than was
   544  // actually returned via calls to ReadMessage().
   545  type WALReader struct {
   546  	rd      io.Reader // NOTE: use brd instead.
   547  	brd     *bufio.Reader
   548  	maxSize int64
   549  }
   550  
   551  // NewWALReader returns a new decoder that reads from rd.
   552  func NewWALReader(rd io.Reader, maxSize int64) *WALReader {
   553  	return &WALReader{rd, bufio.NewReader(rd), maxSize}
   554  }
   555  
   556  // Reads a line until the "\n" delimiter byte, and returns that line without
   557  // the delimiter.
   558  // A line must end with "\n", otherwise EOF.
   559  func (dec *WALReader) readline() ([]byte, error) {
   560  	bz, err := dec.brd.ReadBytes('\n')
   561  	if 0 < len(bz) {
   562  		bz = bz[:len(bz)-1]
   563  	}
   564  	return bz, err
   565  }
   566  
   567  // Implement io.ReadCloser for SearchForHeight() result reader.
   568  func (dec *WALReader) Read(p []byte) (n int, err error) {
   569  	return dec.brd.Read(p)
   570  }
   571  
   572  // Implement io.ReadCloser for SearchForHeight() result reader.
   573  func (dec *WALReader) Close() (err error) {
   574  	// There is no corresponding .Close on a bufio.
   575  	// we will set brd to nil and let the program panic
   576  	// when methods are called after a close.
   577  	dec.brd = nil
   578  
   579  	// Close rd if it is a Closer.
   580  	if cl, ok := dec.rd.(io.Closer); ok {
   581  		err = cl.Close()
   582  		return
   583  	}
   584  
   585  	return
   586  }
   587  
   588  // Decode reads the next custom-encoded value from its reader and returns it.
   589  // One TimedWALMessage or MetaError or error is returned, the rest are nil.
   590  func (dec *WALReader) ReadMessage() (*TimedWALMessage, *MetaMessage, error) {
   591  	line64, err := dec.readline()
   592  	if err != nil {
   593  		return nil, nil, err
   594  	}
   595  
   596  	if len(line64) == 0 {
   597  		return nil, nil, DataCorruptionError{fmt.Errorf("found empty line")}
   598  	}
   599  
   600  	// special case for MetaMessage.
   601  	if line64[0] == '#' {
   602  		var meta MetaMessage
   603  		err := amino.UnmarshalJSON(line64[1:], &meta)
   604  		return nil, &meta, err
   605  	}
   606  
   607  	// is usual TimedWALMessage.
   608  	// decode base64.
   609  	line, err := base64stdnp.DecodeString(string(line64))
   610  	if err != nil {
   611  		return nil, nil, DataCorruptionError{fmt.Errorf("failed to decode base64: %w", err)}
   612  	}
   613  
   614  	// read crc out of bytes.
   615  	crcSize := int64(4)
   616  	if int64(len(line)) < crcSize {
   617  		return nil, nil, DataCorruptionError{fmt.Errorf("failed to read checksum: %w", err)}
   618  	}
   619  	crc, twmBytes := binary.BigEndian.Uint32(line[:crcSize]), line[crcSize:]
   620  	if dec.maxSize < int64(len(twmBytes)) {
   621  		return nil, nil, DataCorruptionError{fmt.Errorf("length %d exceeded maximum possible value of %d bytes", int64(len(twmBytes)), dec.maxSize)}
   622  	}
   623  
   624  	// check checksum before decoding twmBytes
   625  	if len(twmBytes) == 0 {
   626  		return nil, nil, DataCorruptionError{fmt.Errorf("failed to read amino sized bytes: %w", err)}
   627  	}
   628  	actualCRC := crc32.Checksum(twmBytes, crc32c)
   629  	if actualCRC != crc {
   630  		return nil, nil, DataCorruptionError{fmt.Errorf("checksums do not match: read: %v, actual: %v", crc, actualCRC)}
   631  	}
   632  
   633  	// decode amino sized bytes.
   634  	res := new(TimedWALMessage) //nolint: gosimple
   635  	err = amino.UnmarshalSized(twmBytes, res)
   636  	if err != nil {
   637  		return nil, nil, DataCorruptionError{fmt.Errorf("failed to decode twmBytes: %w", err)}
   638  	}
   639  
   640  	return res, nil, err
   641  }
   642  
   643  type NopWAL struct{}
   644  
   645  var _ WAL = NopWAL{}
   646  
   647  func (NopWAL) SetLogger(l *slog.Logger)          {}
   648  func (NopWAL) Write(m WALMessage) error          { return nil }
   649  func (NopWAL) WriteSync(m WALMessage) error      { return nil }
   650  func (NopWAL) WriteMetaSync(m MetaMessage) error { return nil }
   651  func (NopWAL) FlushAndSync() error               { return nil }
   652  func (NopWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error) {
   653  	return nil, false, nil
   654  }
   655  func (NopWAL) Start() error { return nil }
   656  func (NopWAL) Stop() error  { return nil }
   657  func (NopWAL) Wait()        {}