1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package server
18
19import (
20	"context"
21	"time"
22
23	"github.com/containerd/containerd/errdefs"
24	snapshot "github.com/containerd/containerd/snapshots"
25	"github.com/pkg/errors"
26	"github.com/sirupsen/logrus"
27
28	snapshotstore "github.com/containerd/containerd/pkg/cri/store/snapshot"
29	ctrdutil "github.com/containerd/containerd/pkg/cri/util"
30)
31
32// snapshotsSyncer syncs snapshot stats periodically. imagefs info and container stats
33// should both use cached result here.
34// TODO(random-liu): Benchmark with high workload. We may need a statsSyncer instead if
35// benchmark result shows that container cpu/memory stats also need to be cached.
36type snapshotsSyncer struct {
37	store       *snapshotstore.Store
38	snapshotter snapshot.Snapshotter
39	syncPeriod  time.Duration
40}
41
42// newSnapshotsSyncer creates a snapshot syncer.
43func newSnapshotsSyncer(store *snapshotstore.Store, snapshotter snapshot.Snapshotter,
44	period time.Duration) *snapshotsSyncer {
45	return &snapshotsSyncer{
46		store:       store,
47		snapshotter: snapshotter,
48		syncPeriod:  period,
49	}
50}
51
52// start starts the snapshots syncer. No stop function is needed because
53// the syncer doesn't update any persistent states, it's fine to let it
54// exit with the process.
55func (s *snapshotsSyncer) start() {
56	tick := time.NewTicker(s.syncPeriod)
57	go func() {
58		defer tick.Stop()
59		// TODO(random-liu): This is expensive. We should do benchmark to
60		// check the resource usage and optimize this.
61		for {
62			if err := s.sync(); err != nil {
63				logrus.WithError(err).Error("Failed to sync snapshot stats")
64			}
65			<-tick.C
66		}
67	}()
68}
69
70// sync updates all snapshots stats.
71func (s *snapshotsSyncer) sync() error {
72	ctx := ctrdutil.NamespacedContext()
73	start := time.Now().UnixNano()
74	var snapshots []snapshot.Info
75	// Do not call `Usage` directly in collect function, because
76	// `Usage` takes time, we don't want `Walk` to hold read lock
77	// of snapshot metadata store for too long time.
78	// TODO(random-liu): Set timeout for the following 2 contexts.
79	if err := s.snapshotter.Walk(ctx, func(ctx context.Context, info snapshot.Info) error {
80		snapshots = append(snapshots, info)
81		return nil
82	}); err != nil {
83		return errors.Wrap(err, "walk all snapshots failed")
84	}
85	for _, info := range snapshots {
86		sn, err := s.store.Get(info.Name)
87		if err == nil {
88			// Only update timestamp for non-active snapshot.
89			if sn.Kind == info.Kind && sn.Kind != snapshot.KindActive {
90				sn.Timestamp = time.Now().UnixNano()
91				s.store.Add(sn)
92				continue
93			}
94		}
95		// Get newest stats if the snapshot is new or active.
96		sn = snapshotstore.Snapshot{
97			Key:       info.Name,
98			Kind:      info.Kind,
99			Timestamp: time.Now().UnixNano(),
100		}
101		usage, err := s.snapshotter.Usage(ctx, info.Name)
102		if err != nil {
103			if !errdefs.IsNotFound(err) {
104				logrus.WithError(err).Errorf("Failed to get usage for snapshot %q", info.Name)
105			}
106			continue
107		}
108		sn.Size = uint64(usage.Size)
109		sn.Inodes = uint64(usage.Inodes)
110		s.store.Add(sn)
111	}
112	for _, sn := range s.store.List() {
113		if sn.Timestamp >= start {
114			continue
115		}
116		// Delete the snapshot stats if it's not updated this time.
117		s.store.Delete(sn.Key)
118	}
119	return nil
120}
121