Skip to content

Commit 6a5e241

Browse files
authored
Detect and delete stuck pods (#2029)
1 parent ce1383c commit 6a5e241

File tree

12 files changed

+391
-10
lines changed

12 files changed

+391
-10
lines changed

operator/src/main/java/oracle/kubernetes/operator/Main.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public class Main {
109109
private static String principal;
110110
private static KubernetesVersion version = null;
111111
private static SemanticVersion productVersion = null;
112+
private static final StuckPodProcessing stuckPodProcessing
113+
= new StuckPodProcessing(operatorNamespace, Main::readExistingResources);
112114

113115
static {
114116
try {
@@ -222,10 +224,15 @@ private static void completeBegin() {
222224

223225
// start periodic retry and recheck
224226
int recheckInterval = tuningAndConfig.getMainTuning().targetNamespaceRecheckIntervalSeconds;
227+
int stuckPodInterval = tuningAndConfig.getMainTuning().stuckPodRecheckSeconds;
225228
engine
226229
.getExecutor()
227230
.scheduleWithFixedDelay(
228231
recheckDomains(), recheckInterval, recheckInterval, TimeUnit.SECONDS);
232+
engine
233+
.getExecutor()
234+
.scheduleWithFixedDelay(
235+
checkStuckPods(), stuckPodInterval, stuckPodInterval, TimeUnit.SECONDS);
229236

230237
// Wait until all other initialization is done before marking ready and
231238
// starting liveness thread
@@ -336,6 +343,14 @@ static Runnable recheckDomains() {
336343
};
337344
}
338345

346+
static Runnable checkStuckPods() {
347+
return () -> getTargetNamespaces().stream().map(Main::checkStuckPodsIn).forEach(Main::runSteps);
348+
}
349+
350+
private static Step checkStuckPodsIn(String namespace) {
351+
return stuckPodProcessing.createStuckPodCheckSteps(namespace);
352+
}
353+
339354
static Step readExistingResources(String operatorNamespace, String ns) {
340355
return Step.chain(
341356
new ReadExistingResourcesBeforeStep(),
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2020, Oracle Corporation and/or its affiliates.
2+
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
3+
4+
package oracle.kubernetes.operator;
5+
6+
import java.util.ArrayList;
7+
import java.util.Collection;
8+
import java.util.List;
9+
import java.util.Objects;
10+
import java.util.Optional;
11+
import java.util.function.BiFunction;
12+
13+
import io.kubernetes.client.openapi.models.V1ObjectMeta;
14+
import io.kubernetes.client.openapi.models.V1Pod;
15+
import io.kubernetes.client.openapi.models.V1PodList;
16+
import io.kubernetes.client.openapi.models.V1Status;
17+
import oracle.kubernetes.operator.calls.CallResponse;
18+
import oracle.kubernetes.operator.helpers.CallBuilder;
19+
import oracle.kubernetes.operator.helpers.PodHelper;
20+
import oracle.kubernetes.operator.logging.LoggingFacade;
21+
import oracle.kubernetes.operator.logging.LoggingFactory;
22+
import oracle.kubernetes.operator.steps.DefaultResponseStep;
23+
import oracle.kubernetes.operator.work.NextAction;
24+
import oracle.kubernetes.operator.work.Packet;
25+
import oracle.kubernetes.operator.work.Step;
26+
import oracle.kubernetes.utils.SystemClock;
27+
import org.joda.time.DateTime;
28+
29+
import static oracle.kubernetes.operator.logging.MessageKeys.POD_FORCE_DELETED;
30+
31+
/**
32+
* Under certain circumstances, when a Kubernetes node goes down, it may mark its pods as terminating, but never
33+
* actually remove them. This code detects such cases, deletes the pods and triggers the necessary make-right flows.
34+
*/
35+
public class StuckPodProcessing {
36+
private static final LoggingFacade LOGGER = LoggingFactory.getLogger("Operator", "Operator");
37+
38+
private final String operatorNamespace;
39+
private final BiFunction<String,String,Step> readExistingNamespaces;
40+
41+
public StuckPodProcessing(String operatorNamespace, BiFunction<String, String, Step> readExistingNamespaces) {
42+
this.operatorNamespace = operatorNamespace;
43+
this.readExistingNamespaces = readExistingNamespaces;
44+
}
45+
46+
Step createStuckPodCheckSteps(String namespace) {
47+
return new CallBuilder()
48+
.withLabelSelectors(LabelConstants.getCreatedbyOperatorSelector())
49+
.listPodAsync(namespace, new PodListProcessing(namespace, SystemClock.now()));
50+
}
51+
52+
@SuppressWarnings("unchecked")
53+
private List<V1Pod> getStuckPodList(Packet packet) {
54+
return (List<V1Pod>) packet.computeIfAbsent("STUCK_PODS", k -> new ArrayList<>());
55+
}
56+
57+
class PodListProcessing extends DefaultResponseStep<V1PodList> {
58+
59+
private final DateTime now;
60+
61+
public PodListProcessing(String namespace, DateTime dateTime) {
62+
super(new PodActionsStep(namespace));
63+
now = dateTime;
64+
}
65+
66+
@Override
67+
public NextAction onSuccess(Packet packet, CallResponse<V1PodList> callResponse) {
68+
callResponse.getResult().getItems().stream()
69+
.filter(pod -> isStuck(pod, now))
70+
.forEach(pod -> addStuckPodToPacket(packet, pod));
71+
72+
return doNext(packet);
73+
}
74+
75+
private boolean isStuck(V1Pod pod, DateTime now) {
76+
return getExpectedDeleteTime(pod).isBefore(now);
77+
}
78+
79+
private DateTime getExpectedDeleteTime(V1Pod pod) {
80+
return getDeletionTimeStamp(pod).plusSeconds((int) getDeletionGracePeriodSeconds(pod));
81+
}
82+
83+
private long getDeletionGracePeriodSeconds(V1Pod pod) {
84+
return Optional.of(pod).map(V1Pod::getMetadata).map(V1ObjectMeta::getDeletionGracePeriodSeconds).orElse(1L);
85+
}
86+
87+
private DateTime getDeletionTimeStamp(V1Pod pod) {
88+
return Optional.of(pod).map(V1Pod::getMetadata).map(V1ObjectMeta::getDeletionTimestamp).orElse(SystemClock.now());
89+
}
90+
91+
private void addStuckPodToPacket(Packet packet, V1Pod stuckPod) {
92+
getStuckPodList(packet).add(stuckPod);
93+
}
94+
}
95+
96+
class PodActionsStep extends Step {
97+
98+
private final String namespace;
99+
100+
public PodActionsStep(String namespace) {
101+
this.namespace = namespace;
102+
}
103+
104+
@Override
105+
public NextAction apply(Packet packet) {
106+
final List<V1Pod> stuckPodList = getStuckPodList(packet);
107+
if (stuckPodList.isEmpty()) {
108+
return doNext(packet);
109+
} else {
110+
Collection<StepAndPacket> startDetails = new ArrayList<>();
111+
112+
for (V1Pod pod : stuckPodList) {
113+
startDetails.add(new StepAndPacket(createForcedDeletePodStep(pod), packet.clone()));
114+
}
115+
return doForkJoin(readExistingNamespaces.apply(operatorNamespace, namespace), packet, startDetails);
116+
}
117+
}
118+
119+
private Step createForcedDeletePodStep(V1Pod pod) {
120+
return new CallBuilder()
121+
.withGracePeriodSeconds(0)
122+
.deletePodAsync(getName(pod), getNamespace(pod), null,
123+
new ForcedDeleteResponseStep(getName(pod), getNamespace(pod)));
124+
}
125+
126+
private String getName(V1Pod pod) {
127+
return Objects.requireNonNull(pod.getMetadata()).getName();
128+
}
129+
130+
private String getNamespace(V1Pod pod) {
131+
return Objects.requireNonNull(pod.getMetadata()).getNamespace();
132+
}
133+
134+
private String getDomainUid(V1Pod pod) {
135+
return PodHelper.getPodDomainUid(pod);
136+
}
137+
}
138+
139+
static class ForcedDeleteResponseStep extends DefaultResponseStep<V1Status> {
140+
141+
private final String name;
142+
private final String namespace;
143+
144+
public ForcedDeleteResponseStep(String name, String namespace) {
145+
this.name = name;
146+
this.namespace = namespace;
147+
}
148+
149+
@Override
150+
public NextAction onSuccess(Packet packet, CallResponse<V1Status> callResponse) {
151+
LOGGER.info(POD_FORCE_DELETED, name, namespace);
152+
return super.onSuccess(packet, callResponse);
153+
}
154+
}
155+
156+
}

operator/src/main/java/oracle/kubernetes/operator/TuningParameters.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public static class MainTuning {
3737
public final int targetNamespaceRecheckIntervalSeconds;
3838
public final int statusUpdateTimeoutSeconds;
3939
public final int unchangedCountToDelayStatusRecheck;
40+
public final int stuckPodRecheckSeconds;
4041
public final long initialShortDelay;
4142
public final long eventualLongDelay;
4243

@@ -48,6 +49,7 @@ public static class MainTuning {
4849
* @param targetNamespaceRecheckIntervalSeconds target namespace recheck interval
4950
* @param statusUpdateTimeoutSeconds status update timeout
5051
* @param unchangedCountToDelayStatusRecheck unchanged count to delay status recheck
52+
* @param stuckPodRecheckSeconds time between checks for stuck pods
5153
* @param initialShortDelay initial short delay
5254
* @param eventualLongDelay eventual long delay
5355
*/
@@ -58,6 +60,7 @@ public MainTuning(
5860
int targetNamespaceRecheckIntervalSeconds,
5961
int statusUpdateTimeoutSeconds,
6062
int unchangedCountToDelayStatusRecheck,
63+
int stuckPodRecheckSeconds,
6164
long initialShortDelay,
6265
long eventualLongDelay) {
6366
this.domainPresenceFailureRetrySeconds = domainPresenceFailureRetrySeconds;
@@ -66,6 +69,7 @@ public MainTuning(
6669
this.targetNamespaceRecheckIntervalSeconds = targetNamespaceRecheckIntervalSeconds;
6770
this.statusUpdateTimeoutSeconds = statusUpdateTimeoutSeconds;
6871
this.unchangedCountToDelayStatusRecheck = unchangedCountToDelayStatusRecheck;
72+
this.stuckPodRecheckSeconds = stuckPodRecheckSeconds;
6973
this.initialShortDelay = initialShortDelay;
7074
this.eventualLongDelay = eventualLongDelay;
7175
}

operator/src/main/java/oracle/kubernetes/operator/TuningParametersImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ private void update() {
5454
(int) readTuningParameter("targetNamespaceRecheckIntervalSeconds", 3),
5555
(int) readTuningParameter("statusUpdateTimeoutSeconds", 10),
5656
(int) readTuningParameter("statusUpdateUnchangedCountToDelayStatusRecheck", 10),
57+
(int) readTuningParameter("stuckPodRecheckSeconds", 30),
5758
readTuningParameter("statusUpdateInitialShortDelay", 5),
5859
readTuningParameter("statusUpdateEventualLongDelay", 30));
5960

operator/src/main/java/oracle/kubernetes/operator/helpers/CallBuilder.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ public <T> T execute(
289289
private final CallFactory<V1Secret> readSecret =
290290
(requestParams, usage, cont, callback) ->
291291
wrap(readSecretAsync(usage, requestParams.name, requestParams.namespace, callback));
292-
private final Integer gracePeriodSeconds = null;
292+
private Integer gracePeriodSeconds = null;
293293
private final Boolean orphanDependents = null;
294294
private final String propagationPolicy = null;
295295

@@ -499,6 +499,11 @@ public CallBuilder withFieldSelector(String fieldSelector) {
499499
return this;
500500
}
501501

502+
public CallBuilder withGracePeriodSeconds(int gracePeriodSeconds) {
503+
this.gracePeriodSeconds = gracePeriodSeconds;
504+
return this;
505+
}
506+
502507
private void tuning(int limit, int timeoutSeconds, int maxRetryCount) {
503508
this.limit = limit;
504509
this.timeoutSeconds = timeoutSeconds;

operator/src/main/java/oracle/kubernetes/operator/logging/MessageKeys.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public class MessageKeys {
133133
public static final String INTROSPECTOR_JOB_FAILED = "WLSKO-0175";
134134
public static final String INTROSPECTOR_JOB_FAILED_DETAIL = "WLSKO-0176";
135135
public static final String INTROSPECTOR_POD_FAILED = "WLSKO-0177";
136+
public static final String POD_FORCE_DELETED = "WLSKO-0179";
136137

137138
// domain status messages
138139
public static final String DUPLICATE_SERVER_NAME_FOUND = "WLSDO-0001";

operator/src/main/java/oracle/kubernetes/utils/SystemClock.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
/** A wrapper for the system clock that facilitates unit testing of time. */
99
public abstract class SystemClock {
1010

11-
private static SystemClock DELEGATE =
12-
new SystemClock() {
11+
// Leave as non-final; unit tests may replace this value
12+
@SuppressWarnings("FieldMayBeFinal")
13+
private static SystemClock DELEGATE = new SystemClock() {
1314
@Override
1415
public DateTime getCurrentTime() {
1516
return DateTime.now();

operator/src/main/resources/Operator.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ WLSKO-0175=Job {0} in namespace {1} failed with status {2}. Check log messages \
187187
copied from the introspector pod {3} log for additional information.
188188
WLSKO-0176=Job {1} in namespace {0} failed, job details are {2}
189189
WLSKO-0177=Pod {0} in namespace {1} failed, the pod status is {2}
190+
WLSKO-0179=Pod {0} in namespace {1} detected as stuck, and force-deleted
190191

191192
# Domain status messages
192193

operator/src/test/java/oracle/kubernetes/operator/NamespaceTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ private Map<String, AtomicBoolean> createNamespaceFlags() {
201201
.collect(Collectors.toMap(identity(), a -> new AtomicBoolean()));
202202
}
203203

204-
@SuppressWarnings("unchecked")
205204
private void invoke_stopNamespace(String namespace, boolean inTargetNamespaceList)
206205
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
207206
if (stopNamespace == null) {
@@ -227,7 +226,7 @@ public static Memento install(int newValue) throws NoSuchFieldException {
227226

228227
@Override
229228
public MainTuning getMainTuning() {
230-
return new MainTuning(2, 2, domainPresenceRecheckIntervalSeconds, 2, 2, 2, 2L, 2L);
229+
return new MainTuning(2, 2, domainPresenceRecheckIntervalSeconds, 2, 2, 2, 30, 2L, 2L);
231230
}
232231
}
233232

0 commit comments

Comments
 (0)