Line data Source code
1 : // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
2 : // of this source code is governed by a BSD-style license that can be found in
3 : // the LICENSE file.
4 :
5 : package tool
6 :
7 : import (
8 : "cmp"
9 : "fmt"
10 : "io"
11 : "slices"
12 : "time"
13 :
14 : "github.com/cockroachdb/pebble"
15 : "github.com/cockroachdb/pebble/internal/base"
16 : "github.com/cockroachdb/pebble/internal/humanize"
17 : "github.com/cockroachdb/pebble/internal/manifest"
18 : "github.com/cockroachdb/pebble/record"
19 : "github.com/cockroachdb/pebble/sstable"
20 : "github.com/spf13/cobra"
21 : )
22 :
23 : // manifestT implements manifest-level tools, including both configuration
24 : // state and the commands themselves.
25 : type manifestT struct {
26 : Root *cobra.Command
27 : Dump *cobra.Command
28 : Summarize *cobra.Command
29 : Check *cobra.Command
30 :
31 : opts *pebble.Options
32 : comparers sstable.Comparers
33 : fmtKey keyFormatter
34 : verbose bool
35 :
36 : filterStart key
37 : filterEnd key
38 :
39 : summarizeDur time.Duration
40 : }
41 :
42 1 : func newManifest(opts *pebble.Options, comparers sstable.Comparers) *manifestT {
43 1 : m := &manifestT{
44 1 : opts: opts,
45 1 : comparers: comparers,
46 1 : summarizeDur: time.Hour,
47 1 : }
48 1 : m.fmtKey.mustSet("quoted")
49 1 :
50 1 : m.Root = &cobra.Command{
51 1 : Use: "manifest",
52 1 : Short: "manifest introspection tools",
53 1 : }
54 1 :
55 1 : // Add dump command
56 1 : m.Dump = &cobra.Command{
57 1 : Use: "dump <manifest-files>",
58 1 : Short: "print manifest contents",
59 1 : Long: `
60 1 : Print the contents of the MANIFEST files.
61 1 : `,
62 1 : Args: cobra.MinimumNArgs(1),
63 1 : Run: m.runDump,
64 1 : }
65 1 : m.Dump.Flags().Var(&m.fmtKey, "key", "key formatter")
66 1 : m.Dump.Flags().Var(&m.filterStart, "filter-start", "start key filters out all version edits that only reference sstables containing keys strictly before the given key")
67 1 : m.Dump.Flags().Var(&m.filterEnd, "filter-end", "end key filters out all version edits that only reference sstables containing keys at or strictly after the given key")
68 1 : m.Root.AddCommand(m.Dump)
69 1 : m.Root.PersistentFlags().BoolVarP(&m.verbose, "verbose", "v", false, "verbose output")
70 1 :
71 1 : // Add summarize command
72 1 : m.Summarize = &cobra.Command{
73 1 : Use: "summarize <manifest-files>",
74 1 : Short: "summarize manifest contents",
75 1 : Long: `
76 1 : Summarize the edits to the MANIFEST files over time.
77 1 : `,
78 1 : Args: cobra.MinimumNArgs(1),
79 1 : Run: m.runSummarize,
80 1 : }
81 1 : m.Root.AddCommand(m.Summarize)
82 1 : m.Summarize.Flags().DurationVar(
83 1 : &m.summarizeDur, "dur", time.Hour, "bucket duration as a Go duration string (eg, '1h', '15m')")
84 1 :
85 1 : // Add check command
86 1 : m.Check = &cobra.Command{
87 1 : Use: "check <manifest-files>",
88 1 : Short: "check manifest contents",
89 1 : Long: `
90 1 : Check the contents of the MANIFEST files.
91 1 : `,
92 1 : Args: cobra.MinimumNArgs(1),
93 1 : Run: m.runCheck,
94 1 : }
95 1 : m.Root.AddCommand(m.Check)
96 1 : m.Check.Flags().Var(
97 1 : &m.fmtKey, "key", "key formatter")
98 1 :
99 1 : return m
100 1 : }
101 :
102 1 : func (m *manifestT) printLevels(cmp base.Compare, stdout io.Writer, v *manifest.Version) {
103 1 : for level := range v.Levels {
104 1 : if level == 0 && len(v.L0SublevelFiles) > 0 && !v.Levels[level].Empty() {
105 1 : for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- {
106 1 : fmt.Fprintf(stdout, "--- L0.%d ---\n", sublevel)
107 1 : v.L0SublevelFiles[sublevel].Each(func(f *manifest.FileMetadata) {
108 1 : if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) {
109 1 : return
110 1 : }
111 1 : fmt.Fprintf(stdout, " %s:%d", f.FileNum, f.Size)
112 1 : formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum)
113 1 : formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest)
114 1 : fmt.Fprintf(stdout, "\n")
115 : })
116 : }
117 1 : continue
118 : }
119 1 : fmt.Fprintf(stdout, "--- L%d ---\n", level)
120 1 : iter := v.Levels[level].Iter()
121 1 : for f := iter.First(); f != nil; f = iter.Next() {
122 1 : if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) {
123 0 : continue
124 : }
125 1 : fmt.Fprintf(stdout, " %s:%d", f.FileNum, f.Size)
126 1 : formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum)
127 1 : formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest)
128 1 : fmt.Fprintf(stdout, "\n")
129 : }
130 : }
131 : }
132 :
133 1 : func (m *manifestT) runDump(cmd *cobra.Command, args []string) {
134 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
135 1 : for _, arg := range args {
136 1 : func() {
137 1 : f, err := m.opts.FS.Open(arg)
138 1 : if err != nil {
139 0 : fmt.Fprintf(stderr, "%s\n", err)
140 0 : return
141 0 : }
142 1 : defer f.Close()
143 1 :
144 1 : fmt.Fprintf(stdout, "%s\n", arg)
145 1 :
146 1 : var bve manifest.BulkVersionEdit
147 1 : bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata)
148 1 : var comparer *base.Comparer
149 1 : var editIdx int
150 1 : rr := record.NewReader(f, 0 /* logNum */)
151 1 : for {
152 1 : offset := rr.Offset()
153 1 : r, err := rr.Next()
154 1 : if err != nil {
155 1 : fmt.Fprintf(stdout, "%s\n", err)
156 1 : break
157 : }
158 :
159 1 : var ve manifest.VersionEdit
160 1 : err = ve.Decode(r)
161 1 : if err != nil {
162 0 : fmt.Fprintf(stdout, "%s\n", err)
163 0 : break
164 : }
165 1 : if err := bve.Accumulate(&ve); err != nil {
166 0 : fmt.Fprintf(stdout, "%s\n", err)
167 0 : break
168 : }
169 :
170 1 : if comparer != nil && !anyOverlap(comparer.Compare, &ve, m.filterStart, m.filterEnd) {
171 1 : continue
172 : }
173 :
174 1 : empty := true
175 1 : fmt.Fprintf(stdout, "%d/%d\n", offset, editIdx)
176 1 : if ve.ComparerName != "" {
177 1 : empty = false
178 1 : fmt.Fprintf(stdout, " comparer: %s", ve.ComparerName)
179 1 : comparer = m.comparers[ve.ComparerName]
180 1 : if comparer == nil {
181 0 : fmt.Fprintf(stdout, " (unknown)")
182 0 : }
183 1 : fmt.Fprintf(stdout, "\n")
184 1 : m.fmtKey.setForComparer(ve.ComparerName, m.comparers)
185 : }
186 1 : if ve.MinUnflushedLogNum != 0 {
187 1 : empty = false
188 1 : fmt.Fprintf(stdout, " log-num: %d\n", ve.MinUnflushedLogNum)
189 1 : }
190 1 : if ve.ObsoletePrevLogNum != 0 {
191 0 : empty = false
192 0 : fmt.Fprintf(stdout, " prev-log-num: %d\n", ve.ObsoletePrevLogNum)
193 0 : }
194 1 : if ve.NextFileNum != 0 {
195 1 : empty = false
196 1 : fmt.Fprintf(stdout, " next-file-num: %d\n", ve.NextFileNum)
197 1 : }
198 1 : if ve.LastSeqNum != 0 {
199 1 : empty = false
200 1 : fmt.Fprintf(stdout, " last-seq-num: %d\n", ve.LastSeqNum)
201 1 : }
202 1 : entries := make([]manifest.DeletedFileEntry, 0, len(ve.DeletedFiles))
203 1 : for df := range ve.DeletedFiles {
204 1 : empty = false
205 1 : entries = append(entries, df)
206 1 : }
207 1 : slices.SortFunc(entries, func(a, b manifest.DeletedFileEntry) int {
208 1 : if v := cmp.Compare(a.Level, b.Level); v != 0 {
209 1 : return v
210 1 : }
211 1 : return cmp.Compare(a.FileNum, b.FileNum)
212 : })
213 1 : for _, df := range entries {
214 1 : fmt.Fprintf(stdout, " deleted: L%d %s\n", df.Level, df.FileNum)
215 1 : }
216 1 : for _, nf := range ve.NewFiles {
217 1 : empty = false
218 1 : fmt.Fprintf(stdout, " added: L%d %s:%d",
219 1 : nf.Level, nf.Meta.FileNum, nf.Meta.Size)
220 1 : formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum)
221 1 : formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest)
222 1 : if nf.Meta.CreationTime != 0 {
223 1 : fmt.Fprintf(stdout, " (%s)",
224 1 : time.Unix(nf.Meta.CreationTime, 0).UTC().Format(time.RFC3339))
225 1 : }
226 1 : fmt.Fprintf(stdout, "\n")
227 : }
228 1 : if empty {
229 0 : // NB: An empty version edit can happen if we log a version edit with
230 0 : // a zero field. RocksDB does this with a version edit that contains
231 0 : // `LogNum == 0`.
232 0 : fmt.Fprintf(stdout, " <empty>\n")
233 0 : }
234 1 : editIdx++
235 : }
236 :
237 1 : if comparer != nil {
238 1 : v, err := bve.Apply(
239 1 : nil /* version */, comparer.Compare, m.fmtKey.fn, 0,
240 1 : m.opts.Experimental.ReadCompactionRate,
241 1 : nil /* zombies */, manifest.AllowSplitUserKeys,
242 1 : )
243 1 : if err != nil {
244 1 : fmt.Fprintf(stdout, "%s\n", err)
245 1 : return
246 1 : }
247 1 : m.printLevels(comparer.Compare, stdout, v)
248 : }
249 : }()
250 : }
251 : }
252 :
253 1 : func anyOverlap(cmp base.Compare, ve *manifest.VersionEdit, start, end key) bool {
254 1 : if start == nil && end == nil {
255 1 : return true
256 1 : }
257 1 : for _, df := range ve.DeletedFiles {
258 1 : if anyOverlapFile(cmp, df, start, end) {
259 1 : return true
260 1 : }
261 : }
262 1 : for _, nf := range ve.NewFiles {
263 1 : if anyOverlapFile(cmp, nf.Meta, start, end) {
264 1 : return true
265 1 : }
266 : }
267 1 : return false
268 : }
269 :
270 1 : func anyOverlapFile(cmp base.Compare, f *manifest.FileMetadata, start, end key) bool {
271 1 : if f == nil {
272 1 : return true
273 1 : }
274 1 : if start != nil {
275 1 : if v := cmp(f.Largest.UserKey, start); v < 0 {
276 1 : return false
277 1 : } else if f.Largest.IsExclusiveSentinel() && v == 0 {
278 0 : return false
279 0 : }
280 : }
281 1 : if end != nil && cmp(f.Smallest.UserKey, end) >= 0 {
282 1 : return false
283 1 : }
284 1 : return true
285 : }
286 :
287 1 : func (m *manifestT) runSummarize(cmd *cobra.Command, args []string) {
288 1 : for _, arg := range args {
289 1 : err := m.runSummarizeOne(cmd.OutOrStdout(), arg)
290 1 : if err != nil {
291 0 : fmt.Fprintf(cmd.OutOrStderr(), "%s\n", err)
292 0 : }
293 : }
294 : }
295 :
296 1 : func (m *manifestT) runSummarizeOne(stdout io.Writer, arg string) error {
297 1 : f, err := m.opts.FS.Open(arg)
298 1 : if err != nil {
299 0 : return err
300 0 : }
301 1 : defer f.Close()
302 1 : fmt.Fprintf(stdout, "%s\n", arg)
303 1 :
304 1 : type summaryBucket struct {
305 1 : bytesAdded [manifest.NumLevels]uint64
306 1 : bytesCompactOut [manifest.NumLevels]uint64
307 1 : }
308 1 : var (
309 1 : bve manifest.BulkVersionEdit
310 1 : newestOverall time.Time
311 1 : oldestOverall time.Time // oldest after initial version edit
312 1 : buckets = map[time.Time]*summaryBucket{}
313 1 : metadatas = map[base.FileNum]*manifest.FileMetadata{}
314 1 : )
315 1 : bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata)
316 1 : rr := record.NewReader(f, 0 /* logNum */)
317 1 : for i := 0; ; i++ {
318 1 : r, err := rr.Next()
319 1 : if err == io.EOF {
320 1 : break
321 1 : } else if err != nil {
322 0 : return err
323 0 : }
324 :
325 1 : var ve manifest.VersionEdit
326 1 : err = ve.Decode(r)
327 1 : if err != nil {
328 0 : return err
329 0 : }
330 1 : if err := bve.Accumulate(&ve); err != nil {
331 0 : return err
332 0 : }
333 :
334 1 : veNewest, veOldest := newestOverall, newestOverall
335 1 : for _, nf := range ve.NewFiles {
336 1 : _, seen := metadatas[nf.Meta.FileNum]
337 1 : metadatas[nf.Meta.FileNum] = nf.Meta
338 1 : if nf.Meta.CreationTime == 0 {
339 0 : continue
340 : }
341 :
342 1 : t := time.Unix(nf.Meta.CreationTime, 0).UTC()
343 1 : if veNewest.Before(t) {
344 1 : veNewest = t
345 1 : }
346 : // Only update the oldest if we haven't already seen this
347 : // file; it might've been moved in which case the sstable's
348 : // creation time is from when it was originally created.
349 1 : if veOldest.After(t) && !seen {
350 0 : veOldest = t
351 0 : }
352 : }
353 : // Ratchet up the most recent timestamp we've seen.
354 1 : if newestOverall.Before(veNewest) {
355 1 : newestOverall = veNewest
356 1 : }
357 :
358 1 : if i == 0 || newestOverall.IsZero() {
359 1 : continue
360 : }
361 : // Update oldestOverall once, when we encounter the first version edit
362 : // at index >= 1. It should be approximately the start time of the
363 : // manifest.
364 1 : if !newestOverall.IsZero() && oldestOverall.IsZero() {
365 1 : oldestOverall = newestOverall
366 1 : }
367 :
368 1 : bucketKey := newestOverall.Truncate(m.summarizeDur)
369 1 : b := buckets[bucketKey]
370 1 : if b == nil {
371 1 : b = &summaryBucket{}
372 1 : buckets[bucketKey] = b
373 1 : }
374 :
375 : // Increase `bytesAdded` for any version edits that only add files.
376 : // These are either flushes or ingests.
377 1 : if len(ve.NewFiles) > 0 && len(ve.DeletedFiles) == 0 {
378 1 : for _, nf := range ve.NewFiles {
379 1 : b.bytesAdded[nf.Level] += nf.Meta.Size
380 1 : }
381 1 : continue
382 : }
383 :
384 : // Increase `bytesCompactOut` for the input level of any compactions
385 : // that remove bytes from a level (excluding intra-L0 compactions).
386 : // compactions.
387 1 : destLevel := -1
388 1 : if len(ve.NewFiles) > 0 {
389 1 : destLevel = ve.NewFiles[0].Level
390 1 : }
391 1 : for dfe := range ve.DeletedFiles {
392 1 : if dfe.Level != destLevel {
393 1 : b.bytesCompactOut[dfe.Level] += metadatas[dfe.FileNum].Size
394 1 : }
395 : }
396 : }
397 :
398 1 : formatUint64 := func(v uint64, _ time.Duration) string {
399 1 : if v == 0 {
400 1 : return "."
401 1 : }
402 1 : return humanize.Bytes.Uint64(v).String()
403 : }
404 1 : formatRate := func(v uint64, dur time.Duration) string {
405 1 : if v == 0 {
406 1 : return "."
407 1 : }
408 1 : secs := dur.Seconds()
409 1 : if secs == 0 {
410 1 : secs = 1
411 1 : }
412 1 : return humanize.Bytes.Uint64(uint64(float64(v)/secs)).String() + "/s"
413 : }
414 :
415 1 : if newestOverall.IsZero() {
416 1 : fmt.Fprintf(stdout, "(no timestamps)\n")
417 1 : } else {
418 1 : // NB: bt begins unaligned with the bucket duration (m.summarizeDur),
419 1 : // but after the first bucket will always be aligned.
420 1 : for bi, bt := 0, oldestOverall; !bt.After(newestOverall); bi, bt = bi+1, bt.Truncate(m.summarizeDur).Add(m.summarizeDur) {
421 1 : // Truncate the start time to calculate the bucket key, and
422 1 : // retrieve the appropriate bucket.
423 1 : bk := bt.Truncate(m.summarizeDur)
424 1 : var bucket summaryBucket
425 1 : if buckets[bk] != nil {
426 1 : bucket = *buckets[bk]
427 1 : }
428 :
429 1 : if bi%10 == 0 {
430 1 : fmt.Fprintf(stdout, " ")
431 1 : fmt.Fprintf(stdout, "_______L0_______L1_______L2_______L3_______L4_______L5_______L6_____TOTAL\n")
432 1 : }
433 1 : fmt.Fprintf(stdout, "%s\n", bt.Format(time.RFC3339))
434 1 :
435 1 : // Compute the bucket duration. It may < `m.summarizeDur` if this is
436 1 : // the first or last bucket.
437 1 : bucketEnd := bt.Truncate(m.summarizeDur).Add(m.summarizeDur)
438 1 : if bucketEnd.After(newestOverall) {
439 1 : bucketEnd = newestOverall
440 1 : }
441 1 : dur := bucketEnd.Sub(bt)
442 1 :
443 1 : stats := []struct {
444 1 : label string
445 1 : format func(uint64, time.Duration) string
446 1 : vals [manifest.NumLevels]uint64
447 1 : }{
448 1 : {"Ingest+Flush", formatUint64, bucket.bytesAdded},
449 1 : {"Ingest+Flush", formatRate, bucket.bytesAdded},
450 1 : {"Compact (out)", formatUint64, bucket.bytesCompactOut},
451 1 : {"Compact (out)", formatRate, bucket.bytesCompactOut},
452 1 : }
453 1 : for _, stat := range stats {
454 1 : var sum uint64
455 1 : for _, v := range stat.vals {
456 1 : sum += v
457 1 : }
458 1 : fmt.Fprintf(stdout, "%20s %8s %8s %8s %8s %8s %8s %8s %8s\n",
459 1 : stat.label,
460 1 : stat.format(stat.vals[0], dur),
461 1 : stat.format(stat.vals[1], dur),
462 1 : stat.format(stat.vals[2], dur),
463 1 : stat.format(stat.vals[3], dur),
464 1 : stat.format(stat.vals[4], dur),
465 1 : stat.format(stat.vals[5], dur),
466 1 : stat.format(stat.vals[6], dur),
467 1 : stat.format(sum, dur))
468 : }
469 : }
470 1 : fmt.Fprintf(stdout, "%s\n", newestOverall.Format(time.RFC3339))
471 : }
472 :
473 1 : dur := newestOverall.Sub(oldestOverall)
474 1 : fmt.Fprintf(stdout, "---\n")
475 1 : fmt.Fprintf(stdout, "Estimated start time: %s\n", oldestOverall.Format(time.RFC3339))
476 1 : fmt.Fprintf(stdout, "Estimated end time: %s\n", newestOverall.Format(time.RFC3339))
477 1 : fmt.Fprintf(stdout, "Estimated duration: %s\n", dur.String())
478 1 :
479 1 : return nil
480 : }
481 :
482 1 : func (m *manifestT) runCheck(cmd *cobra.Command, args []string) {
483 1 : stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
484 1 : ok := true
485 1 : for _, arg := range args {
486 1 : func() {
487 1 : f, err := m.opts.FS.Open(arg)
488 1 : if err != nil {
489 1 : fmt.Fprintf(stderr, "%s\n", err)
490 1 : ok = false
491 1 : return
492 1 : }
493 1 : defer f.Close()
494 1 :
495 1 : var v *manifest.Version
496 1 : var cmp *base.Comparer
497 1 : rr := record.NewReader(f, 0 /* logNum */)
498 1 : // Contains the FileMetadata needed by BulkVersionEdit.Apply.
499 1 : // It accumulates the additions since later edits contain
500 1 : // deletions of earlier added files.
501 1 : addedByFileNum := make(map[base.FileNum]*manifest.FileMetadata)
502 1 : for {
503 1 : offset := rr.Offset()
504 1 : r, err := rr.Next()
505 1 : if err != nil {
506 1 : if err == io.EOF {
507 1 : break
508 : }
509 0 : fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err)
510 0 : ok = false
511 0 : break
512 : }
513 :
514 1 : var ve manifest.VersionEdit
515 1 : err = ve.Decode(r)
516 1 : if err != nil {
517 0 : fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err)
518 0 : ok = false
519 0 : break
520 : }
521 1 : var bve manifest.BulkVersionEdit
522 1 : bve.AddedByFileNum = addedByFileNum
523 1 : if err := bve.Accumulate(&ve); err != nil {
524 0 : fmt.Fprintf(stderr, "%s\n", err)
525 0 : ok = false
526 0 : return
527 0 : }
528 :
529 1 : empty := true
530 1 : if ve.ComparerName != "" {
531 1 : empty = false
532 1 : cmp = m.comparers[ve.ComparerName]
533 1 : if cmp == nil {
534 0 : fmt.Fprintf(stdout, "%s: offset: %d comparer %s not found",
535 0 : arg, offset, ve.ComparerName)
536 0 : ok = false
537 0 : break
538 : }
539 1 : m.fmtKey.setForComparer(ve.ComparerName, m.comparers)
540 : }
541 1 : empty = empty && ve.MinUnflushedLogNum == 0 && ve.ObsoletePrevLogNum == 0 &&
542 1 : ve.LastSeqNum == 0 && len(ve.DeletedFiles) == 0 &&
543 1 : len(ve.NewFiles) == 0
544 1 : if empty {
545 0 : continue
546 : }
547 : // TODO(sbhola): add option to Apply that reports all errors instead of
548 : // one error.
549 1 : newv, err := bve.Apply(v, cmp.Compare, m.fmtKey.fn, 0, m.opts.Experimental.ReadCompactionRate, nil /* zombies */, manifest.AllowSplitUserKeys)
550 1 : if err != nil {
551 1 : fmt.Fprintf(stdout, "%s: offset: %d err: %s\n",
552 1 : arg, offset, err)
553 1 : fmt.Fprintf(stdout, "Version state before failed Apply\n")
554 1 : m.printLevels(cmp.Compare, stdout, v)
555 1 : fmt.Fprintf(stdout, "Version edit that failed\n")
556 1 : for df := range ve.DeletedFiles {
557 0 : fmt.Fprintf(stdout, " deleted: L%d %s\n", df.Level, df.FileNum)
558 0 : }
559 1 : for _, nf := range ve.NewFiles {
560 1 : fmt.Fprintf(stdout, " added: L%d %s:%d",
561 1 : nf.Level, nf.Meta.FileNum, nf.Meta.Size)
562 1 : formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum)
563 1 : formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest)
564 1 : fmt.Fprintf(stdout, "\n")
565 1 : }
566 1 : ok = false
567 1 : break
568 : }
569 1 : v = newv
570 : }
571 : }()
572 : }
573 1 : if ok {
574 1 : fmt.Fprintf(stdout, "OK\n")
575 1 : }
576 : }
|