Merge branch '22124-keepstore-double-slash'
[arvados.git] / lib / deduplicationreport / report.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package deduplicationreport
6
7 import (
8         "bytes"
9         "flag"
10         "fmt"
11         "io"
12         "strings"
13
14         "git.arvados.org/arvados.git/lib/cmd"
15         "git.arvados.org/arvados.git/sdk/go/arvados"
16         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
17         "git.arvados.org/arvados.git/sdk/go/blockdigest"
18
19         "github.com/dustin/go-humanize"
20         "github.com/sirupsen/logrus"
21 )
22
23 func deDuplicate(inputs []string) (trimmed []string) {
24         seen := make(map[string]bool)
25         for _, uuid := range inputs {
26                 if !seen[uuid] {
27                         seen[uuid] = true
28                         trimmed = append(trimmed, uuid)
29                 }
30         }
31         return
32 }
33
34 // parseFlags returns either some inputs to process, or (if there are
35 // no inputs to process) a nil slice and a suitable exit code.
36 func parseFlags(prog string, args []string, logger *logrus.Logger, stderr io.Writer) (inputs []string, exitcode int) {
37         flags := flag.NewFlagSet(prog, flag.ContinueOnError)
38         flags.Usage = func() {
39                 fmt.Fprintf(flags.Output(), `
40 Usage:
41   %s [options ...] <collection-uuid> <collection-uuid> ...
42
43   %s [options ...] <collection-pdh>,<collection-uuid> \
44      <collection-pdh>,<collection-uuid> ...
45
46   This program analyzes the overlap in blocks used by 2 or more collections. It
47   prints a deduplication report that shows the nominal space used by the
48   collections, as well as the actual size and the amount of space that is saved
49   by Keep's deduplication.
50
51   The list of collections may be provided in two ways. A list of collection
52   uuids is sufficient. Alternatively, the PDH for each collection may also be
53   provided. This is will greatly speed up operation when the list contains
54   multiple collections with the same PDH.
55
56   Exit status will be zero if there were no errors generating the report.
57
58 Example:
59
60   Use the 'arv' and 'jq' commands to get the list of the 100
61   largest collections and generate the deduplication report:
62
63   arv collection list --order 'file_size_total desc' --limit 100 | \
64     jq -r '.items[] | [.portable_data_hash,.uuid] |@csv' | \
65     sed -e 's/"//g'|tr '\n' ' ' | \
66     xargs %s
67
68 Options:
69 `, prog, prog, prog)
70                 flags.PrintDefaults()
71         }
72         loglevel := flags.String("log-level", "info", "logging level (debug, info, ...)")
73         if ok, code := cmd.ParseFlags(flags, prog, args, "collection-uuid [...]", stderr); !ok {
74                 return nil, code
75         }
76
77         inputs = deDuplicate(flags.Args())
78
79         if len(inputs) < 1 {
80                 fmt.Fprintf(stderr, "Error: no collections provided\n")
81                 return nil, 2
82         }
83
84         lvl, err := logrus.ParseLevel(*loglevel)
85         if err != nil {
86                 fmt.Fprintf(stderr, "Error: cannot parse log level: %s\n", err)
87                 return nil, 2
88         }
89         logger.SetLevel(lvl)
90         return inputs, 0
91 }
92
93 func blockList(collection arvados.Collection) (blocks map[string]int) {
94         blocks = make(map[string]int)
95         for _, token := range bytes.Split([]byte(collection.ManifestText), []byte{' '}) {
96                 if blockdigest.IsBlockLocator(string(token)) {
97                         loc, _ := blockdigest.ParseBlockLocator(string(token))
98                         blocks[loc.Digest.String()] = loc.Size
99                 }
100         }
101         return
102 }
103
104 func report(prog string, args []string, logger *logrus.Logger, stdout, stderr io.Writer) (exitcode int) {
105         var inputs []string
106
107         inputs, exitcode = parseFlags(prog, args, logger, stderr)
108         if inputs == nil {
109                 return
110         }
111
112         // Arvados Client setup
113         arv, err := arvadosclient.MakeArvadosClient()
114         if err != nil {
115                 logger.Errorf("Error creating Arvados object: %s", err)
116                 exitcode = 1
117                 return
118         }
119
120         type Col struct {
121                 FileSizeTotal int64
122                 FileCount     int64
123         }
124
125         blocks := make(map[string]map[string]int)
126         pdhs := make(map[string]Col)
127         var nominalSize int64
128
129         for _, input := range inputs {
130                 var uuid string
131                 var pdh string
132                 if strings.Contains(input, ",") {
133                         // The input is in the format pdh,uuid. This will allow us to save time on duplicate pdh's
134                         tmp := strings.Split(input, ",")
135                         pdh = tmp[0]
136                         uuid = tmp[1]
137                 } else {
138                         // The input must be a plain uuid
139                         uuid = input
140                 }
141                 if !strings.Contains(uuid, "-4zz18-") {
142                         logger.Errorf("Error: uuid must refer to collection object")
143                         exitcode = 1
144                         return
145                 }
146                 if _, ok := pdhs[pdh]; ok {
147                         // We've processed a collection with this pdh already. Simply add its
148                         // size to the totals and move on to the next one.
149                         // Note that we simply trust the PDH matches the collection UUID here,
150                         // in other words, we use it over the UUID. If they don't match, the report
151                         // will be wrong.
152                         nominalSize += pdhs[pdh].FileSizeTotal
153                 } else {
154                         var collection arvados.Collection
155                         err = arv.Get("collections", uuid, nil, &collection)
156                         if err != nil {
157                                 logger.Errorf("Error: unable to retrieve collection: %s", err)
158                                 exitcode = 1
159                                 return
160                         }
161                         blocks[uuid] = make(map[string]int)
162                         blocks[uuid] = blockList(collection)
163                         if pdh != "" && collection.PortableDataHash != pdh {
164                                 logger.Errorf("Error: the collection with UUID %s has PDH %s, but a different PDH was provided in the arguments: %s", uuid, collection.PortableDataHash, pdh)
165                                 exitcode = 1
166                                 return
167                         }
168                         if pdh == "" {
169                                 pdh = collection.PortableDataHash
170                         }
171
172                         col := Col{}
173                         if collection.FileSizeTotal != 0 || collection.FileCount != 0 {
174                                 nominalSize += collection.FileSizeTotal
175                                 col.FileSizeTotal = collection.FileSizeTotal
176                                 col.FileCount = int64(collection.FileCount)
177                         } else {
178                                 // Collections created with old Arvados versions do not always have the total file size and count cached in the collections object
179                                 var collSize int64
180                                 for _, size := range blocks[uuid] {
181                                         collSize += int64(size)
182                                 }
183                                 nominalSize += collSize
184                                 col.FileSizeTotal = collSize
185                         }
186                         pdhs[pdh] = col
187                 }
188
189                 if pdhs[pdh].FileCount != 0 {
190                         fmt.Fprintf(stdout, "Collection %s: pdh %s; nominal size %d (%s); file count %d\n", uuid, pdh, pdhs[pdh].FileSizeTotal, humanize.IBytes(uint64(pdhs[pdh].FileSizeTotal)), pdhs[pdh].FileCount)
191                 } else {
192                         fmt.Fprintf(stdout, "Collection %s: pdh %s; nominal size %d (%s)\n", uuid, pdh, pdhs[pdh].FileSizeTotal, humanize.IBytes(uint64(pdhs[pdh].FileSizeTotal)))
193                 }
194         }
195
196         var totalSize int64
197         seen := make(map[string]bool)
198         for _, v := range blocks {
199                 for pdh, size := range v {
200                         if !seen[pdh] {
201                                 seen[pdh] = true
202                                 totalSize += int64(size)
203                         }
204                 }
205         }
206         fmt.Fprintln(stdout)
207         fmt.Fprintf(stdout, "Collections:                 %15d\n", len(inputs))
208         fmt.Fprintf(stdout, "Nominal size of stored data: %15d bytes (%s)\n", nominalSize, humanize.IBytes(uint64(nominalSize)))
209         fmt.Fprintf(stdout, "Actual size of stored data:  %15d bytes (%s)\n", totalSize, humanize.IBytes(uint64(totalSize)))
210         fmt.Fprintf(stdout, "Saved by Keep deduplication: %15d bytes (%s)\n", nominalSize-totalSize, humanize.IBytes(uint64(nominalSize-totalSize)))
211
212         return exitcode
213 }