17574: Batch updates into transactions, skip when unchanged.
[arvados.git] / services / keep-balance / balance_run_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bytes"
9         "encoding/json"
10         "fmt"
11         "io"
12         "io/ioutil"
13         "net/http"
14         "net/http/httptest"
15         "os"
16         "strings"
17         "sync"
18         "time"
19
20         "git.arvados.org/arvados.git/lib/config"
21         "git.arvados.org/arvados.git/sdk/go/arvados"
22         "git.arvados.org/arvados.git/sdk/go/arvadostest"
23         "git.arvados.org/arvados.git/sdk/go/ctxlog"
24         "github.com/jmoiron/sqlx"
25         "github.com/prometheus/client_golang/prometheus"
26         "github.com/prometheus/common/expfmt"
27         check "gopkg.in/check.v1"
28 )
29
30 var _ = check.Suite(&runSuite{})
31
32 type reqTracker struct {
33         reqs []http.Request
34         sync.Mutex
35 }
36
37 func (rt *reqTracker) Count() int {
38         rt.Lock()
39         defer rt.Unlock()
40         return len(rt.reqs)
41 }
42
43 func (rt *reqTracker) Add(req *http.Request) int {
44         rt.Lock()
45         defer rt.Unlock()
46         rt.reqs = append(rt.reqs, *req)
47         return len(rt.reqs)
48 }
49
50 var stubServices = []arvados.KeepService{
51         {
52                 UUID:           "zzzzz-bi6l4-000000000000000",
53                 ServiceHost:    "keep0.zzzzz.arvadosapi.com",
54                 ServicePort:    25107,
55                 ServiceSSLFlag: false,
56                 ServiceType:    "disk",
57         },
58         {
59                 UUID:           "zzzzz-bi6l4-000000000000001",
60                 ServiceHost:    "keep1.zzzzz.arvadosapi.com",
61                 ServicePort:    25107,
62                 ServiceSSLFlag: false,
63                 ServiceType:    "disk",
64         },
65         {
66                 UUID:           "zzzzz-bi6l4-000000000000002",
67                 ServiceHost:    "keep2.zzzzz.arvadosapi.com",
68                 ServicePort:    25107,
69                 ServiceSSLFlag: false,
70                 ServiceType:    "disk",
71         },
72         {
73                 UUID:           "zzzzz-bi6l4-000000000000003",
74                 ServiceHost:    "keep3.zzzzz.arvadosapi.com",
75                 ServicePort:    25107,
76                 ServiceSSLFlag: false,
77                 ServiceType:    "disk",
78         },
79         {
80                 UUID:           "zzzzz-bi6l4-h0a0xwut9qa6g3a",
81                 ServiceHost:    "keep.zzzzz.arvadosapi.com",
82                 ServicePort:    25333,
83                 ServiceSSLFlag: true,
84                 ServiceType:    "proxy",
85         },
86 }
87
88 var stubMounts = map[string][]arvados.KeepMount{
89         "keep0.zzzzz.arvadosapi.com:25107": {{
90                 UUID:     "zzzzz-ivpuk-000000000000000",
91                 DeviceID: "keep0-vol0",
92         }},
93         "keep1.zzzzz.arvadosapi.com:25107": {{
94                 UUID:     "zzzzz-ivpuk-100000000000000",
95                 DeviceID: "keep1-vol0",
96         }},
97         "keep2.zzzzz.arvadosapi.com:25107": {{
98                 UUID:     "zzzzz-ivpuk-200000000000000",
99                 DeviceID: "keep2-vol0",
100         }},
101         "keep3.zzzzz.arvadosapi.com:25107": {{
102                 UUID:     "zzzzz-ivpuk-300000000000000",
103                 DeviceID: "keep3-vol0",
104         }},
105 }
106
107 // stubServer is an HTTP transport that intercepts and processes all
108 // requests using its own handlers.
109 type stubServer struct {
110         mux      *http.ServeMux
111         srv      *httptest.Server
112         mutex    sync.Mutex
113         Requests reqTracker
114         logf     func(string, ...interface{})
115 }
116
117 // Start initializes the stub server and returns an *http.Client that
118 // uses the stub server to handle all requests.
119 //
120 // A stubServer that has been started should eventually be shut down
121 // with Close().
122 func (s *stubServer) Start() *http.Client {
123         // Set up a config.Client that forwards all requests to s.mux
124         // via s.srv. Test cases will attach handlers to s.mux to get
125         // the desired responses.
126         s.mux = http.NewServeMux()
127         s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
128                 s.mutex.Lock()
129                 s.Requests.Add(r)
130                 s.mutex.Unlock()
131                 w.Header().Set("Content-Type", "application/json")
132                 s.mux.ServeHTTP(w, r)
133         }))
134         return &http.Client{Transport: s}
135 }
136
137 func (s *stubServer) RoundTrip(req *http.Request) (*http.Response, error) {
138         w := httptest.NewRecorder()
139         s.mux.ServeHTTP(w, req)
140         return &http.Response{
141                 StatusCode: w.Code,
142                 Status:     fmt.Sprintf("%d %s", w.Code, http.StatusText(w.Code)),
143                 Header:     w.HeaderMap,
144                 Body:       ioutil.NopCloser(w.Body)}, nil
145 }
146
147 // Close releases resources used by the server.
148 func (s *stubServer) Close() {
149         s.srv.Close()
150 }
151
152 func (s *stubServer) serveStatic(path, data string) *reqTracker {
153         rt := &reqTracker{}
154         s.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
155                 rt.Add(r)
156                 if r.Body != nil {
157                         ioutil.ReadAll(r.Body)
158                         r.Body.Close()
159                 }
160                 io.WriteString(w, data)
161         })
162         return rt
163 }
164
165 func (s *stubServer) serveCurrentUserAdmin() *reqTracker {
166         return s.serveStatic("/arvados/v1/users/current",
167                 `{"uuid":"zzzzz-tpzed-000000000000000","is_admin":true,"is_active":true}`)
168 }
169
170 func (s *stubServer) serveCurrentUserNotAdmin() *reqTracker {
171         return s.serveStatic("/arvados/v1/users/current",
172                 `{"uuid":"zzzzz-tpzed-000000000000000","is_admin":false,"is_active":true}`)
173 }
174
175 func (s *stubServer) serveDiscoveryDoc() *reqTracker {
176         return s.serveStatic("/discovery/v1/apis/arvados/v1/rest",
177                 `{"defaultCollectionReplication":2}`)
178 }
179
180 func (s *stubServer) serveZeroCollections() *reqTracker {
181         return s.serveStatic("/arvados/v1/collections",
182                 `{"items":[],"items_available":0}`)
183 }
184
185 func (s *stubServer) serveFooBarFileCollections() *reqTracker {
186         rt := &reqTracker{}
187         s.mux.HandleFunc("/arvados/v1/collections", func(w http.ResponseWriter, r *http.Request) {
188                 r.ParseForm()
189                 rt.Add(r)
190                 if strings.Contains(r.Form.Get("filters"), `modified_at`) {
191                         io.WriteString(w, `{"items_available":0,"items":[]}`)
192                 } else {
193                         io.WriteString(w, `{"items_available":3,"items":[
194                                 {"uuid":"zzzzz-4zz18-aaaaaaaaaaaaaaa","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
195                                 {"uuid":"zzzzz-4zz18-ehbhgtheo8909or","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
196                                 {"uuid":"zzzzz-4zz18-znfnqtbbv4spc3w","portable_data_hash":"1f4b0bc7583c2a7f9102c395f4ffc5e3+45","manifest_text":". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo\n","modified_at":"2014-02-03T17:22:54Z"}]}`)
197                 }
198         })
199         return rt
200 }
201
202 func (s *stubServer) serveCollectionsButSkipOne() *reqTracker {
203         rt := &reqTracker{}
204         s.mux.HandleFunc("/arvados/v1/collections", func(w http.ResponseWriter, r *http.Request) {
205                 r.ParseForm()
206                 rt.Add(r)
207                 if strings.Contains(r.Form.Get("filters"), `"modified_at","\u003c="`) {
208                         io.WriteString(w, `{"items_available":3,"items":[]}`)
209                 } else if strings.Contains(r.Form.Get("filters"), `"modified_at","\u003e`) {
210                         io.WriteString(w, `{"items_available":0,"items":[]}`)
211                 } else if strings.Contains(r.Form.Get("filters"), `"modified_at","="`) && strings.Contains(r.Form.Get("filters"), `"uuid","\u003e"`) {
212                         io.WriteString(w, `{"items_available":0,"items":[]}`)
213                 } else if strings.Contains(r.Form.Get("filters"), `"modified_at","=",null`) {
214                         io.WriteString(w, `{"items_available":0,"items":[]}`)
215                 } else {
216                         io.WriteString(w, `{"items_available":2,"items":[
217                                 {"uuid":"zzzzz-4zz18-ehbhgtheo8909or","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
218                                 {"uuid":"zzzzz-4zz18-znfnqtbbv4spc3w","portable_data_hash":"1f4b0bc7583c2a7f9102c395f4ffc5e3+45","manifest_text":". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo\n","modified_at":"2014-02-03T17:22:54Z"}]}`)
219                 }
220         })
221         return rt
222 }
223
224 func (s *stubServer) serveZeroKeepServices() *reqTracker {
225         return s.serveJSON("/arvados/v1/keep_services", arvados.KeepServiceList{})
226 }
227
228 func (s *stubServer) serveKeepServices(svcs []arvados.KeepService) *reqTracker {
229         return s.serveJSON("/arvados/v1/keep_services", arvados.KeepServiceList{
230                 ItemsAvailable: len(svcs),
231                 Items:          svcs,
232         })
233 }
234
235 func (s *stubServer) serveJSON(path string, resp interface{}) *reqTracker {
236         rt := &reqTracker{}
237         s.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
238                 rt.Add(r)
239                 json.NewEncoder(w).Encode(resp)
240         })
241         return rt
242 }
243
244 func (s *stubServer) serveKeepstoreMounts() *reqTracker {
245         rt := &reqTracker{}
246         s.mux.HandleFunc("/mounts", func(w http.ResponseWriter, r *http.Request) {
247                 rt.Add(r)
248                 json.NewEncoder(w).Encode(stubMounts[r.Host])
249         })
250         return rt
251 }
252
253 func (s *stubServer) serveKeepstoreIndexFoo4Bar1() *reqTracker {
254         rt := &reqTracker{}
255         s.mux.HandleFunc("/index/", func(w http.ResponseWriter, r *http.Request) {
256                 count := rt.Add(r)
257                 if r.Host == "keep0.zzzzz.arvadosapi.com:25107" {
258                         io.WriteString(w, "37b51d194a7513e45b56f6524f2d51f2+3 12345678\n")
259                 }
260                 fmt.Fprintf(w, "acbd18db4cc2f85cedef654fccc4a4d8+3 %d\n\n", 12345678+count)
261         })
262         for _, mounts := range stubMounts {
263                 for i, mnt := range mounts {
264                         i := i
265                         s.mux.HandleFunc(fmt.Sprintf("/mounts/%s/blocks", mnt.UUID), func(w http.ResponseWriter, r *http.Request) {
266                                 count := rt.Add(r)
267                                 if i == 0 && r.Host == "keep0.zzzzz.arvadosapi.com:25107" {
268                                         io.WriteString(w, "37b51d194a7513e45b56f6524f2d51f2+3 12345678\n")
269                                 }
270                                 if i == 0 {
271                                         fmt.Fprintf(w, "acbd18db4cc2f85cedef654fccc4a4d8+3 %d\n", 12345678+count)
272                                 }
273                                 fmt.Fprintf(w, "\n")
274                         })
275                 }
276         }
277         return rt
278 }
279
280 func (s *stubServer) serveKeepstoreIndexFoo1() *reqTracker {
281         rt := &reqTracker{}
282         s.mux.HandleFunc("/index/", func(w http.ResponseWriter, r *http.Request) {
283                 rt.Add(r)
284                 io.WriteString(w, "acbd18db4cc2f85cedef654fccc4a4d8+3 12345678\n\n")
285         })
286         for _, mounts := range stubMounts {
287                 for i, mnt := range mounts {
288                         i := i
289                         s.mux.HandleFunc(fmt.Sprintf("/mounts/%s/blocks", mnt.UUID), func(w http.ResponseWriter, r *http.Request) {
290                                 rt.Add(r)
291                                 if i == 0 {
292                                         io.WriteString(w, "acbd18db4cc2f85cedef654fccc4a4d8+3 12345678\n\n")
293                                 } else {
294                                         io.WriteString(w, "\n")
295                                 }
296                         })
297                 }
298         }
299         return rt
300 }
301
302 func (s *stubServer) serveKeepstoreTrash() *reqTracker {
303         return s.serveStatic("/trash", `{}`)
304 }
305
306 func (s *stubServer) serveKeepstorePull() *reqTracker {
307         return s.serveStatic("/pull", `{}`)
308 }
309
310 type runSuite struct {
311         stub   stubServer
312         config *arvados.Cluster
313         db     *sqlx.DB
314         client *arvados.Client
315 }
316
317 func (s *runSuite) newServer(options *RunOptions) *Server {
318         srv := &Server{
319                 Cluster:    s.config,
320                 ArvClient:  s.client,
321                 RunOptions: *options,
322                 Metrics:    newMetrics(prometheus.NewRegistry()),
323                 Logger:     options.Logger,
324                 Dumper:     options.Dumper,
325                 DB:         s.db,
326         }
327         return srv
328 }
329
330 func (s *runSuite) SetUpTest(c *check.C) {
331         cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
332         c.Assert(err, check.Equals, nil)
333         s.config, err = cfg.GetCluster("")
334         c.Assert(err, check.Equals, nil)
335         s.db, err = sqlx.Open("postgres", s.config.PostgreSQL.Connection.String())
336         c.Assert(err, check.IsNil)
337
338         s.config.Collections.BalancePeriod = arvados.Duration(time.Second)
339         arvadostest.SetServiceURL(&s.config.Services.Keepbalance, "http://localhost:/")
340
341         s.client = &arvados.Client{
342                 AuthToken: "xyzzy",
343                 APIHost:   "zzzzz.arvadosapi.com",
344                 Client:    s.stub.Start()}
345
346         s.stub.serveDiscoveryDoc()
347         s.stub.logf = c.Logf
348 }
349
350 func (s *runSuite) TearDownTest(c *check.C) {
351         s.stub.Close()
352 }
353
354 func (s *runSuite) TestRefuseZeroCollections(c *check.C) {
355         defer arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil)
356         _, err := s.db.Exec(`delete from collections`)
357         c.Assert(err, check.IsNil)
358         opts := RunOptions{
359                 CommitPulls: true,
360                 CommitTrash: true,
361                 Logger:      ctxlog.TestLogger(c),
362         }
363         s.stub.serveCurrentUserAdmin()
364         s.stub.serveZeroCollections()
365         s.stub.serveKeepServices(stubServices)
366         s.stub.serveKeepstoreMounts()
367         s.stub.serveKeepstoreIndexFoo4Bar1()
368         trashReqs := s.stub.serveKeepstoreTrash()
369         pullReqs := s.stub.serveKeepstorePull()
370         srv := s.newServer(&opts)
371         _, err = srv.runOnce()
372         c.Check(err, check.ErrorMatches, "received zero collections")
373         c.Check(trashReqs.Count(), check.Equals, 4)
374         c.Check(pullReqs.Count(), check.Equals, 0)
375 }
376
377 func (s *runSuite) TestRefuseNonAdmin(c *check.C) {
378         opts := RunOptions{
379                 CommitPulls: true,
380                 CommitTrash: true,
381                 Logger:      ctxlog.TestLogger(c),
382         }
383         s.stub.serveCurrentUserNotAdmin()
384         s.stub.serveZeroCollections()
385         s.stub.serveKeepServices(stubServices)
386         s.stub.serveKeepstoreMounts()
387         trashReqs := s.stub.serveKeepstoreTrash()
388         pullReqs := s.stub.serveKeepstorePull()
389         srv := s.newServer(&opts)
390         _, err := srv.runOnce()
391         c.Check(err, check.ErrorMatches, "current user .* is not .* admin user")
392         c.Check(trashReqs.Count(), check.Equals, 0)
393         c.Check(pullReqs.Count(), check.Equals, 0)
394 }
395
396 func (s *runSuite) TestWriteLostBlocks(c *check.C) {
397         lostf, err := ioutil.TempFile("", "keep-balance-lost-blocks-test-")
398         c.Assert(err, check.IsNil)
399         s.config.Collections.BlobMissingReport = lostf.Name()
400         defer os.Remove(lostf.Name())
401         opts := RunOptions{
402                 CommitPulls: true,
403                 CommitTrash: true,
404                 Logger:      ctxlog.TestLogger(c),
405         }
406         s.stub.serveCurrentUserAdmin()
407         s.stub.serveFooBarFileCollections()
408         s.stub.serveKeepServices(stubServices)
409         s.stub.serveKeepstoreMounts()
410         s.stub.serveKeepstoreIndexFoo1()
411         s.stub.serveKeepstoreTrash()
412         s.stub.serveKeepstorePull()
413         srv := s.newServer(&opts)
414         c.Assert(err, check.IsNil)
415         _, err = srv.runOnce()
416         c.Check(err, check.IsNil)
417         lost, err := ioutil.ReadFile(lostf.Name())
418         c.Assert(err, check.IsNil)
419         c.Check(string(lost), check.Matches, `(?ms).*37b51d194a7513e45b56f6524f2d51f2.* fa7aeb5140e2848d39b416daeef4ffc5\+45.*`)
420 }
421
422 func (s *runSuite) TestDryRun(c *check.C) {
423         opts := RunOptions{
424                 CommitPulls: false,
425                 CommitTrash: false,
426                 Logger:      ctxlog.TestLogger(c),
427         }
428         s.stub.serveCurrentUserAdmin()
429         collReqs := s.stub.serveFooBarFileCollections()
430         s.stub.serveKeepServices(stubServices)
431         s.stub.serveKeepstoreMounts()
432         s.stub.serveKeepstoreIndexFoo4Bar1()
433         trashReqs := s.stub.serveKeepstoreTrash()
434         pullReqs := s.stub.serveKeepstorePull()
435         srv := s.newServer(&opts)
436         bal, err := srv.runOnce()
437         c.Check(err, check.IsNil)
438         for _, req := range collReqs.reqs {
439                 c.Check(req.Form.Get("include_trash"), check.Equals, "true")
440                 c.Check(req.Form.Get("include_old_versions"), check.Equals, "true")
441         }
442         c.Check(trashReqs.Count(), check.Equals, 0)
443         c.Check(pullReqs.Count(), check.Equals, 0)
444         c.Check(bal.stats.pulls, check.Not(check.Equals), 0)
445         c.Check(bal.stats.underrep.replicas, check.Not(check.Equals), 0)
446         c.Check(bal.stats.overrep.replicas, check.Not(check.Equals), 0)
447 }
448
449 func (s *runSuite) TestCommit(c *check.C) {
450         s.config.Collections.BlobMissingReport = c.MkDir() + "/keep-balance-lost-blocks-test-"
451         s.config.ManagementToken = "xyzzy"
452         opts := RunOptions{
453                 CommitPulls: true,
454                 CommitTrash: true,
455                 Logger:      ctxlog.TestLogger(c),
456                 Dumper:      ctxlog.TestLogger(c),
457         }
458         s.stub.serveCurrentUserAdmin()
459         s.stub.serveFooBarFileCollections()
460         s.stub.serveKeepServices(stubServices)
461         s.stub.serveKeepstoreMounts()
462         s.stub.serveKeepstoreIndexFoo4Bar1()
463         trashReqs := s.stub.serveKeepstoreTrash()
464         pullReqs := s.stub.serveKeepstorePull()
465         srv := s.newServer(&opts)
466         bal, err := srv.runOnce()
467         c.Check(err, check.IsNil)
468         c.Check(trashReqs.Count(), check.Equals, 8)
469         c.Check(pullReqs.Count(), check.Equals, 4)
470         // "foo" block is overreplicated by 2
471         c.Check(bal.stats.trashes, check.Equals, 2)
472         // "bar" block is underreplicated by 1, and its only copy is
473         // in a poor rendezvous position
474         c.Check(bal.stats.pulls, check.Equals, 2)
475
476         lost, err := ioutil.ReadFile(s.config.Collections.BlobMissingReport)
477         c.Assert(err, check.IsNil)
478         c.Check(string(lost), check.Not(check.Matches), `(?ms).*acbd18db4cc2f85cedef654fccc4a4d8.*`)
479
480         buf, err := s.getMetrics(c, srv)
481         c.Check(err, check.IsNil)
482         bufstr := buf.String()
483         c.Check(bufstr, check.Matches, `(?ms).*\narvados_keep_total_bytes 15\n.*`)
484         c.Check(bufstr, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_sum [0-9\.]+\n.*`)
485         c.Check(bufstr, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_count 1\n.*`)
486         c.Check(bufstr, check.Matches, `(?ms).*\narvados_keep_dedup_byte_ratio [1-9].*`)
487         c.Check(bufstr, check.Matches, `(?ms).*\narvados_keep_dedup_block_ratio [1-9].*`)
488 }
489
490 func (s *runSuite) TestRunForever(c *check.C) {
491         s.config.ManagementToken = "xyzzy"
492         opts := RunOptions{
493                 CommitPulls: true,
494                 CommitTrash: true,
495                 Logger:      ctxlog.TestLogger(c),
496                 Dumper:      ctxlog.TestLogger(c),
497         }
498         s.stub.serveCurrentUserAdmin()
499         s.stub.serveFooBarFileCollections()
500         s.stub.serveKeepServices(stubServices)
501         s.stub.serveKeepstoreMounts()
502         s.stub.serveKeepstoreIndexFoo4Bar1()
503         trashReqs := s.stub.serveKeepstoreTrash()
504         pullReqs := s.stub.serveKeepstorePull()
505
506         stop := make(chan interface{})
507         s.config.Collections.BalancePeriod = arvados.Duration(time.Millisecond)
508         srv := s.newServer(&opts)
509
510         done := make(chan bool)
511         go func() {
512                 srv.runForever(stop)
513                 close(done)
514         }()
515
516         // Each run should send 4 pull lists + 4 trash lists. The
517         // first run should also send 4 empty trash lists at
518         // startup. We should complete all four runs in much less than
519         // a second.
520         for t0 := time.Now(); pullReqs.Count() < 16 && time.Since(t0) < 10*time.Second; {
521                 time.Sleep(time.Millisecond)
522         }
523         stop <- true
524         <-done
525         c.Check(pullReqs.Count() >= 16, check.Equals, true)
526         c.Check(trashReqs.Count(), check.Equals, pullReqs.Count()+4)
527
528         buf, err := s.getMetrics(c, srv)
529         c.Check(err, check.IsNil)
530         c.Check(buf, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_count `+fmt.Sprintf("%d", pullReqs.Count()/4)+`\n.*`)
531 }
532
533 func (s *runSuite) getMetrics(c *check.C, srv *Server) (*bytes.Buffer, error) {
534         mfs, err := srv.Metrics.reg.Gather()
535         if err != nil {
536                 return nil, err
537         }
538
539         var buf bytes.Buffer
540         for _, mf := range mfs {
541                 if _, err := expfmt.MetricFamilyToText(&buf, mf); err != nil {
542                         return nil, err
543                 }
544         }
545
546         return &buf, nil
547 }