github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/replay/workload_capture_test.go (about)

     1  package replay
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"fmt"
     7  	"io"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  	"unicode"
    13  
    14  	"github.com/cockroachdb/datadriven"
    15  	"github.com/cockroachdb/pebble"
    16  	"github.com/cockroachdb/pebble/internal/base"
    17  	"github.com/cockroachdb/pebble/internal/humanize"
    18  	"github.com/cockroachdb/pebble/vfs"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func TestWorkloadCollector(t *testing.T) {
    23  	const srcDir = `src`
    24  	const destDir = `dst`
    25  	datadriven.Walk(t, "testdata/collect", func(t *testing.T, path string) {
    26  		fs := vfs.NewMem()
    27  		require.NoError(t, fs.MkdirAll(srcDir, 0755))
    28  		require.NoError(t, fs.MkdirAll(destDir, 0755))
    29  		c := NewWorkloadCollector(srcDir)
    30  		o := &pebble.Options{FS: fs}
    31  		c.Attach(o)
    32  		var currentManifest vfs.File
    33  		var buf bytes.Buffer
    34  		defer func() {
    35  			if currentManifest != nil {
    36  				currentManifest.Close()
    37  			}
    38  		}()
    39  		datadriven.RunTest(t, path, func(t *testing.T, td *datadriven.TestData) string {
    40  			buf.Reset()
    41  			switch td.Cmd {
    42  			case "cmp-files":
    43  				if len(td.CmdArgs) != 2 {
    44  					return fmt.Sprintf("expected exactly 2 args, received %d", len(td.CmdArgs))
    45  				}
    46  				b1 := readFile(t, fs, td.CmdArgs[0].String())
    47  				b2 := readFile(t, fs, td.CmdArgs[1].String())
    48  				if !bytes.Equal(b1, b2) {
    49  					return fmt.Sprintf("files are unequal: %s (%s) and %s (%s)",
    50  						td.CmdArgs[0].String(), humanize.Bytes.Uint64(uint64(len(b1))),
    51  						td.CmdArgs[1].String(), humanize.Bytes.Uint64(uint64(len(b2))))
    52  				}
    53  				return "equal"
    54  			case "clean":
    55  				for _, path := range strings.Fields(td.Input) {
    56  					typ, _, ok := base.ParseFilename(fs, path)
    57  					require.True(t, ok)
    58  					require.NoError(t, o.Cleaner.Clean(fs, typ, path))
    59  				}
    60  				return ""
    61  			case "create-manifest":
    62  				if currentManifest != nil {
    63  					require.NoError(t, currentManifest.Close())
    64  				}
    65  
    66  				var fileNum uint64
    67  				var err error
    68  				td.ScanArgs(t, "filenum", &fileNum)
    69  				path := base.MakeFilepath(fs, srcDir, base.FileTypeManifest, base.FileNum(fileNum).DiskFileNum())
    70  				currentManifest, err = fs.Create(path)
    71  				require.NoError(t, err)
    72  				_, err = currentManifest.Write(randData(100))
    73  				require.NoError(t, err)
    74  
    75  				c.onManifestCreated(pebble.ManifestCreateInfo{
    76  					Path:    path,
    77  					FileNum: base.FileNum(fileNum),
    78  				})
    79  				return ""
    80  			case "flush":
    81  				flushInfo := pebble.FlushInfo{
    82  					Done:          true,
    83  					Input:         1,
    84  					Duration:      100 * time.Millisecond,
    85  					TotalDuration: 100 * time.Millisecond,
    86  				}
    87  				for _, line := range strings.Split(td.Input, "\n") {
    88  					if line == "" {
    89  						continue
    90  					}
    91  
    92  					parts := strings.FieldsFunc(line, func(r rune) bool { return unicode.IsSpace(r) || r == ':' })
    93  					tableInfo := pebble.TableInfo{Size: 10 << 10}
    94  					fileNum, err := strconv.ParseUint(parts[0], 10, 64)
    95  					require.NoError(t, err)
    96  					tableInfo.FileNum = base.FileNum(fileNum)
    97  
    98  					p := writeFile(t, fs, srcDir, base.FileTypeTable, tableInfo.FileNum.DiskFileNum(), randData(int(tableInfo.Size)))
    99  					fmt.Fprintf(&buf, "created %s\n", p)
   100  					flushInfo.Output = append(flushInfo.Output, tableInfo)
   101  
   102  					// Simulate a version edit applied to the current manifest.
   103  					_, err = currentManifest.Write(randData(25))
   104  					require.NoError(t, err)
   105  				}
   106  				flushInfo.InputBytes = 100 // Determinism
   107  				fmt.Fprint(&buf, flushInfo.String())
   108  				c.onFlushEnd(flushInfo)
   109  				return buf.String()
   110  			case "ingest":
   111  				ingestInfo := pebble.TableIngestInfo{}
   112  				for _, line := range strings.Split(td.Input, "\n") {
   113  					if line == "" {
   114  						continue
   115  					}
   116  
   117  					parts := strings.FieldsFunc(line, func(r rune) bool { return unicode.IsSpace(r) || r == ':' })
   118  					tableInfo := pebble.TableInfo{Size: 10 << 10}
   119  					fileNum, err := strconv.ParseUint(parts[0], 10, 64)
   120  					require.NoError(t, err)
   121  					tableInfo.FileNum = base.FileNum(fileNum)
   122  
   123  					p := writeFile(t, fs, srcDir, base.FileTypeTable, tableInfo.FileNum.DiskFileNum(), randData(int(tableInfo.Size)))
   124  					fmt.Fprintf(&buf, "created %s\n", p)
   125  					ingestInfo.Tables = append(ingestInfo.Tables, struct {
   126  						pebble.TableInfo
   127  						Level int
   128  					}{Level: 0, TableInfo: tableInfo})
   129  
   130  					// Simulate a version edit applied to the current manifest.
   131  					_, err = currentManifest.Write(randData(25))
   132  					require.NoError(t, err)
   133  				}
   134  				fmt.Fprint(&buf, ingestInfo.String())
   135  				c.onTableIngest(ingestInfo)
   136  				return buf.String()
   137  
   138  			case "ls":
   139  				return runListFiles(t, fs, td)
   140  			case "start":
   141  				c.Start(fs, destDir)
   142  				return ""
   143  			case "stat":
   144  				var buf bytes.Buffer
   145  				for _, arg := range td.CmdArgs {
   146  					fi, err := fs.Stat(arg.String())
   147  					if err != nil {
   148  						fmt.Fprintf(&buf, "%s: %s\n", arg.String(), err)
   149  						continue
   150  					}
   151  					fmt.Fprintf(&buf, "%s:\n", arg.String())
   152  					fmt.Fprintf(&buf, "  size: %d\n", fi.Size())
   153  				}
   154  				return buf.String()
   155  			case "stop":
   156  				c.Stop()
   157  				return ""
   158  			case "wait":
   159  				// Wait until all pending sstables have been copied, then list
   160  				// the files in the destination directory.
   161  				c.mu.Lock()
   162  				for c.mu.tablesEnqueued != c.mu.tablesCopied {
   163  					c.mu.copyCond.Wait()
   164  				}
   165  				c.mu.Unlock()
   166  				listFiles(t, fs, &buf, destDir)
   167  				return buf.String()
   168  			default:
   169  				return fmt.Sprintf("unrecognized command %q", td.Cmd)
   170  			}
   171  		})
   172  	})
   173  }
   174  
   175  func randData(byteCount int) []byte {
   176  	b := make([]byte, byteCount)
   177  	rand.Read(b)
   178  	return b
   179  }
   180  
   181  func writeFile(
   182  	t *testing.T, fs vfs.FS, dir string, typ base.FileType, fileNum base.DiskFileNum, data []byte,
   183  ) string {
   184  	path := base.MakeFilepath(fs, dir, typ, fileNum)
   185  	f, err := fs.Create(path)
   186  	require.NoError(t, err)
   187  	_, err = f.Write(data)
   188  	require.NoError(t, err)
   189  	require.NoError(t, f.Close())
   190  	return path
   191  }
   192  
   193  func readFile(t *testing.T, fs vfs.FS, path string) []byte {
   194  	r, err := fs.Open(path)
   195  	require.NoError(t, err)
   196  	b, err := io.ReadAll(r)
   197  	require.NoError(t, err)
   198  	require.NoError(t, r.Close())
   199  	return b
   200  }