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