summaryrefslogtreecommitdiffstats
path: root/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImageGarbageCollector.java
blob: ab36ace890450d42446504664e89566bcf3b3846 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.dockerapi;

import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Container;

import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author freva
 */
public class DockerImageGarbageCollector {
    private final Duration MIN_AGE_IMAGE_GC;
    private Map<String, Instant> lastTimeUsedByImageId = new HashMap<>();

    public DockerImageGarbageCollector(Duration minAgeImageToDelete) {
        MIN_AGE_IMAGE_GC = minAgeImageToDelete;
    }

    public void updateLastUsedTimeFor(String imageId) {
        updateLastUsedTimeFor(imageId, Instant.now());
    }

    void updateLastUsedTimeFor(String imageId, Instant at) {
        lastTimeUsedByImageId.put(imageId, at);
    }

    /**
     * Generates lists of images that are safe to delete, in the order that is safe to delete them (children before
     * parents). The function starts with the set of all local images and then filters out images that are used now
     * and images that have been used recently (because they might be re-used again in near future).
     *
     * @param images List of all the local images
     * @param containers List of all the containers, including the ones that are stopped
     * @return List of image tags of unused images, if unused image has no tag, will return image ID instead.
     */
    public List<DockerImage> getUnusedDockerImages(List<Image> images, List<Container> containers) {
        Map<String, Image> dockerImageByImageId = images.stream().collect(Collectors.toMap(Image::getId, img -> img));
        Map<String, Image> unusedImagesByContainers = filterOutImagesUsedByContainers(dockerImageByImageId, containers);
        Map<String, Image> unusedImagesByRecent = filterOutRecentImages(unusedImagesByContainers);

        return unusedImagesByRecent.keySet().stream()
                .sorted((o1, o2) -> {
                    // If image2 is parent of image1, image1 comes before image2
                    if (imageIsDescendantOf(unusedImagesByRecent, o1, o2)) return -1;
                    // If image1 is parent of image2, image2 comes before image1
                    else if (imageIsDescendantOf(unusedImagesByRecent, o2, o1)) return 1;
                    // Otherwise, sort lexicographically by image name (For testing)
                    else return o1.compareTo(o2);
                })
                .flatMap(imageId -> {
                    // Deleting an image by image ID with multiple tags will fail -> map IDs to all the tags refering to the ID
                    String[] repoTags = unusedImagesByRecent.get(imageId).getRepoTags();
                    return (repoTags == null) ? Stream.of(imageId) : Stream.of(repoTags);
                })
                .map(DockerImage::new)
                .collect(Collectors.toList());
    }

    private Map<String, Image> filterOutImagesUsedByContainers(
            Map<String, Image> dockerImagesByImageId, List<com.github.dockerjava.api.model.Container> containerList) {
        Map<String, Image> filteredDockerImagesByImageId = new HashMap<>(dockerImagesByImageId);

        for (com.github.dockerjava.api.model.Container container : containerList) {
            String imageToSpare = container.getImageId();
            do {
                // May be null if two images have have the same parent, the first image will remove the parent, the
                // second will get null.
                Image sparedImage = filteredDockerImagesByImageId.remove(imageToSpare);
                imageToSpare = sparedImage == null ? "" : sparedImage.getParentId();
            } while (!imageToSpare.isEmpty());
        }

        return filteredDockerImagesByImageId;
    }

    private Map<String, Image> filterOutRecentImages(Map<String, Image> dockerImageByImageId) {
        Map<String, Image> filteredDockerImagesByImageId = new HashMap<>(dockerImageByImageId);

        final Instant now = Instant.now();
        filteredDockerImagesByImageId.keySet().forEach(imageId -> {
            if (! lastTimeUsedByImageId.containsKey(imageId)) lastTimeUsedByImageId.put(imageId, now);
        });

        lastTimeUsedByImageId.entrySet().stream()
                .filter(entry -> Duration.between(entry.getValue(), now).minus(MIN_AGE_IMAGE_GC).isNegative())
                .map(Map.Entry::getKey)
                .forEach(image -> {
                    String imageToSpare = image;
                    do {
                        Image sparedImage = filteredDockerImagesByImageId.remove(imageToSpare);
                        imageToSpare = sparedImage == null ? "" : sparedImage.getParentId();
                    } while (!imageToSpare.isEmpty());
                });
        return filteredDockerImagesByImageId;
    }

    /**
     * Returns true if ancestor is a parent or grand-parent or grand-grand-parent, etc. of img
     */
    private boolean imageIsDescendantOf(Map<String, Image> imageIdToImage, String img, String ancestor) {
        while (imageIdToImage.containsKey(img)) {
            img = imageIdToImage.get(img).getParentId();
            if (img == null) return false;
            if (ancestor.equals(img)) return true;
        }
        return false;
    }
}