1   /*
2    * Copyright 2024 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.management;
18  
19  import static java.util.Objects.requireNonNull;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.Properties;
26  import java.util.concurrent.Executor;
27  import java.util.concurrent.Executors;
28  
29  import com.linecorp.armeria.common.util.ThreadFactories;
30  
31  /**
32   * Manages the server status.
33   */
34  public final class ServerStatusManager {
35  
36      private static final String STATUS = "status";
37  
38      private final Executor sequentialExecutor =
39              Executors.newSingleThreadExecutor(
40                      ThreadFactories.newThreadFactory("server-status-manager", true));
41  
42      private final Path serverStatusFile;
43  
44      /**
45       * Creates a new instance with the specified {@code dataDir}.
46       */
47      public ServerStatusManager(File dataDir) {
48          requireNonNull(dataDir, "dataDir");
49          final File serverStatusFile = new File(dataDir, "server-status.properties");
50          // Create the file if it does not exist.
51          try {
52              serverStatusFile.createNewFile();
53          } catch (IOException e) {
54              throw new IllegalStateException("Failed to create server status file: " + serverStatusFile, e);
55          }
56          this.serverStatusFile = serverStatusFile.toPath();
57      }
58  
59      /**
60       * Reads the {@link ServerStatus} from the {@code "<data-dir>/server-status.properties"} file.
61       *
62       * <p>The stored {@link ServerStatus} may be used to determine whether the server is writable and
63       * replicating when the server is started.
64       */
65      public ServerStatus serverStatus() {
66          final Properties properties = new Properties();
67          synchronized (serverStatusFile) {
68              try {
69                  properties.load(Files.newInputStream(serverStatusFile));
70              } catch (IOException e) {
71                  throw new IllegalStateException("Failed to load server status file: " + serverStatusFile, e);
72              }
73          }
74  
75          final String status = properties.getProperty(STATUS, "WRITABLE");
76          return ServerStatus.valueOf(status);
77      }
78  
79      /**
80       * Updates the server status with the specified {@code writable} and {@code replicating} values.
81       *
82       * <p>The status may be stored in the {@code "<data-dir>/server-status.properties"} file so that the server
83       * can be initialized with the same status when it is restarted.
84       */
85      public void updateStatus(ServerStatus newServerStatus) {
86          synchronized (serverStatusFile) {
87              final Properties properties = new Properties();
88              try {
89                  properties.load(Files.newInputStream(serverStatusFile));
90              } catch (IOException e) {
91                  throw new IllegalStateException("Failed to load server status file: " + serverStatusFile,
92                                                  e);
93              }
94              properties.setProperty(STATUS, newServerStatus.name());
95              try {
96                  properties.store(Files.newOutputStream(serverStatusFile),
97                                   "Do not edit this file manually. Use the AdministrativeService API.");
98              } catch (IOException e) {
99                  throw new IllegalStateException("Failed to store server status file: " + serverStatusFile,
100                                                 e);
101             }
102         }
103     }
104 
105     /**
106      * Returns the {@link Executor} which is used to execute the status update sequentially.
107      */
108     public Executor sequentialExecutor() {
109         return sequentialExecutor;
110     }
111 }