aboutsummaryrefslogtreecommitdiffstats
path: root/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/resources/ApplicationSuspensionRequestHandler.java
blob: f3ae5746fbcec27c64134ec3adc2f569e89f22d1 (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.orchestrator.resources;

import com.yahoo.component.annotation.Inject;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.container.jdisc.EmptyResponse;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.jdisc.http.HttpResponse.Status;
import com.yahoo.restapi.RestApi;
import com.yahoo.restapi.RestApiException;
import com.yahoo.restapi.RestApiRequestHandler;
import com.yahoo.vespa.orchestrator.ApplicationIdNotFoundException;
import com.yahoo.vespa.orchestrator.ApplicationStateChangeDeniedException;
import com.yahoo.vespa.orchestrator.Orchestrator;
import com.yahoo.vespa.orchestrator.status.ApplicationInstanceStatus;

import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * @author smorgrav
 * @author bjorncs
 */
public class ApplicationSuspensionRequestHandler extends RestApiRequestHandler<ApplicationSuspensionRequestHandler> {

    private static final Logger log = Logger.getLogger(ApplicationSuspensionRequestHandler.class.getName());

    private final Orchestrator orchestrator;

    @Inject
    public ApplicationSuspensionRequestHandler(ThreadedHttpRequestHandler.Context context, Orchestrator orchestrator) {
        super(context, ApplicationSuspensionRequestHandler::createRestApiDefinition);
        this.orchestrator = orchestrator;
    }

    private static RestApi createRestApiDefinition(ApplicationSuspensionRequestHandler self) {
        return RestApi.builder()
                .addRoute(RestApi.route("/orchestrator/v1/suspensions/applications")
                        .get(self::getApplications)
                        .post(String.class, self::suspend))
                .addRoute(RestApi.route("/orchestrator/v1/suspensions/applications/{application}")
                        .get(self::getApplication)
                        .delete(self::resume))
                .registerJacksonResponseEntity(Set.class)
                .build();
    }

    /**
     * Lists all applications that are currently suspended.
     *
     * HTTP Behavior:
     * Always 200
     *
     * @return A list of application ids of suspended applications
     */
    private Set<String> getApplications(RestApi.RequestContext context) {
        Set<ApplicationId> refs = orchestrator.getAllSuspendedApplications();
        return refs.stream().map(ApplicationId::serializedForm).collect(Collectors.toSet());
    }

    /**
     * Shows the Orchestrator status for an application instance
     *
     * HTTP Behavior:
     * 204 if the application is suspended
     * 400 if the applicationId is invalid
     * 404 if the application is not suspended
     */
    private HttpResponse getApplication(RestApi.RequestContext context) {
        String applicationIdString = context.pathParameters().getStringOrThrow("application");
        ApplicationId appId = toApplicationId(applicationIdString);
        ApplicationInstanceStatus status;

        try {
            status = orchestrator.getApplicationInstanceStatus(appId);
        } catch (ApplicationIdNotFoundException e) {
            throw new RestApiException.NotFound("Application " + applicationIdString + " could not be found", e);
        }

        if (status.equals(ApplicationInstanceStatus.NO_REMARKS)) {
            throw new RestApiException.NotFound("Application " + applicationIdString + " is not suspended");
        }
        return new EmptyResponse(Status.NO_CONTENT);
    }

    /**
     * Ask for permission to temporarily suspend all services for an application instance.
     *
     * On success all content nodes for this application instance have been set in maintenance mode.
     *
     * Once the application is ready to resume normal operations, it must finish with resume() (see below).
     *
     * If the application has already been granted permission to suspend all services, requesting
     * suspension again is idempotent and will succeed.
     *
     * HTTP Behavior:
     * 204 is the suspend operation was successful
     * 400 if the applicationId is invalid
     * 409 if the suspend was denied
     */
    private HttpResponse suspend(RestApi.RequestContext context, String applicationIdString) {
        ApplicationId applicationId = toApplicationId(applicationIdString);
        try {
            orchestrator.suspend(applicationId);
        } catch (ApplicationIdNotFoundException e) {
            log.log(Level.INFO, "ApplicationId " + applicationIdString + " not found.", e);
            throw new RestApiException.NotFound(e);
        } catch (ApplicationStateChangeDeniedException e) {
            log.log(Level.INFO, "Suspend for " + applicationIdString + " failed.", e);
            throw new RestApiException.Conflict();
        } catch (RuntimeException e) {
            log.log(Level.INFO, "Suspend for " + applicationIdString + " failed from unknown reasons", e);
            throw new RestApiException.InternalServerError(e);
        }
        return new EmptyResponse(Status.NO_CONTENT);
    }

    /**
     * Resume normal operations for all services for an application
     * instance that has previously been allowed suspension.
     *
     * If the host is already registered as running normal operations, then resume() is idempotent
     * and will succeed.
     *
     * HTTP Behavior:
     * Returns 204 is the resume operation was successful (or the application was not suspended)
     * Returns 400 if the applicationId is invalid
     */
    private HttpResponse resume(RestApi.RequestContext context) {
        String applicationIdString = context.pathParameters().getStringOrThrow("application");
        ApplicationId applicationId = toApplicationId(applicationIdString);
        try {
            orchestrator.resume(applicationId);
        } catch (ApplicationIdNotFoundException e) {
            log.log(Level.INFO, "ApplicationId " + applicationIdString + " not found.", e);
            throw new RestApiException.NotFound(e);
        } catch (ApplicationStateChangeDeniedException e) {
            log.log(Level.INFO, "Suspend for " + applicationIdString + " failed.", e);
            throw new RestApiException.Conflict();
        } catch (RuntimeException e) {
            log.log(Level.INFO, "Suspend for " + applicationIdString + " failed from unknown reasons", e);
            throw new RestApiException.InternalServerError(e);
        }
        return new EmptyResponse(Status.NO_CONTENT);
    }

    private ApplicationId toApplicationId(String applicationIdString) {
        try {
            return ApplicationId.fromSerializedForm(applicationIdString);
        } catch (IllegalArgumentException e) {
            throw new RestApiException.BadRequest(e);
        }
    }

}