7a0a05e1053861dc7421075754595113d0687f75
AGENTS.md
| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | +We want all local variables, fields, and parameters to be declared "final" as far as possible. |
|
| 2 | + |
|
| 3 | +We want all literals used as parameters (true, false, numeric literals, Optional.empty() and null) to be prefixed with a comment telling the parameter name. |
|
| 4 | + |
|
| 5 | +We want a single exit point in any method only, so no multiple "return" statements. |
|
| 6 | + |
|
| 7 | +Avoid unnecessary empty lines. If you feel like you need to separate code within a single method into "sections", consider using double-slash comments to explain what the next section is. |
configuration/buildAndUpdateProduct.sh
| ... | ... | @@ -671,7 +671,7 @@ if [[ "$@" == "build" ]] || [[ "$@" == "all" ]]; then |
| 671 | 671 | PATH=$PATH:$ANDROID_HOME/platform-tools |
| 672 | 672 | SDK_MANAGER="$ANDROID_HOME/cmdline-tools/8.0/bin/sdkmanager" |
| 673 | 673 | if [ \! -x "$SDK_MANAGER" ]; then |
| 674 | - SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager.bat" |
|
| 674 | + SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager" |
|
| 675 | 675 | fi |
| 676 | 676 | echo "SDK_MANAGER=${SDK_MANAGER}" |
| 677 | 677 | echo "cmdline-tools:" |
configuration/github-download-workflow-artifacts.sh
| ... | ... | @@ -1,8 +1,9 @@ |
| 1 | 1 | #!/bin/bash |
| 2 | -# Call with two arguments: |
|
| 2 | +# Call with three arguments: |
|
| 3 | 3 | # - the name of the branch for which you would like |
| 4 | 4 | # to download the latest workflow run's build artifacts |
| 5 | 5 | # - the Github PAT (personal access token) |
| 6 | +# - the Github repository {owner}/{repo}, such as SAP/sailing-analytics |
|
| 6 | 7 | # If found, the build.log.zip and test-result.zip files |
| 7 | 8 | # will be downloaded to the current working directory. |
| 8 | 9 | # The script will exit with status 0 if the workflow's |
| ... | ... | @@ -21,21 +22,64 @@ HEADERS_FILE=$( mktemp headersXXXXX ) |
| 21 | 22 | NEXT_PAGE="https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/runs?created=${DATE_YESTERDAY/+/%2B}..${UNIX_DATE/+/%2B}&per_page=100" |
| 22 | 23 | ARTIFACTS_JSON="" |
| 23 | 24 | LATEST_RUN_STARTED_AT="0000-00-00T00:00:00Z" |
| 25 | + |
|
| 26 | +# Function to check a workflow run and all its previous attempts |
|
| 27 | +# Arguments: $1 = workflow run JSON |
|
| 28 | +# Sets LAST_WORKFLOW_FOR_BRANCH and LATEST_RUN_STARTED_AT if a better match is found |
|
| 29 | +check_run_and_previous_attempts() { |
|
| 30 | + local RUN_JSON="${1}" |
|
| 31 | + local CURRENT_RUN="${RUN_JSON}" |
|
| 32 | + |
|
| 33 | + while [ -n "${CURRENT_RUN}" ] && [ "${CURRENT_RUN}" != "null" ]; do |
|
| 34 | + local RUN_STATUS=$( echo "${CURRENT_RUN}" | jq -r '.status' ) |
|
| 35 | + local RUN_NAME=$( echo "${CURRENT_RUN}" | jq -r '.name' ) |
|
| 36 | + local RUN_HEAD_BRANCH=$( echo "${CURRENT_RUN}" | jq -r '.head_branch' ) |
|
| 37 | + |
|
| 38 | + # Check if this run matches our criteria |
|
| 39 | + if [ "${RUN_STATUS}" == "completed" ] && [ "${RUN_NAME}" == "release" ]; then |
|
| 40 | + if [[ "${RUN_HEAD_BRANCH}" == ${BRANCH}* ]] || [[ "${RUN_HEAD_BRANCH}" == releases/${BRANCH}* ]]; then |
|
| 41 | + local RUN_STARTED_AT=$( echo "${CURRENT_RUN}" | jq -r '.run_started_at' ) |
|
| 42 | + echo "Found completed run (or previous attempt) started at ${RUN_STARTED_AT}" |
|
| 43 | + if [[ "${RUN_STARTED_AT}" > "${LATEST_RUN_STARTED_AT}" ]]; then |
|
| 44 | + echo "This is later than the latest run so far (${LATEST_RUN_STARTED_AT})" |
|
| 45 | + LAST_WORKFLOW_FOR_BRANCH="${CURRENT_RUN}" |
|
| 46 | + LATEST_RUN_STARTED_AT="${RUN_STARTED_AT}" |
|
| 47 | + fi |
|
| 48 | + fi |
|
| 49 | + fi |
|
| 50 | + # Check for previous attempt |
|
| 51 | + local PREVIOUS_ATTEMPT_URL=$( echo "${CURRENT_RUN}" | jq -r '.previous_attempt_url // empty' ) |
|
| 52 | + if [ -n "${PREVIOUS_ATTEMPT_URL}" ]; then |
|
| 53 | + echo "Checking previous attempt at ${PREVIOUS_ATTEMPT_URL} ..." |
|
| 54 | + CURRENT_RUN=$( curl --silent -L -H 'Authorization: Bearer '${BEARER_TOKEN} "${PREVIOUS_ATTEMPT_URL}" 2>/dev/null ) |
|
| 55 | + # Validate we got a proper response |
|
| 56 | + if [ -z "${CURRENT_RUN}" ] || [ "$( echo "${CURRENT_RUN}" | jq -r '.id // empty' )" == "" ]; then |
|
| 57 | + echo "Could not fetch previous attempt, stopping chain" |
|
| 58 | + break |
|
| 59 | + fi |
|
| 60 | + else |
|
| 61 | + break |
|
| 62 | + fi |
|
| 63 | + done |
|
| 64 | +} |
|
| 65 | + |
|
| 24 | 66 | # Now go through the pages as long as we have a non-empty NEXT_PAGE URL and find the completed "release" workflow that was started last |
| 25 | 67 | while [ -n "${NEXT_PAGE}" ]; do |
| 26 | 68 | echo "Trying page ${NEXT_PAGE} ..." |
| 27 | 69 | # Get the artifacts URL of the last workflow run triggered by a branch push for ${BRANCH}: |
| 28 | - LAST_WORKFLOW_FOR_BRANCH_ON_PAGE=$( curl -D "${HEADERS_FILE}" --silent -L -H 'Authorization: Bearer '${BEARER_TOKEN} "${NEXT_PAGE}" 2>/dev/null | jq -r '.workflow_runs | map(select(.status == "completed" and .name == "release" and ((.head_branch | startswith("'${BRANCH}'")) or (.head_branch | startswith("releases/'${BRANCH}'"))))) | sort_by(.updated_at) | reverse | .[0]' | sed -e '/^$/d' ) |
|
| 29 | - if [ -n "${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" -a "${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" != "null" ]; then |
|
| 30 | - RUN_STARTED_AT=$( echo "${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" | jq -r '.run_started_at' ) |
|
| 31 | - echo "Found completed run started at ${RUN_STARTED_AT}" |
|
| 32 | - if [[ "${RUN_STARTED_AT}" > "${LATEST_RUN_STARTED_AT}" ]]; then |
|
| 33 | - echo "This is later than the latest run so far (${LATEST_RUN_STARTED_AT})" |
|
| 34 | - LAST_WORKFLOW_FOR_BRANCH="${LAST_WORKFLOW_FOR_BRANCH_ON_PAGE}" |
|
| 35 | - LATEST_RUN_STARTED_AT="${RUN_STARTED_AT}" |
|
| 36 | - fi |
|
| 70 | + NEXT_PAGE_CONTENTS=$( curl -D "${HEADERS_FILE}" --silent -L -H 'Authorization: Bearer '${BEARER_TOKEN} "${NEXT_PAGE}" 2>/dev/null ) |
|
| 71 | + # Get all workflow runs that match our branch criteria (completed or not, we'll check status in the function) |
|
| 72 | + MATCHING_RUNS=$( echo "${NEXT_PAGE_CONTENTS}" | jq -c '.workflow_runs | map(select(.name == "release" and ((.head_branch | startswith("'${BRANCH}'")) or (.head_branch | startswith("releases/'${BRANCH}'"))))) | .[]' 2>/dev/null ) |
|
| 73 | + if [ -n "${MATCHING_RUNS}" ]; then |
|
| 74 | + # Process each matching run |
|
| 75 | + while IFS= read -r RUN_JSON; do |
|
| 76 | + if [ -n "${RUN_JSON}" ] && [ "${RUN_JSON}" != "null" ]; then |
|
| 77 | + echo "Checking run and its previous attempts..." |
|
| 78 | + check_run_and_previous_attempts "${RUN_JSON}" |
|
| 79 | + fi |
|
| 80 | + done <<< "${MATCHING_RUNS}" |
|
| 37 | 81 | else |
| 38 | - echo "Found no completed run for branch ${BRANCH} on page" |
|
| 82 | + echo "Found no runs for branch ${BRANCH} on page" |
|
| 39 | 83 | fi |
| 40 | 84 | NEXT_PAGE=$( grep "^link: .*; rel=\"next\"" "${HEADERS_FILE}" | sed -e 's/^.*<\([^>]*\)>; rel="next".*$/\1/' ) |
| 41 | 85 | done |
java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/BoatClass.java
| ... | ... | @@ -8,7 +8,7 @@ import com.sap.sse.common.Named; |
| 8 | 8 | import com.sap.sse.common.TimePoint; |
| 9 | 9 | |
| 10 | 10 | public interface BoatClass extends Named, IsManagedByCache<SharedDomainFactory<?>> { |
| 11 | - final Duration APPROXIMATE_AVERAGE_MANEUVER_DURATION = Duration.ONE_SECOND.times(8); // as discussed with Dennis Gehrlein |
|
| 11 | + final Duration APPROXIMATE_AVERAGE_MANEUVER_DURATION = Duration.ONE_SECOND.times(10); |
|
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | 14 | * The distance returned by this method should be appropriate for use in |
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationTest.java
| ... | ... | @@ -69,15 +69,22 @@ public class CourseChangeBasedTrackApproximationTest { |
| 69 | 69 | @Test |
| 70 | 70 | public void testDirectionChangeJustAboveThreshold() { |
| 71 | 71 | final Duration samplingInterval = Duration.ONE_SECOND; |
| 72 | - final double aBitOverMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() * 1.2; |
|
| 72 | + final double aBitOverMinimumManeuverAngleDegrees = boatClass.getManeuverDegreeAngleThreshold() * 1.5; |
|
| 73 | 73 | final TimePoint start = TimePoint.of(10000l); |
| 74 | 74 | final Speed speed = new KnotSpeedImpl(5.0); |
| 75 | 75 | GPSFixMoving next = fix(start.asMillis(), 0, 0, speed.getKnots(), 0); |
| 76 | 76 | track.add(next); |
| 77 | 77 | // perform aBitOverMinimumManeuverAngleDegrees within five fixes: |
| 78 | 78 | final int NUMBER_OF_FIXES_FOR_MANEUVER = 5; |
| 79 | + double cog = 0.0; |
|
| 79 | 80 | for (int i=0; i<NUMBER_OF_FIXES_FOR_MANEUVER; i++) { |
| 80 | - next = travel(next, samplingInterval.asMillis(), speed.getKnots(), ((double) i+1.0)/((double) NUMBER_OF_FIXES_FOR_MANEUVER) * aBitOverMinimumManeuverAngleDegrees); |
|
| 81 | + cog = ((double) i+1.0)/((double) NUMBER_OF_FIXES_FOR_MANEUVER) * aBitOverMinimumManeuverAngleDegrees; |
|
| 82 | + next = travel(next, samplingInterval.asMillis(), speed.getKnots(), cog); |
|
| 83 | + track.add(next); |
|
| 84 | + } |
|
| 85 | + // now go straight for the maneuver duration to ensure that the approximation has read buffered fixes up to and including the end of the maneuver: |
|
| 86 | + for (int i=0; i<boatClass.getApproximateManeuverDurationInMilliseconds()/1000; i++) { |
|
| 87 | + next = travel(next, Duration.ONE_SECOND.asMillis(), speed.getKnots(), cog); |
|
| 81 | 88 | track.add(next); |
| 82 | 89 | } |
| 83 | 90 | final Iterable<GPSFixMoving> oneManeuverCandidate = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_MANEUVER))); |
| ... | ... | @@ -117,6 +124,11 @@ public class CourseChangeBasedTrackApproximationTest { |
| 117 | 124 | next = travel(next, samplingInterval.asMillis(), speed.getKnots(), cog); |
| 118 | 125 | track.add(next); |
| 119 | 126 | } |
| 127 | + // now go straight for the maneuver duration to ensure that the approximation has read buffered fixes up to and including the end of the maneuver: |
|
| 128 | + for (int i=0; i<boatClass.getApproximateManeuverDurationInMilliseconds()/1000; i++) { |
|
| 129 | + next = travel(next, Duration.ONE_SECOND.asMillis(), speed.getKnots(), cog); |
|
| 130 | + track.add(next); |
|
| 131 | + } |
|
| 120 | 132 | final Iterable<GPSFixMoving> oneManeuverCandidate = approximation.approximate(start, start.plus(samplingInterval.times(NUMBER_OF_FIXES_FOR_NON_MANEUVER+NUMBER_OF_FIXES_FOR_MANEUVER))); |
| 121 | 133 | assertFalse(Util.isEmpty(oneManeuverCandidate)); |
| 122 | 134 | assertEquals(1, Util.size(oneManeuverCandidate)); |
java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/CourseChangeBasedTrackApproximationWithTracTracDataTest.java
| ... | ... | @@ -65,7 +65,9 @@ public class CourseChangeBasedTrackApproximationWithTracTracDataTest extends Onl |
| 65 | 65 | */ |
| 66 | 66 | @Test |
| 67 | 67 | public void testNoDiffBetweenEarlyAndLateInitialization() { |
| 68 | - final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>(sampleCompetitor, /* millisecondsOverWhichToAverage */ 15000); |
|
| 68 | + final DynamicGPSFixTrack<Competitor, GPSFixMoving> trackCopy = new DynamicGPSFixMovingTrackImpl<Competitor>( |
|
| 69 | + sampleCompetitor, |
|
| 70 | + /* millisecondsOverWhichToAverage */ boatClass.getApproximateManeuverDurationInMilliseconds()); |
|
| 69 | 71 | final CourseChangeBasedTrackApproximation earlyInitApproximation = new CourseChangeBasedTrackApproximation(trackCopy, sampleCompetitor.getBoat().getBoatClass()); |
| 70 | 72 | final TimePoint from = sampleTrack.getFirstRawFix().getTimePoint(); |
| 71 | 73 | final TimePoint to = sampleTrack.getLastRawFix().getTimePoint(); |
java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverdetection/impl/ManeuverDetectorImpl.java
| ... | ... | @@ -197,7 +197,7 @@ public class ManeuverDetectorImpl extends AbstractManeuverDetectorImpl { |
| 197 | 197 | * Checks whether {@code currentFix} can be grouped together with the previous fixes in order to be regarded as a |
| 198 | 198 | * single maneuver spot. For this, the {@code newCourseChangeDirection must match the direction of provided {@code |
| 199 | 199 | * lastCourseChangeDirection}. Additionally, the distance from {@code previousFix} to {@code currentFix} must be <= |
| 200 | - * 3 hull lengths, or the time difference <= getApproximatedManeuverDuration(). |
|
| 200 | + * 4 hull lengths, or the time difference <= getApproximatedManeuverDuration(). |
|
| 201 | 201 | * |
| 202 | 202 | * @param lastCourseChangeDirection The last course within previous three fixes counting from {@code currentFix} |
| 203 | 203 | * |
| ... | ... | @@ -212,17 +212,20 @@ public class ManeuverDetectorImpl extends AbstractManeuverDetectorImpl { |
| 212 | 212 | */ |
| 213 | 213 | protected boolean checkDouglasPeuckerFixesGroupable(NauticalSide lastCourseChangeDirection, |
| 214 | 214 | NauticalSide newCourseChangeDirection, GPSFixMoving previousFix, GPSFixMoving currentFix) { |
| 215 | + final boolean result; |
|
| 215 | 216 | if (lastCourseChangeDirection != newCourseChangeDirection) { |
| 216 | - return false; |
|
| 217 | - } |
|
| 218 | - Distance threeHullLengths = trackedRace.getRace().getBoatOfCompetitor(competitor).getBoatClass().getHullLength() |
|
| 219 | - .scale(3); |
|
| 220 | - if (currentFix.getTimePoint().asMillis() |
|
| 221 | - - previousFix.getTimePoint().asMillis() > getApproximateManeuverDuration().asMillis() |
|
| 222 | - && currentFix.getPosition().getDistance(previousFix.getPosition()).compareTo(threeHullLengths) > 0) { |
|
| 223 | - return false; |
|
| 217 | + result = false; |
|
| 218 | + } else { |
|
| 219 | + Distance fourHullLengths = trackedRace.getRace().getBoatOfCompetitor(competitor).getBoatClass().getHullLength().scale(4); |
|
| 220 | + if (currentFix.getTimePoint().asMillis() |
|
| 221 | + - previousFix.getTimePoint().asMillis() > getApproximateManeuverDuration().asMillis() |
|
| 222 | + && currentFix.getPosition().getDistance(previousFix.getPosition()).compareTo(fourHullLengths) > 0) { |
|
| 223 | + result = false; |
|
| 224 | + } else { |
|
| 225 | + result = true; |
|
| 226 | + } |
|
| 224 | 227 | } |
| 225 | - return true; |
|
| 228 | + return result; |
|
| 226 | 229 | } |
| 227 | 230 | |
| 228 | 231 | private List<Maneuver> getAllManeuversFromManeuverSpots(List<? extends ManeuverSpot> maneuverSpots) { |
java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/CourseChangeBasedTrackApproximation.java
| ... | ... | @@ -6,6 +6,7 @@ import java.io.Serializable; |
| 6 | 6 | import java.util.ArrayList; |
| 7 | 7 | import java.util.Collections; |
| 8 | 8 | import java.util.Comparator; |
| 9 | +import java.util.Deque; |
|
| 9 | 10 | import java.util.LinkedList; |
| 10 | 11 | import java.util.List; |
| 11 | 12 | import java.util.ListIterator; |
| ... | ... | @@ -105,6 +106,17 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 105 | 106 | private final LinkedList<GPSFixMoving> window; |
| 106 | 107 | |
| 107 | 108 | /** |
| 109 | + * New fixes shall be inserted into the {@link FixWindow} only when it is unlikely that newer fixes will still |
|
| 110 | + * influence the calculation of its {@link GPSFixMoving#getCachedEstimatedSpeed() estimated speed}. This way, |
|
| 111 | + * the differences between seeing and not seeing newer fixes is reduced, and so it isn't so relevant anymore |
|
| 112 | + * whether the approximation is updated incrementally as fixes arrive, or after a race has been fully loaded. |
|
| 113 | + * <p> |
|
| 114 | + * |
|
| 115 | + * See also bug 6209. |
|
| 116 | + */ |
|
| 117 | + private final Deque<GPSFixMoving> queueOfNewFixes; |
|
| 118 | + |
|
| 119 | + /** |
|
| 108 | 120 | * We need to remember the speed / bearing as we saw them when we inserted the fixes into the {@link #window} |
| 109 | 121 | * collection. Based on more fixes getting added to the track, things may change. In particular, fixes that may have |
| 110 | 122 | * had a valid speed when inserted may later have their cached speed/bearing invalidated, and computing it again |
| ... | ... | @@ -125,6 +137,7 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 125 | 137 | |
| 126 | 138 | FixWindow() { |
| 127 | 139 | this.window = new LinkedList<>(); |
| 140 | + this.queueOfNewFixes = new LinkedList<>(); |
|
| 128 | 141 | this.speedForFixesInWindow = new LinkedList<>(); |
| 129 | 142 | this.windowDuration = Duration.NULL; |
| 130 | 143 | // use twice the maneuver duration to also catch slowly-executed gybes |
| ... | ... | @@ -160,6 +173,18 @@ public class CourseChangeBasedTrackApproximation implements Serializable, GPSTra |
| 160 | 173 | * or {@code null} if no maneuver candidate became available |
| 161 | 174 | */ |
| 162 | 175 | GPSFixMoving add(GPSFixMoving next) { |
| 176 | + final GPSFixMoving result; |
|
| 177 | + queueOfNewFixes.add(next); // FIXME bug6209: the queueOfNewFixes needs to remain ordered by fix TimePoint! |
|
| 178 | + final GPSFixMoving first = queueOfNewFixes.getFirst(); |
|
| 179 | + if (first.getTimePoint().until(next.getTimePoint()).asMillis() > track.getMillisecondsOverWhichToAverageSpeed()/2) { |
|
| 180 | + result = addOldEnoughFix(queueOfNewFixes.removeFirst()); |
|
| 181 | + } else { |
|
| 182 | + result = null; |
|
| 183 | + } |
|
| 184 | + return result; |
|
| 185 | + } |
|
| 186 | + |
|
| 187 | + private GPSFixMoving addOldEnoughFix(GPSFixMoving next) { |
|
| 163 | 188 | assert window.isEmpty() || !next.getTimePoint().before(window.peekFirst().getTimePoint()); |
| 164 | 189 | final GPSFixMoving result; |
| 165 | 190 | final SpeedWithBearing nextSpeed = /* TODO this was the original code that can depend on fixes newer than next: */ next.isEstimatedSpeedCached() ? next.getCachedEstimatedSpeed() : track.getEstimatedSpeed(next.getTimePoint()); |
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/StatusServlet.java
| ... | ... | @@ -2,6 +2,9 @@ package com.sap.sailing.gwt.ui.server; |
| 2 | 2 | |
| 3 | 3 | import java.io.IOException; |
| 4 | 4 | import java.io.OutputStreamWriter; |
| 5 | +import java.lang.management.ManagementFactory; |
|
| 6 | +import java.util.concurrent.ScheduledExecutorService; |
|
| 7 | +import java.util.concurrent.ThreadPoolExecutor; |
|
| 5 | 8 | |
| 6 | 9 | import javax.servlet.ServletContext; |
| 7 | 10 | import javax.servlet.ServletException; |
| ... | ... | @@ -20,9 +23,12 @@ import com.mongodb.connection.ClusterDescription; |
| 20 | 23 | import com.mongodb.connection.ServerDescription; |
| 21 | 24 | import com.sap.sailing.server.interfaces.RacingEventService; |
| 22 | 25 | import com.sap.sse.ServerInfo; |
| 26 | +import com.sap.sse.common.Duration; |
|
| 27 | +import com.sap.sse.common.Util; |
|
| 23 | 28 | import com.sap.sse.mongodb.MongoDBService; |
| 24 | 29 | import com.sap.sse.replication.ReplicationService; |
| 25 | 30 | import com.sap.sse.replication.ReplicationStatus; |
| 31 | +import com.sap.sse.util.ThreadPoolUtil; |
|
| 26 | 32 | |
| 27 | 33 | public class StatusServlet extends HttpServlet { |
| 28 | 34 | private static final String WAIT_UNTIL_RACES_LOADED = "waitUntilRacesLoaded"; |
| ... | ... | @@ -58,6 +64,22 @@ public class StatusServlet extends HttpServlet { |
| 58 | 64 | try { |
| 59 | 65 | final JSONObject versionAsJson = ServerInfo.getBuildVersionJson(); |
| 60 | 66 | result.putAll(versionAsJson); |
| 67 | + final ScheduledExecutorService defaultBackgroundTaskThreadPoolExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(); |
|
| 68 | + if (defaultBackgroundTaskThreadPoolExecutor instanceof ThreadPoolExecutor) { |
|
| 69 | + final long queueLengthDefaultBackgroundThreadPoolExecutor = ((ThreadPoolExecutor) defaultBackgroundTaskThreadPoolExecutor).getQueue().size(); |
|
| 70 | + result.put("defaultbackgroundthreadpoolexecutorqueuelength", queueLengthDefaultBackgroundThreadPoolExecutor); |
|
| 71 | + final long nonDelayedQueueLengthDefaultBackgroundThreadPoolExecutor = Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan((ThreadPoolExecutor) defaultBackgroundTaskThreadPoolExecutor, Duration.ONE_SECOND)); |
|
| 72 | + result.put("defaultbackgroundthreadpoolexecutorqueuelengthnondelayed", nonDelayedQueueLengthDefaultBackgroundThreadPoolExecutor); |
|
| 73 | + } |
|
| 74 | + final ScheduledExecutorService defaultForegroundTaskThreadPoolExecutor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor(); |
|
| 75 | + if (defaultForegroundTaskThreadPoolExecutor instanceof ThreadPoolExecutor) { |
|
| 76 | + final long queueLengthDefaultForegroundThreadPoolExecutor = ((ThreadPoolExecutor) defaultForegroundTaskThreadPoolExecutor).getQueue().size(); |
|
| 77 | + result.put("defaultforegroundthreadpoolexecutorqueuelength", queueLengthDefaultForegroundThreadPoolExecutor); |
|
| 78 | + final long nonDelayedQueueLengthDefaultForegroundThreadPoolExecutor = Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan((ThreadPoolExecutor) defaultForegroundTaskThreadPoolExecutor, Duration.ONE_SECOND)); |
|
| 79 | + result.put("defaultforegroundthreadpoolexecutorqueuelengthnondelayed", nonDelayedQueueLengthDefaultForegroundThreadPoolExecutor); |
|
| 80 | + } |
|
| 81 | + final double systemLoadAverageLastMinute = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage(); |
|
| 82 | + result.put("systemloadaveragelastminute", systemLoadAverageLastMinute); |
|
| 61 | 83 | final long numberOfTrackedRacesToRestore = service.getNumberOfTrackedRacesToRestore(); |
| 62 | 84 | result.put("numberofracestorestore", numberOfTrackedRacesToRestore); |
| 63 | 85 | final int numberOfTrackedRacesRestored = service.getNumberOfTrackedRacesRestored(); |
java/com.sap.sailing.landscape.common/src/com/sap/sailing/landscape/common/SharedLandscapeConstants.java
| ... | ... | @@ -125,6 +125,14 @@ public interface SharedLandscapeConstants { |
| 125 | 125 | String SAILING_ANALYTICS_APPLICATION_HOST_TAG = "sailing-analytics-server"; |
| 126 | 126 | |
| 127 | 127 | String ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME = "ARCHIVE"; |
| 128 | + |
|
| 129 | + String ARCHIVE_SERVER_INSTANCE_NAME = "SL Archive"; |
|
| 130 | + |
|
| 131 | + String ARCHIVE_SERVER_NEW_CANDIDATE_INSTANCE_NAME = ARCHIVE_SERVER_INSTANCE_NAME+" (New Candidate)"; |
|
| 132 | + |
|
| 133 | + String ARCHIVE_SERVER_FAILOVER_INSTANCE_NAME = ARCHIVE_SERVER_INSTANCE_NAME+" (Failover)"; |
|
| 134 | + |
|
| 135 | + String ARCHIVE_CANDIDATE_SUBDOMAIN = "archive-candidate"; |
|
| 128 | 136 | |
| 129 | 137 | /** |
| 130 | 138 | * Value of the {@link #SAILING_ANALYTICS_APPLICATION_HOST_TAG} tag |
java/com.sap.sailing.landscape.ui/META-INF/MANIFEST.MF
| ... | ... | @@ -36,6 +36,8 @@ Export-Package: com.google.gwt.user.client.rpc.core.com.sap.sse.landscape.aws.co |
| 36 | 36 | Bundle-ActivationPolicy: lazy |
| 37 | 37 | Import-Package: com.sap.sailing.domain.common, |
| 38 | 38 | com.sap.sailing.server.gateway.interfaces, |
| 39 | + javax.servlet;version="[3.1.0,4.0.0)", |
|
| 40 | + javax.servlet.http;version="[3.1.0,4.0.0)", |
|
| 39 | 41 | org.osgi.framework;version="1.8.0", |
| 40 | 42 | org.osgi.util.tracker;version="1.5.1" |
| 41 | 43 | Bundle-Activator: com.sap.sailing.landscape.ui.impl.Activator |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/AbstractApplicationReplicaSetDialog.java
| ... | ... | @@ -1,15 +1,8 @@ |
| 1 | 1 | package com.sap.sailing.landscape.ui.client; |
| 2 | 2 | |
| 3 | -import java.util.Collections; |
|
| 4 | -import java.util.Comparator; |
|
| 5 | -import java.util.LinkedList; |
|
| 6 | -import java.util.List; |
|
| 7 | - |
|
| 8 | -import com.google.gwt.user.client.ui.MultiWordSuggestOracle; |
|
| 9 | 3 | import com.google.gwt.user.client.ui.SuggestBox; |
| 10 | 4 | import com.google.gwt.user.client.ui.TextBox; |
| 11 | 5 | import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
| 12 | -import com.sap.sse.common.Util; |
|
| 13 | 6 | import com.sap.sse.gwt.client.ErrorReporter; |
| 14 | 7 | import com.sap.sse.gwt.client.dialog.DataEntryDialog; |
| 15 | 8 | |
| ... | ... | @@ -45,16 +38,7 @@ public abstract class AbstractApplicationReplicaSetDialog<I extends AbstractAppl |
| 45 | 38 | Iterable<String> releaseNames, StringMessages stringMessages, ErrorReporter errorReporter, Validator<I> validator, DialogCallback<I> callback) { |
| 46 | 39 | super(title, /* message */ null, stringMessages.ok(), stringMessages.cancel(), validator, callback); |
| 47 | 40 | this.stringMessages = stringMessages; |
| 48 | - final List<String> releaseNamesAndLatestMaster = new LinkedList<>(); |
|
| 49 | - Util.addAll(releaseNames, releaseNamesAndLatestMaster); |
|
| 50 | - final Comparator<String> newestFirstComaprator = (r1, r2)->r2.compareTo(r1); |
|
| 51 | - Collections.sort(releaseNamesAndLatestMaster, newestFirstComaprator); |
|
| 52 | - releaseNamesAndLatestMaster.add(0, stringMessages.latestMasterRelease()); |
|
| 53 | - releaseNameBox = createSuggestBox(releaseNamesAndLatestMaster); |
|
| 54 | - if (releaseNameBox.getSuggestOracle() instanceof MultiWordSuggestOracle) { |
|
| 55 | - ((MultiWordSuggestOracle) releaseNameBox.getSuggestOracle()).setComparator(newestFirstComaprator); |
|
| 56 | - } |
|
| 57 | - releaseNameBox.setValue(stringMessages.latestMasterRelease()); |
|
| 41 | + releaseNameBox = LandscapeDialogUtil.createReleaseNameBox(stringMessages, releaseNames, this); |
|
| 58 | 42 | masterReplicationBearerTokenBox = createTextBox("", 40); |
| 59 | 43 | replicaReplicationBearerTokenBox = createTextBox("", 40); |
| 60 | 44 | } |
| ... | ... | @@ -67,11 +51,6 @@ public abstract class AbstractApplicationReplicaSetDialog<I extends AbstractAppl |
| 67 | 51 | return releaseNameBox; |
| 68 | 52 | } |
| 69 | 53 | |
| 70 | - protected String getReleaseNameBoxValue() { |
|
| 71 | - return (!Util.hasLength(releaseNameBox.getValue()) || Util.equalsWithNull(releaseNameBox.getValue(), stringMessages.latestMasterRelease())) |
|
| 72 | - ? null : releaseNameBox.getValue(); |
|
| 73 | - } |
|
| 74 | - |
|
| 75 | 54 | protected TextBox getMasterReplicationBearerTokenBox() { |
| 76 | 55 | return masterReplicationBearerTokenBox; |
| 77 | 56 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/AbstractNewProcessDialog.java
| ... | ... | @@ -0,0 +1,120 @@ |
| 1 | +package com.sap.sailing.landscape.ui.client; |
|
| 2 | + |
|
| 3 | +import com.google.gwt.user.client.ui.Grid; |
|
| 4 | +import com.google.gwt.user.client.ui.Label; |
|
| 5 | +import com.google.gwt.user.client.ui.ListBox; |
|
| 6 | +import com.google.gwt.user.client.ui.TextBox; |
|
| 7 | +import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
|
| 8 | +import com.sap.sse.gwt.client.ErrorReporter; |
|
| 9 | +import com.sap.sse.gwt.client.controls.IntegerBox; |
|
| 10 | +import com.sap.sse.gwt.client.dialog.DataEntryDialog; |
|
| 11 | + |
|
| 12 | +/** |
|
| 13 | + * Allows the user to specify the parameters required for moving a replica set's master process to a different |
|
| 14 | + * instance. |
|
| 15 | + * <p> |
|
| 16 | + * |
|
| 17 | + * @author Axel Uhl (d043530) |
|
| 18 | + * |
|
| 19 | + */ |
|
| 20 | +public abstract class AbstractNewProcessDialog<T> extends DataEntryDialog<T> { |
|
| 21 | + public static class NewProcessInstructions { |
|
| 22 | + private final String instanceTypeOrNull; |
|
| 23 | + private final Integer optionalMemoryInMegabytesOrNull; |
|
| 24 | + private final Integer optionalMemoryTotalSizeFactorOrNull; |
|
| 25 | + private final String masterReplicationBearerToken; |
|
| 26 | + private final String replicaReplicationBearerToken; |
|
| 27 | + |
|
| 28 | + public NewProcessInstructions(String instanceTypeOrNull, |
|
| 29 | + String masterReplicationBearerToken, String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull, |
|
| 30 | + Integer optionalMemoryTotalSizeFactorOrNull) { |
|
| 31 | + this.instanceTypeOrNull = instanceTypeOrNull; |
|
| 32 | + this.optionalMemoryInMegabytesOrNull = optionalMemoryInMegabytesOrNull; |
|
| 33 | + this.optionalMemoryTotalSizeFactorOrNull = optionalMemoryTotalSizeFactorOrNull; |
|
| 34 | + this.masterReplicationBearerToken = masterReplicationBearerToken; |
|
| 35 | + this.replicaReplicationBearerToken = replicaReplicationBearerToken; |
|
| 36 | + } |
|
| 37 | + public String getDedicatedInstanceType() { |
|
| 38 | + return instanceTypeOrNull; |
|
| 39 | + } |
|
| 40 | + public Integer getOptionalMemoryInMegabytesOrNull() { |
|
| 41 | + return optionalMemoryInMegabytesOrNull; |
|
| 42 | + } |
|
| 43 | + public Integer getOptionalMemoryTotalSizeFactorOrNull() { |
|
| 44 | + return optionalMemoryTotalSizeFactorOrNull; |
|
| 45 | + } |
|
| 46 | + public String getInstanceTypeOrNull() { |
|
| 47 | + return instanceTypeOrNull; |
|
| 48 | + } |
|
| 49 | + public String getMasterReplicationBearerToken() { |
|
| 50 | + return masterReplicationBearerToken; |
|
| 51 | + } |
|
| 52 | + public String getReplicaReplicationBearerToken() { |
|
| 53 | + return replicaReplicationBearerToken; |
|
| 54 | + } |
|
| 55 | + } |
|
| 56 | + |
|
| 57 | + protected final StringMessages stringMessages; |
|
| 58 | + private final ListBox instanceTypeListBox; |
|
| 59 | + private final Label instanceTypeLabel; |
|
| 60 | + private final TextBox masterReplicationBearerTokenBox; |
|
| 61 | + private final TextBox replicaReplicationBearerTokenBox; |
|
| 62 | + private final IntegerBox memoryInMegabytesBox; |
|
| 63 | + private final IntegerBox memoryTotalSizeFactorBox; |
|
| 64 | + |
|
| 65 | + public AbstractNewProcessDialog(String title, String defaultInstanceTypeName, |
|
| 66 | + LandscapeManagementWriteServiceAsync landscapeManagementService, StringMessages stringMessages, |
|
| 67 | + ErrorReporter errorReporter, DialogCallback<T> callback) { |
|
| 68 | + super(title, /* message */ null, stringMessages.ok(), stringMessages.cancel(), /* validator */ null, callback); |
|
| 69 | + this.stringMessages = stringMessages; |
|
| 70 | + instanceTypeListBox = LandscapeDialogUtil.createInstanceTypeListBox(this, landscapeManagementService, |
|
| 71 | + stringMessages, defaultInstanceTypeName, errorReporter, /* canBeDeployedInNlbInstanceBasedTargetGroup */ false); |
|
| 72 | + instanceTypeLabel = new Label(); |
|
| 73 | + masterReplicationBearerTokenBox = createTextBox("", 40); |
|
| 74 | + replicaReplicationBearerTokenBox = createTextBox("", 40); |
|
| 75 | + memoryInMegabytesBox = createIntegerBox(null, 7); |
|
| 76 | + memoryTotalSizeFactorBox = createIntegerBox(null, 2); |
|
| 77 | + getMemoryInMegabytesBox().addValueChangeHandler(e->getMemoryTotalSizeFactorBox().setEnabled(e.getValue() == null)); |
|
| 78 | + } |
|
| 79 | + |
|
| 80 | + @Override |
|
| 81 | + protected Grid getAdditionalWidget() { |
|
| 82 | + final Grid result = new Grid(5, 2); |
|
| 83 | + int row=0; |
|
| 84 | + result.setWidget(row, 0, getInstanceTypeLabel()); |
|
| 85 | + result.setWidget(row++, 1, getInstanceTypeListBox()); |
|
| 86 | + result.setWidget(row, 0, new Label(stringMessages.bearerTokenForSecurityReplication())); |
|
| 87 | + result.setWidget(row++, 1, getMasterReplicationBearerTokenBox()); |
|
| 88 | + result.setWidget(row, 0, new Label(stringMessages.replicaReplicationBearerToken())); |
|
| 89 | + result.setWidget(row++, 1, getReplicaReplicationBearerTokenBox()); |
|
| 90 | + result.setWidget(row, 0, new Label(stringMessages.memoryInMegabytes())); |
|
| 91 | + result.setWidget(row++, 1, getMemoryInMegabytesBox()); |
|
| 92 | + result.setWidget(row, 0, new Label(stringMessages.memoryTotalSizeFactor())); |
|
| 93 | + result.setWidget(row++, 1, getMemoryTotalSizeFactorBox()); |
|
| 94 | + return result; |
|
| 95 | + } |
|
| 96 | + |
|
| 97 | + protected IntegerBox getMemoryTotalSizeFactorBox() { |
|
| 98 | + return memoryTotalSizeFactorBox; |
|
| 99 | + } |
|
| 100 | + |
|
| 101 | + protected IntegerBox getMemoryInMegabytesBox() { |
|
| 102 | + return memoryInMegabytesBox; |
|
| 103 | + } |
|
| 104 | + |
|
| 105 | + protected TextBox getReplicaReplicationBearerTokenBox() { |
|
| 106 | + return replicaReplicationBearerTokenBox; |
|
| 107 | + } |
|
| 108 | + |
|
| 109 | + protected TextBox getMasterReplicationBearerTokenBox() { |
|
| 110 | + return masterReplicationBearerTokenBox; |
|
| 111 | + } |
|
| 112 | + |
|
| 113 | + protected Label getInstanceTypeLabel() { |
|
| 114 | + return instanceTypeLabel; |
|
| 115 | + } |
|
| 116 | + |
|
| 117 | + protected ListBox getInstanceTypeListBox() { |
|
| 118 | + return instanceTypeListBox; |
|
| 119 | + } |
|
| 120 | +} |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/ApplicationReplicaSetsImagesBarCell.java
| ... | ... | @@ -14,6 +14,7 @@ import com.sap.sse.security.ui.client.UserService; |
| 14 | 14 | public class ApplicationReplicaSetsImagesBarCell extends ImagesBarCell { |
| 15 | 15 | static final String ACTION_REMOVE = DefaultActions.DELETE.name(); |
| 16 | 16 | static final String ACTION_UPGRADE = "UPGRADE"; |
| 17 | + static final String ACTION_ACTIVATE_ARCHIVE_CANDIDATE = "ACTIVATE_ARCHIVE_CANDIDATE"; |
|
| 17 | 18 | static final String ACTION_ARCHIVE = "ARCHIVE"; |
| 18 | 19 | static final String ACTION_DEFINE_LANDING_PAGE = "DEFINE_LANDING_PAGE"; |
| 19 | 20 | static final String ACTION_CREATE_LOAD_BALANCER_MAPPING = "CREATE_LOAD_BALANGER_MAPPING"; |
| ... | ... | @@ -60,9 +61,12 @@ public class ApplicationReplicaSetsImagesBarCell extends ImagesBarCell { |
| 60 | 61 | result.add(new ImageSpec(ACTION_LAUNCH_ANOTHER_REPLICA_SET_ON_THIS_MASTER, |
| 61 | 62 | stringMessages.launchAnotherReplicaSetOnThisMaster(), |
| 62 | 63 | IconResources.INSTANCE.launchAnotherReplicaSetOnThisMasterIcon())); |
| 63 | - if (!applicationReplicaSet.isLocalReplicaSet(userService) && !applicationReplicaSet.isArchive()) { |
|
| 64 | + if (!applicationReplicaSet.isLocalReplicaSet(userService)) { |
|
| 64 | 65 | result.add(new ImageSpec(ACTION_UPGRADE, stringMessages.upgrade(), IconResources.INSTANCE.refreshIcon())); |
| 65 | 66 | } |
| 67 | + if (applicationReplicaSet.isArchive()) { |
|
| 68 | + result.add(new ImageSpec(ACTION_ACTIVATE_ARCHIVE_CANDIDATE, stringMessages.activateArchiveCandidate(), IconResources.INSTANCE.check())); |
|
| 69 | + } |
|
| 66 | 70 | if (!applicationReplicaSet.isArchive()) { |
| 67 | 71 | result.add( |
| 68 | 72 | new ImageSpec(ACTION_ENSURE_ONE_REPLICA_THEN_STOP_REPLICATING_AND_REMOVE_MASTER_FROM_TARGET_GROUPS, |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/CreateApplicationReplicaSetDialog.java
| ... | ... | @@ -319,7 +319,7 @@ public class CreateApplicationReplicaSetDialog extends AbstractApplicationReplic |
| 319 | 319 | return new CreateApplicationReplicaSetInstructions(nameBox.getValue(), sharedMasterInstanceBox.getValue(), |
| 320 | 320 | getDedicatedInstanceTypeListBox().getSelectedValue(), |
| 321 | 321 | getSharedInstanceTypeListBox().getSelectedValue(), |
| 322 | - getReleaseNameBoxValue(), dynamicLoadBalancerCheckBox==null?false:dynamicLoadBalancerCheckBox.getValue(), |
|
| 322 | + LandscapeDialogUtil.getReleaseNameBoxValue(getReleaseNameBox(), stringMessages), dynamicLoadBalancerCheckBox==null?false:dynamicLoadBalancerCheckBox.getValue(), |
|
| 323 | 323 | getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue(), |
| 324 | 324 | domainNameBox.getValue(), memoryInMegabytesBox.getValue(), memoryTotalSizeFactorBox.getValue(), |
| 325 | 325 | igtimiRiotPortBox.getValue(), startWithReplicaOnSharedInstanceBox.getValue()); |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeDialogUtil.java
| ... | ... | @@ -2,10 +2,16 @@ package com.sap.sailing.landscape.ui.client; |
| 2 | 2 | |
| 3 | 3 | import java.util.ArrayList; |
| 4 | 4 | import java.util.Collections; |
| 5 | +import java.util.Comparator; |
|
| 6 | +import java.util.LinkedList; |
|
| 7 | +import java.util.List; |
|
| 5 | 8 | |
| 6 | 9 | import com.google.gwt.user.client.rpc.AsyncCallback; |
| 7 | 10 | import com.google.gwt.user.client.ui.ListBox; |
| 11 | +import com.google.gwt.user.client.ui.MultiWordSuggestOracle; |
|
| 12 | +import com.google.gwt.user.client.ui.SuggestBox; |
|
| 8 | 13 | import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
| 14 | +import com.sap.sse.common.Util; |
|
| 9 | 15 | import com.sap.sse.common.util.NaturalComparator; |
| 10 | 16 | import com.sap.sse.gwt.client.ErrorReporter; |
| 11 | 17 | import com.sap.sse.gwt.client.dialog.DataEntryDialog; |
| ... | ... | @@ -67,4 +73,23 @@ public class LandscapeDialogUtil { |
| 67 | 73 | } |
| 68 | 74 | } |
| 69 | 75 | } |
| 76 | + |
|
| 77 | + public static SuggestBox createReleaseNameBox(StringMessages stringMessages, Iterable<String> releaseNames, DataEntryDialog<?> dialog) { |
|
| 78 | + final List<String> releaseNamesAndLatestMaster = new LinkedList<>(); |
|
| 79 | + Util.addAll(releaseNames, releaseNamesAndLatestMaster); |
|
| 80 | + final Comparator<String> newestFirstComaprator = (r1, r2)->r2.compareTo(r1); |
|
| 81 | + Collections.sort(releaseNamesAndLatestMaster, newestFirstComaprator); |
|
| 82 | + releaseNamesAndLatestMaster.add(0, stringMessages.latestMasterRelease()); |
|
| 83 | + SuggestBox releaseNameBox = dialog.createSuggestBox(releaseNamesAndLatestMaster); |
|
| 84 | + if (releaseNameBox.getSuggestOracle() instanceof MultiWordSuggestOracle) { |
|
| 85 | + ((MultiWordSuggestOracle) releaseNameBox.getSuggestOracle()).setComparator(newestFirstComaprator); |
|
| 86 | + } |
|
| 87 | + releaseNameBox.setValue(stringMessages.latestMasterRelease()); |
|
| 88 | + return releaseNameBox; |
|
| 89 | + } |
|
| 90 | + |
|
| 91 | + public static String getReleaseNameBoxValue(SuggestBox releaseNameBox, StringMessages stringMessages) { |
|
| 92 | + return (!Util.hasLength(releaseNameBox.getValue()) || Util.equalsWithNull(releaseNameBox.getValue(), stringMessages.latestMasterRelease())) |
|
| 93 | + ? null : releaseNameBox.getValue(); |
|
| 94 | + } |
|
| 70 | 95 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementPanel.java
| ... | ... | @@ -39,6 +39,7 @@ import com.sap.sailing.landscape.ui.client.CreateApplicationReplicaSetDialog.Cre |
| 39 | 39 | import com.sap.sailing.landscape.ui.client.MoveMasterProcessDialog.MoveMasterToOtherInstanceInstructions; |
| 40 | 40 | import com.sap.sailing.landscape.ui.client.SwitchToReplicaOnSharedInstanceDialog.SwitchToReplicaOnSharedInstanceDialogInstructions; |
| 41 | 41 | import com.sap.sailing.landscape.ui.client.UpgradeApplicationReplicaSetDialog.UpgradeApplicationReplicaSetInstructions; |
| 42 | +import com.sap.sailing.landscape.ui.client.UpgradeArchiveServerDialog.UpgradeArchiveServerInstructions; |
|
| 42 | 43 | import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
| 43 | 44 | import com.sap.sailing.landscape.ui.shared.AmazonMachineImageDTO; |
| 44 | 45 | import com.sap.sailing.landscape.ui.shared.AvailabilityZoneDTO; |
| ... | ... | @@ -323,8 +324,19 @@ public class LandscapeManagementPanel extends SimplePanel { |
| 323 | 324 | applicationReplicaSetToArchive -> archiveApplicationReplicaSet(stringMessages, |
| 324 | 325 | regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetToArchive)); |
| 325 | 326 | applicationReplicaSetsActionColumn.addAction(ApplicationReplicaSetsImagesBarCell.ACTION_UPGRADE, |
| 326 | - applicationReplicaSetToUpgrade -> upgradeApplicationReplicaSet(stringMessages, |
|
| 327 | - regionsTable.getSelectionModel().getSelectedObject(), Collections.singleton(applicationReplicaSetToUpgrade))); |
|
| 327 | + applicationReplicaSetToUpgrade -> { |
|
| 328 | + if (applicationReplicaSetToUpgrade.isArchive()) { |
|
| 329 | + upgradeArchiveServer(stringMessages, |
|
| 330 | + regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetToUpgrade); |
|
| 331 | + } else { |
|
| 332 | + upgradeApplicationReplicaSet(stringMessages, |
|
| 333 | + regionsTable.getSelectionModel().getSelectedObject(), Collections.singleton(applicationReplicaSetToUpgrade)); |
|
| 334 | + } |
|
| 335 | + } |
|
| 336 | + ); |
|
| 337 | + applicationReplicaSetsActionColumn.addAction(ApplicationReplicaSetsImagesBarCell.ACTION_ACTIVATE_ARCHIVE_CANDIDATE, |
|
| 338 | + applicationReplicaSetToActivateAsNewArchive -> makeCandidateArchiveServerGoLive(stringMessages, |
|
| 339 | + regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetToActivateAsNewArchive)); |
|
| 328 | 340 | applicationReplicaSetsActionColumn.addAction(ApplicationReplicaSetsImagesBarCell.ACTION_DEFINE_LANDING_PAGE, |
| 329 | 341 | applicationReplicaSetForWhichToDefineLandingPage -> defineLandingPage(stringMessages, |
| 330 | 342 | regionsTable.getSelectionModel().getSelectedObject(), applicationReplicaSetForWhichToDefineLandingPage)); |
| ... | ... | @@ -1406,7 +1418,7 @@ public class LandscapeManagementPanel extends SimplePanel { |
| 1406 | 1418 | new Timer() { |
| 1407 | 1419 | @Override |
| 1408 | 1420 | public void run() { |
| 1409 | - landscapeManagementService.upgradeApplicationReplicaSet(regionId, replicaSet, |
|
| 1421 | + landscapeManagementService.upgradeApplicationReplicaSet(regionId, replicaSet, |
|
| 1410 | 1422 | upgradeInstructions.getReleaseNameOrNullForLatestMaster(), |
| 1411 | 1423 | sshKeyManagementPanel.getSelectedKeyPair()==null?null:sshKeyManagementPanel.getSelectedKeyPair().getName(), |
| 1412 | 1424 | sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null |
| ... | ... | @@ -1447,6 +1459,86 @@ public class LandscapeManagementPanel extends SimplePanel { |
| 1447 | 1459 | } |
| 1448 | 1460 | }); |
| 1449 | 1461 | } |
| 1462 | + |
|
| 1463 | + private void upgradeArchiveServer(StringMessages stringMessages, String regionId, |
|
| 1464 | + SailingApplicationReplicaSetDTO<String> archiveReplicaSet) { |
|
| 1465 | + applicationReplicaSetsBusy.setBusy(true); |
|
| 1466 | + landscapeManagementService.getReleases(new AsyncCallback<ArrayList<ReleaseDTO>>() { |
|
| 1467 | + @Override |
|
| 1468 | + public void onFailure(Throwable caught) { |
|
| 1469 | + applicationReplicaSetsBusy.setBusy(false); |
|
| 1470 | + errorReporter.reportError(caught.getMessage()); |
|
| 1471 | + } |
|
| 1472 | + |
|
| 1473 | + @Override |
|
| 1474 | + public void onSuccess(ArrayList<ReleaseDTO> releases) { |
|
| 1475 | + new UpgradeArchiveServerDialog(landscapeManagementService, archiveReplicaSet.getMaster().getHost().getInstanceType(), |
|
| 1476 | + releases.stream().map(r->r.getName())::iterator, |
|
| 1477 | + stringMessages, errorReporter, new DialogCallback<UpgradeArchiveServerDialog.UpgradeArchiveServerInstructions>() { |
|
| 1478 | + @Override |
|
| 1479 | + public void ok(UpgradeArchiveServerInstructions upgradeInstructions) { |
|
| 1480 | + landscapeManagementService.createArchiveReplicaSet(regionId, archiveReplicaSet, upgradeInstructions.getInstanceTypeOrNull(), |
|
| 1481 | + upgradeInstructions.getReleaseNameOrNullForLatestMaster(), sshKeyManagementPanel.getSelectedKeyPair()==null?null:sshKeyManagementPanel.getSelectedKeyPair().getName(), |
|
| 1482 | + sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null |
|
| 1483 | + ? sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption().getBytes() : null, |
|
| 1484 | + upgradeInstructions.getMasterReplicationBearerToken(), upgradeInstructions.getReplicaReplicationBearerToken(), |
|
| 1485 | + upgradeInstructions.getOptionalMemoryInMegabytesOrNull(), upgradeInstructions.getOptionalMemoryTotalSizeFactorOrNull(), |
|
| 1486 | + new AsyncCallback<Void>() { |
|
| 1487 | + @Override |
|
| 1488 | + public void onFailure(Throwable caught) { |
|
| 1489 | + applicationReplicaSetsBusy.setBusy(false); |
|
| 1490 | + errorReporter.reportError(caught.getMessage()); |
|
| 1491 | + } |
|
| 1492 | + |
|
| 1493 | + @Override |
|
| 1494 | + public void onSuccess(Void result) { |
|
| 1495 | + applicationReplicaSetsBusy.setBusy(false); |
|
| 1496 | + Notification.notify( |
|
| 1497 | + stringMessages.successfullyLaunchedNewArchiveCandidate( |
|
| 1498 | + archiveReplicaSet.getName(), |
|
| 1499 | + upgradeInstructions |
|
| 1500 | + .getReleaseNameOrNullForLatestMaster() == null |
|
| 1501 | + ? "Default" |
|
| 1502 | + : upgradeInstructions |
|
| 1503 | + .getReleaseNameOrNullForLatestMaster()), |
|
| 1504 | + NotificationType.SUCCESS); |
|
| 1505 | + } |
|
| 1506 | + }); |
|
| 1507 | + } |
|
| 1508 | + |
|
| 1509 | + @Override |
|
| 1510 | + public void cancel() { |
|
| 1511 | + applicationReplicaSetsBusy.setBusy(false); |
|
| 1512 | + } |
|
| 1513 | + }).show(); |
|
| 1514 | + } |
|
| 1515 | + }); |
|
| 1516 | + } |
|
| 1517 | + |
|
| 1518 | + private void makeCandidateArchiveServerGoLive(StringMessages stringMessages, String regionId, SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade) { |
|
| 1519 | + if (Window.confirm(stringMessages.reallySwitchToNewArchiveCandidate())) { |
|
| 1520 | + applicationReplicaSetsBusy.setBusy(true); |
|
| 1521 | + landscapeManagementService.makeCandidateArchiveServerGoLive(regionId, archiveReplicaSetToUpgrade, |
|
| 1522 | + sshKeyManagementPanel.getSelectedKeyPair() == null ? null |
|
| 1523 | + : sshKeyManagementPanel.getSelectedKeyPair().getName(), |
|
| 1524 | + sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption() != null |
|
| 1525 | + ? sshKeyManagementPanel.getPassphraseForPrivateKeyDecryption().getBytes() |
|
| 1526 | + : null, |
|
| 1527 | + new AsyncCallback<Void>() { |
|
| 1528 | + @Override |
|
| 1529 | + public void onFailure(Throwable caught) { |
|
| 1530 | + applicationReplicaSetsBusy.setBusy(false); |
|
| 1531 | + errorReporter.reportError(caught.getMessage()); |
|
| 1532 | + } |
|
| 1533 | + |
|
| 1534 | + @Override |
|
| 1535 | + public void onSuccess(Void result) { |
|
| 1536 | + applicationReplicaSetsBusy.setBusy(false); |
|
| 1537 | + Notification.notify(stringMessages.successfullySwitchedToNewArchiveCandidate(archiveReplicaSetToUpgrade.getName()), NotificationType.SUCCESS); |
|
| 1538 | + } |
|
| 1539 | + }); |
|
| 1540 | + } |
|
| 1541 | + } |
|
| 1450 | 1542 | |
| 1451 | 1543 | private void refreshRegionsTable(UserService userService) { |
| 1452 | 1544 | landscapeManagementService.getRegions(new AsyncCallback<ArrayList<String>>() { |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteService.java
| ... | ... | @@ -166,6 +166,11 @@ public interface LandscapeManagementWriteService extends RemoteService { |
| 166 | 166 | SailingApplicationReplicaSetDTO<String> replicaSet, String instanceTypeName, |
| 167 | 167 | String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
| 168 | 168 | |
| 169 | + void createArchiveReplicaSet( |
|
| 170 | + String regionId, SailingApplicationReplicaSetDTO<String> applicationReplicaSetToUpgrade, String optionalSharedInstanceType, String releaseOrNullForLatestMaster, |
|
| 171 | + String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String securityReplicationBearerToken, String replicaReplicationBearerToken, |
|
| 172 | + Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) throws Exception; |
|
| 173 | + |
|
| 169 | 174 | ArrayList<LeaderboardNameDTO> getLeaderboardNames(SailingApplicationReplicaSetDTO<String> replicaSet, String bearerToken) throws Exception; |
| 170 | 175 | |
| 171 | 176 | void addShard(String shardName, ArrayList<LeaderboardNameDTO> selectedLeaderBoardNames, SailingApplicationReplicaSetDTO<String> replicaSet, |
| ... | ... | @@ -183,4 +188,8 @@ public interface LandscapeManagementWriteService extends RemoteService { |
| 183 | 188 | String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
| 184 | 189 | |
| 185 | 190 | boolean hasDNSResourceRecordsForReplicaSet(String replicaSetName, String optionalDomainName); |
| 191 | + |
|
| 192 | + void makeCandidateArchiveServerGoLive(String regionId, |
|
| 193 | + SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade, String optionalKeyName, |
|
| 194 | + byte[] privateKeyEncryptionPassphrase) throws Exception; |
|
| 186 | 195 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/LandscapeManagementWriteServiceAsync.java
| ... | ... | @@ -189,7 +189,15 @@ public interface LandscapeManagementWriteServiceAsync { |
| 189 | 189 | Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, |
| 190 | 190 | AwsInstanceDTO optionalPreferredInstanceToDeployUnmanagedReplicaTo, |
| 191 | 191 | AsyncCallback<SailingApplicationReplicaSetDTO<String>> callback); |
| 192 | - |
|
| 192 | + |
|
| 193 | + void createArchiveReplicaSet(String regionId, SailingApplicationReplicaSetDTO<String> applicationReplicaSetToUpgrade, |
|
| 194 | + String optionalSharedInstanceType, String releaseOrNullForLatestMaster, String optionalKeyName, |
|
| 195 | + byte[] privateKeyEncryptionPassphrase, String securityReplicationBearerToken, String replicaReplicationBearerToken, |
|
| 196 | + Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull, AsyncCallback<Void> callback); |
|
| 197 | + |
|
| 198 | + void makeCandidateArchiveServerGoLive(String regionId, |
|
| 199 | + SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade, String optionalKeyName, |
|
| 200 | + byte[] privateKeyEncryptionPassphrase, AsyncCallback<Void> callback); |
|
| 193 | 201 | /** |
| 194 | 202 | * For the given replica set ensures there is at least one healthy replica, then stops replicating on all replicas and |
| 195 | 203 | * removes the master from the public and master target groups. This can be used as a preparatory action for upgrading |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/MoveMasterProcessDialog.java
| ... | ... | @@ -3,14 +3,9 @@ package com.sap.sailing.landscape.ui.client; |
| 3 | 3 | import com.google.gwt.user.client.ui.CheckBox; |
| 4 | 4 | import com.google.gwt.user.client.ui.Grid; |
| 5 | 5 | import com.google.gwt.user.client.ui.Label; |
| 6 | -import com.google.gwt.user.client.ui.ListBox; |
|
| 7 | -import com.google.gwt.user.client.ui.TextBox; |
|
| 8 | -import com.google.gwt.user.client.ui.Widget; |
|
| 9 | 6 | import com.sap.sailing.landscape.common.SharedLandscapeConstants; |
| 10 | 7 | import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
| 11 | 8 | import com.sap.sse.gwt.client.ErrorReporter; |
| 12 | -import com.sap.sse.gwt.client.controls.IntegerBox; |
|
| 13 | -import com.sap.sse.gwt.client.dialog.DataEntryDialog; |
|
| 14 | 9 | |
| 15 | 10 | /** |
| 16 | 11 | * Allows the user to specify the parameters required for moving a replica set's master process to a different |
| ... | ... | @@ -20,115 +15,65 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog; |
| 20 | 15 | * @author Axel Uhl (d043530) |
| 21 | 16 | * |
| 22 | 17 | */ |
| 23 | -public class MoveMasterProcessDialog extends DataEntryDialog<MoveMasterProcessDialog.MoveMasterToOtherInstanceInstructions> { |
|
| 18 | +public class MoveMasterProcessDialog extends AbstractNewProcessDialog<MoveMasterProcessDialog.MoveMasterToOtherInstanceInstructions> { |
|
| 24 | 19 | |
| 25 | - public static class MoveMasterToOtherInstanceInstructions { |
|
| 20 | + public static class MoveMasterToOtherInstanceInstructions extends AbstractNewProcessDialog.NewProcessInstructions { |
|
| 26 | 21 | private final boolean sharedMasterInstance; |
| 27 | - private final String instanceTypeOrNull; |
|
| 28 | - private final Integer optionalMemoryInMegabytesOrNull; |
|
| 29 | - private final Integer optionalMemoryTotalSizeFactorOrNull; |
|
| 30 | - private final String masterReplicationBearerToken; |
|
| 31 | - private final String replicaReplicationBearerToken; |
|
| 32 | 22 | |
| 33 | 23 | public MoveMasterToOtherInstanceInstructions(boolean sharedMasterInstance, |
| 34 | 24 | String instanceTypeOrNull, |
| 35 | 25 | String masterReplicationBearerToken, String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull, |
| 36 | 26 | Integer optionalMemoryTotalSizeFactorOrNull) { |
| 27 | + super(instanceTypeOrNull, masterReplicationBearerToken, replicaReplicationBearerToken, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull); |
|
| 37 | 28 | this.sharedMasterInstance = sharedMasterInstance; |
| 38 | - this.instanceTypeOrNull = instanceTypeOrNull; |
|
| 39 | - this.optionalMemoryInMegabytesOrNull = optionalMemoryInMegabytesOrNull; |
|
| 40 | - this.optionalMemoryTotalSizeFactorOrNull = optionalMemoryTotalSizeFactorOrNull; |
|
| 41 | - this.masterReplicationBearerToken = masterReplicationBearerToken; |
|
| 42 | - this.replicaReplicationBearerToken = replicaReplicationBearerToken; |
|
| 43 | 29 | } |
| 44 | 30 | public boolean isSharedMasterInstance() { |
| 45 | 31 | return sharedMasterInstance; |
| 46 | 32 | } |
| 47 | - public String getDedicatedInstanceType() { |
|
| 48 | - return instanceTypeOrNull; |
|
| 49 | - } |
|
| 50 | - public Integer getOptionalMemoryInMegabytesOrNull() { |
|
| 51 | - return optionalMemoryInMegabytesOrNull; |
|
| 52 | - } |
|
| 53 | - public Integer getOptionalMemoryTotalSizeFactorOrNull() { |
|
| 54 | - return optionalMemoryTotalSizeFactorOrNull; |
|
| 55 | - } |
|
| 56 | - public String getInstanceTypeOrNull() { |
|
| 57 | - return instanceTypeOrNull; |
|
| 58 | - } |
|
| 59 | - public String getMasterReplicationBearerToken() { |
|
| 60 | - return masterReplicationBearerToken; |
|
| 61 | - } |
|
| 62 | - public String getReplicaReplicationBearerToken() { |
|
| 63 | - return replicaReplicationBearerToken; |
|
| 64 | - } |
|
| 65 | 33 | } |
| 66 | 34 | |
| 67 | - private final StringMessages stringMessages; |
|
| 68 | 35 | private final CheckBox sharedMasterInstanceBox; |
| 69 | - private final ListBox instanceTypeListBox; |
|
| 70 | - private final Label instanceTypeLabel; |
|
| 71 | - private final TextBox masterReplicationBearerTokenBox; |
|
| 72 | - private final TextBox replicaReplicationBearerTokenBox; |
|
| 73 | - private final IntegerBox memoryInMegabytesBox; |
|
| 74 | - private final IntegerBox memoryTotalSizeFactorBox; |
|
| 75 | 36 | private boolean memoryAsFactorToTotalMemoryAdjusted; |
| 76 | 37 | |
| 77 | 38 | public MoveMasterProcessDialog(LandscapeManagementWriteServiceAsync landscapeManagementService, |
| 78 | - StringMessages stringMessages, ErrorReporter errorReporter, DialogCallback<MoveMasterToOtherInstanceInstructions> callback) { |
|
| 79 | - super(stringMessages.moveMasterToOtherInstance(), /* message */ null, stringMessages.ok(), stringMessages.cancel(), /* validator */ null, callback); |
|
| 80 | - this.stringMessages = stringMessages; |
|
| 81 | - instanceTypeListBox = LandscapeDialogUtil.createInstanceTypeListBox(this, landscapeManagementService, |
|
| 82 | - stringMessages, SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME, errorReporter, /* canBeDeployedInNlbInstanceBasedTargetGroup */ false); |
|
| 83 | - instanceTypeLabel = new Label(); |
|
| 84 | - masterReplicationBearerTokenBox = createTextBox("", 40); |
|
| 85 | - replicaReplicationBearerTokenBox = createTextBox("", 40); |
|
| 86 | - memoryInMegabytesBox = createIntegerBox(null, 7); |
|
| 87 | - memoryTotalSizeFactorBox = createIntegerBox(null, 2); |
|
| 88 | - memoryTotalSizeFactorBox.addValueChangeHandler(e->memoryAsFactorToTotalMemoryAdjusted=true); |
|
| 89 | - memoryInMegabytesBox.addValueChangeHandler(e->memoryTotalSizeFactorBox.setEnabled(e.getValue() == null)); |
|
| 39 | + StringMessages stringMessages, ErrorReporter errorReporter, |
|
| 40 | + DialogCallback<MoveMasterToOtherInstanceInstructions> callback) { |
|
| 41 | + super(stringMessages.moveMasterToOtherInstance(), SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME, |
|
| 42 | + landscapeManagementService, stringMessages, errorReporter, callback); |
|
| 43 | + getMemoryTotalSizeFactorBox().addValueChangeHandler(e->memoryAsFactorToTotalMemoryAdjusted=true); |
|
| 90 | 44 | sharedMasterInstanceBox = createCheckbox(stringMessages.sharedMasterInstance()); |
| 91 | 45 | sharedMasterInstanceBox.addValueChangeHandler(e->updateInstanceTypeBasedOnSharedMasterInstanceBox()); |
| 92 | 46 | updateInstanceTypeBasedOnSharedMasterInstanceBox(); |
| 93 | 47 | } |
| 94 | 48 | |
| 95 | 49 | private void updateInstanceTypeBasedOnSharedMasterInstanceBox() { |
| 96 | - instanceTypeLabel.setText(sharedMasterInstanceBox.getValue() ? stringMessages.sharedMasterInstanceType() : stringMessages.dedicatedInstanceType()); |
|
| 97 | - LandscapeDialogUtil.selectInstanceType(instanceTypeListBox, |
|
| 50 | + getInstanceTypeLabel().setText(sharedMasterInstanceBox.getValue() ? stringMessages.sharedMasterInstanceType() : stringMessages.dedicatedInstanceType()); |
|
| 51 | + LandscapeDialogUtil.selectInstanceType(getInstanceTypeListBox(), |
|
| 98 | 52 | sharedMasterInstanceBox.getValue() ? SharedLandscapeConstants.DEFAULT_SHARED_INSTANCE_TYPE_NAME : SharedLandscapeConstants.DEFAULT_DEDICATED_INSTANCE_TYPE_NAME); |
| 99 | 53 | if (!memoryAsFactorToTotalMemoryAdjusted) { |
| 100 | 54 | if (sharedMasterInstanceBox.getValue()) { |
| 101 | - memoryTotalSizeFactorBox.setValue(SharedLandscapeConstants.DEFAULT_NUMBER_OF_PROCESSES_IN_MEMORY); |
|
| 55 | + getMemoryTotalSizeFactorBox().setValue(SharedLandscapeConstants.DEFAULT_NUMBER_OF_PROCESSES_IN_MEMORY); |
|
| 102 | 56 | } else { |
| 103 | - memoryTotalSizeFactorBox.setText(""); |
|
| 57 | + getMemoryTotalSizeFactorBox().setText(""); |
|
| 104 | 58 | } |
| 105 | 59 | } |
| 106 | 60 | } |
| 107 | 61 | |
| 108 | 62 | @Override |
| 109 | - protected Widget getAdditionalWidget() { |
|
| 110 | - final Grid result = new Grid(6, 2); |
|
| 63 | + protected Grid getAdditionalWidget() { |
|
| 64 | + final Grid result = super.getAdditionalWidget(); |
|
| 111 | 65 | int row=0; |
| 66 | + result.insertRow(row); |
|
| 112 | 67 | result.setWidget(row, 0, new Label(stringMessages.sharedMasterInstance())); |
| 113 | 68 | result.setWidget(row++, 1, sharedMasterInstanceBox); |
| 114 | - result.setWidget(row, 0, instanceTypeLabel); |
|
| 115 | - result.setWidget(row++, 1, instanceTypeListBox); |
|
| 116 | - result.setWidget(row, 0, new Label(stringMessages.bearerTokenForSecurityReplication())); |
|
| 117 | - result.setWidget(row++, 1, masterReplicationBearerTokenBox); |
|
| 118 | - result.setWidget(row, 0, new Label(stringMessages.replicaReplicationBearerToken())); |
|
| 119 | - result.setWidget(row++, 1, replicaReplicationBearerTokenBox); |
|
| 120 | - result.setWidget(row, 0, new Label(stringMessages.memoryInMegabytes())); |
|
| 121 | - result.setWidget(row++, 1, memoryInMegabytesBox); |
|
| 122 | - result.setWidget(row, 0, new Label(stringMessages.memoryTotalSizeFactor())); |
|
| 123 | - result.setWidget(row++, 1, memoryTotalSizeFactorBox); |
|
| 124 | 69 | return result; |
| 125 | 70 | } |
| 126 | 71 | |
| 127 | 72 | @Override |
| 128 | 73 | protected MoveMasterToOtherInstanceInstructions getResult() { |
| 129 | 74 | return new MoveMasterToOtherInstanceInstructions(sharedMasterInstanceBox.getValue(), |
| 130 | - instanceTypeListBox.getSelectedValue(), |
|
| 131 | - masterReplicationBearerTokenBox.getValue(), replicaReplicationBearerTokenBox.getValue(), |
|
| 132 | - memoryInMegabytesBox.getValue(), memoryTotalSizeFactorBox.getValue()); |
|
| 75 | + getInstanceTypeListBox().getSelectedValue(), |
|
| 76 | + getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue(), |
|
| 77 | + getMemoryInMegabytesBox().getValue(), getMemoryTotalSizeFactorBox().getValue()); |
|
| 133 | 78 | } |
| 134 | 79 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/UpgradeApplicationReplicaSetDialog.java
| ... | ... | @@ -40,6 +40,8 @@ public class UpgradeApplicationReplicaSetDialog extends AbstractApplicationRepli |
| 40 | 40 | |
| 41 | 41 | @Override |
| 42 | 42 | protected UpgradeApplicationReplicaSetInstructions getResult() { |
| 43 | - return new UpgradeApplicationReplicaSetInstructions(getReleaseNameBoxValue(), getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue()); |
|
| 43 | + return new UpgradeApplicationReplicaSetInstructions( |
|
| 44 | + LandscapeDialogUtil.getReleaseNameBoxValue(getReleaseNameBox(), stringMessages), |
|
| 45 | + getMasterReplicationBearerTokenBox().getValue(), getReplicaReplicationBearerTokenBox().getValue()); |
|
| 44 | 46 | } |
| 45 | 47 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/UpgradeArchiveServerDialog.java
| ... | ... | @@ -0,0 +1,57 @@ |
| 1 | +package com.sap.sailing.landscape.ui.client; |
|
| 2 | + |
|
| 3 | +import com.google.gwt.user.client.ui.FocusWidget; |
|
| 4 | +import com.google.gwt.user.client.ui.Grid; |
|
| 5 | +import com.google.gwt.user.client.ui.Label; |
|
| 6 | +import com.google.gwt.user.client.ui.SuggestBox; |
|
| 7 | +import com.sap.sailing.landscape.ui.client.i18n.StringMessages; |
|
| 8 | +import com.sap.sse.gwt.client.ErrorReporter; |
|
| 9 | + |
|
| 10 | +public class UpgradeArchiveServerDialog extends AbstractNewProcessDialog<UpgradeArchiveServerDialog.UpgradeArchiveServerInstructions> { |
|
| 11 | + |
|
| 12 | + public static class UpgradeArchiveServerInstructions extends AbstractNewProcessDialog.NewProcessInstructions { |
|
| 13 | + private final String releaseNameOrNullForLatestMaster; |
|
| 14 | + |
|
| 15 | + public UpgradeArchiveServerInstructions(String releaseNameOrNullForLatestMaster, |
|
| 16 | + String masterReplicationBearerToken, String replicaReplicationBearerToken, String optionalInstanceType, |
|
| 17 | + Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) { |
|
| 18 | + super(optionalInstanceType, masterReplicationBearerToken, replicaReplicationBearerToken, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull); |
|
| 19 | + this.releaseNameOrNullForLatestMaster = releaseNameOrNullForLatestMaster; |
|
| 20 | + } |
|
| 21 | + |
|
| 22 | + public String getReleaseNameOrNullForLatestMaster() { |
|
| 23 | + return releaseNameOrNullForLatestMaster; |
|
| 24 | + } |
|
| 25 | + } |
|
| 26 | + |
|
| 27 | + private final SuggestBox releaseNameBox; |
|
| 28 | + |
|
| 29 | + public UpgradeArchiveServerDialog(LandscapeManagementWriteServiceAsync landscapeManagementService, String defaultInstanceTypeName, |
|
| 30 | + Iterable<String> releaseNames, |
|
| 31 | + StringMessages stringMessages, ErrorReporter errorReporter, DialogCallback<UpgradeArchiveServerInstructions> callback) { |
|
| 32 | + super(stringMessages.upgradeArchiveServer(), defaultInstanceTypeName, landscapeManagementService, stringMessages, errorReporter, callback); |
|
| 33 | + releaseNameBox = LandscapeDialogUtil.createReleaseNameBox(stringMessages, releaseNames, this); |
|
| 34 | + } |
|
| 35 | + |
|
| 36 | + @Override |
|
| 37 | + protected Grid getAdditionalWidget() { |
|
| 38 | + final Grid result = super.getAdditionalWidget(); |
|
| 39 | + int row=0; |
|
| 40 | + result.insertRow(row); |
|
| 41 | + result.setWidget(row, 0, new Label(stringMessages.release())); |
|
| 42 | + result.setWidget(row++, 1, releaseNameBox); |
|
| 43 | + return result; |
|
| 44 | + } |
|
| 45 | + |
|
| 46 | + @Override |
|
| 47 | + public FocusWidget getInitialFocusWidget() { |
|
| 48 | + return releaseNameBox.getValueBox(); |
|
| 49 | + } |
|
| 50 | + |
|
| 51 | + @Override |
|
| 52 | + protected UpgradeArchiveServerInstructions getResult() { |
|
| 53 | + return new UpgradeArchiveServerInstructions(LandscapeDialogUtil.getReleaseNameBoxValue(releaseNameBox, stringMessages), getMasterReplicationBearerTokenBox().getValue(), |
|
| 54 | + getReplicaReplicationBearerTokenBox().getValue(), getInstanceTypeListBox().getSelectedValue(), |
|
| 55 | + getMemoryInMegabytesBox().getValue(), getMemoryTotalSizeFactorBox().getValue()); |
|
| 56 | + } |
|
| 57 | +} |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.java
| ... | ... | @@ -102,6 +102,7 @@ com.sap.sse.gwt.adminconsole.StringMessages { |
| 102 | 102 | String successfullyUpgradedApplicationReplicaSet(String name, String version); |
| 103 | 103 | String upgradingApplicationReplicaSetFailed(String name); |
| 104 | 104 | String upgradeApplicationReplicaSet(); |
| 105 | + String upgradeArchiveServer(); |
|
| 105 | 106 | String successfullyArchivedReplicaSet(String name); |
| 106 | 107 | String removeArchivedReplicaSet(); |
| 107 | 108 | String bearerTokenOrNullForApplicationReplicaSetToArchive(String replicaSetName); |
| ... | ... | @@ -179,4 +180,8 @@ com.sap.sse.gwt.adminconsole.StringMessages { |
| 179 | 180 | String privateIp(); |
| 180 | 181 | String igtimiRiotPort(); |
| 181 | 182 | String examplePort(int examplePort); |
| 183 | + String successfullyLaunchedNewArchiveCandidate(String replicaSetName, String releaseName); |
|
| 184 | + String successfullySwitchedToNewArchiveCandidate(String replicaSetName); |
|
| 185 | + String activateArchiveCandidate(); |
|
| 186 | + String reallySwitchToNewArchiveCandidate(); |
|
| 182 | 187 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages.properties
| ... | ... | @@ -91,6 +91,7 @@ latestMasterRelease=Latest master release |
| 91 | 91 | successfullyUpgradedApplicationReplicaSet=Successfully upgraded application replica set {0} to version {1}. |
| 92 | 92 | upgradingApplicationReplicaSetFailed=Upgrading application replica set {0} failed. |
| 93 | 93 | upgradeApplicationReplicaSet=Upgrade application replica set |
| 94 | +upgradeArchiveServer=Upgrade archive server |
|
| 94 | 95 | successfullyArchivedReplicaSet=Successfully archived replica set {0}. |
| 95 | 96 | removeArchivedReplicaSet=Remove archived replica set after successful verification? |
| 96 | 97 | bearerTokenOrNullForApplicationReplicaSetToArchive=Bearer token for application replica set {0} (leave empty for current user) |
| ... | ... | @@ -167,4 +168,8 @@ runOnExisting=Run on an existing, running instance |
| 167 | 168 | publicIp=Public IP address |
| 168 | 169 | privateIp=Private IP address |
| 169 | 170 | igtimiRiotPort=Igtimi Riot Port |
| 170 | -examplePort=e.g., {0} |
|
| ... | ... | \ No newline at end of file |
| 0 | +examplePort=e.g., {0} |
|
| 1 | +successfullyLaunchedNewArchiveCandidate=Successfully launched new {0} candidate with release {1}. You will receive an e-mail when the candidate is ready for spot checks and rotation to production. This can take several hours, depending on the number of events to load. |
|
| 2 | +successfullySwitchedToNewArchiveCandidate=Successfully switched to new {0} server |
|
| 3 | +activateArchiveCandidate=Activate ARCHIVE candidate (must have run an upgrade before and received the success e-mail!) |
|
| 4 | +reallySwitchToNewArchiveCandidate=Really switch to new ARCHIVE candidate? You confirm that you have previously received a success notification from a prior ARCHIVE upgrade request and that you have done a few spot checks to ensure the ARCHIVE candidate looks good. |
|
| ... | ... | \ No newline at end of file |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/client/i18n/StringMessages_de.properties
| ... | ... | @@ -91,6 +91,7 @@ latestMasterRelease=Aktuellste Standard-Version |
| 91 | 91 | successfullyUpgradedApplicationReplicaSet=Anwendungs-Cluster {0} wurde erfolgreich auf Version {1} aktualisiert. |
| 92 | 92 | upgradingApplicationReplicaSetFailed=Aktualisierung des Anwendungs-Clusters {0} ist fehlgeschlagen. |
| 93 | 93 | upgradeApplicationReplicaSet=Anwendungs-Cluster aktualisieren |
| 94 | +ugradeArchvieServer=Archiv-Server aktualisieren |
|
| 94 | 95 | successfullyArchivedReplicaSet=Anwendungs-Cluster {0} erfolgreich archiviert. |
| 95 | 96 | removeArchivedReplicaSet=Anwendungs-Cluster nach dem Archivieren entfernen? |
| 96 | 97 | bearerTokenOrNullForApplicationReplicaSetToArchive=Token für zu archivierendes Anwendungs-Cluster {0} (leer für aktuellen Benutzer) |
| ... | ... | @@ -166,4 +167,8 @@ runOnExisting=Auf anderem laufenden Server ausführen |
| 166 | 167 | publicIp=Öffentlich IP-Adresse |
| 167 | 168 | privateIP=Private IP-Adresse |
| 168 | 169 | igtimiRiotPort=Igtimi Riot Port |
| 169 | -examplePort=z.B. {0} |
|
| ... | ... | \ No newline at end of file |
| 0 | +examplePort=z.B. {0} |
|
| 1 | +successfullyLaunchedNewArchiveCandidate=Neuen Kandidaten für {0} mit Version {1} gestartet. Es erfolgt eine Benachrichtigung per e-Mail. Das kann, je nach Umfang der zu ladenden Daten, etliche Stunden dauern. |
|
| 2 | +successfullySwitchedToNewArchiveCandidate=Erfolgreich auf neuen Server {0} umgeschaltet |
|
| 3 | +activateArchiveCandidate=ARCHIVE-Kandidaten aktivieren (zuvor muss ein Upgrade-Versuch per e-Mail als erfolgreich markiert worden sein!) |
|
| 4 | +reallySwitchToNewArchiveCandidate=Wirklich auf dne neuen ARCHIVE-Kandidaten umschalten? Du bestätigst damit, dass Du eine Erfolgs-Benachrichtigung zu einer vorangegangenen Update-Prozedur für den ARCHIVE-Server erhalten hast und Du Dich wenigstens stichprobenartig von der Korrektheit des neuen Kandidaten überzeugt hast. |
|
| ... | ... | \ No newline at end of file |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/server/LandscapeManagementWriteServiceImpl.java
| ... | ... | @@ -4,6 +4,7 @@ import java.io.IOException; |
| 4 | 4 | import java.net.InetAddress; |
| 5 | 5 | import java.net.MalformedURLException; |
| 6 | 6 | import java.net.URISyntaxException; |
| 7 | +import java.net.URL; |
|
| 7 | 8 | import java.net.UnknownHostException; |
| 8 | 9 | import java.util.ArrayList; |
| 9 | 10 | import java.util.Arrays; |
| ... | ... | @@ -73,6 +74,7 @@ import com.sap.sse.common.Util; |
| 73 | 74 | import com.sap.sse.common.Util.Pair; |
| 74 | 75 | import com.sap.sse.common.Util.Triple; |
| 75 | 76 | import com.sap.sse.gwt.server.ResultCachingProxiedRemoteServiceServlet; |
| 77 | +import com.sap.sse.landscape.DefaultProcessConfigurationVariables; |
|
| 76 | 78 | import com.sap.sse.landscape.Host; |
| 77 | 79 | import com.sap.sse.landscape.Landscape; |
| 78 | 80 | import com.sap.sse.landscape.Release; |
| ... | ... | @@ -99,6 +101,7 @@ import com.sap.sse.landscape.aws.orchestration.CreateDynamicLoadBalancerMapping; |
| 99 | 101 | import com.sap.sse.landscape.aws.orchestration.CreateLoadBalancerMapping; |
| 100 | 102 | import com.sap.sse.landscape.aws.orchestration.StartMongoDBServer; |
| 101 | 103 | import com.sap.sse.landscape.common.shared.SecuredLandscapeTypes; |
| 104 | +import com.sap.sse.landscape.mongodb.Database; |
|
| 102 | 105 | import com.sap.sse.landscape.mongodb.MongoEndpoint; |
| 103 | 106 | import com.sap.sse.landscape.mongodb.MongoProcess; |
| 104 | 107 | import com.sap.sse.landscape.mongodb.MongoProcessInReplicaSet; |
| ... | ... | @@ -295,10 +298,10 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem |
| 295 | 298 | private ReverseProxyDTO convertToReverseProxyDTO(String region, Map<AwsInstance<String>, TargetHealth> healths, |
| 296 | 299 | AwsInstance<String> instance, boolean isDisposable) { |
| 297 | 300 | return new ReverseProxyDTO(instance.getInstanceId(), |
| 298 | - instance.getPrivateAddress().getHostAddress(), instance.getPublicAddress().getHostAddress(), |
|
| 299 | - region, instance.getLaunchTimePoint(), instance.isSharedHost(), |
|
| 300 | - instance.getNameTag(), instance.getImageId(), extractHealth(healths, instance), |
|
| 301 | - isDisposable, new AvailabilityZoneDTO(instance.getAvailabilityZone().getName(), instance.getRegion().getId(), instance.getAvailabilityZone().getId())); |
|
| 301 | + instance.getInstanceType().name(), instance.getPrivateAddress().getHostAddress(), |
|
| 302 | + instance.getPublicAddress().getHostAddress(), region, instance.getLaunchTimePoint(), |
|
| 303 | + instance.isSharedHost(), instance.getNameTag(), instance.getImageId(), |
|
| 304 | + extractHealth(healths, instance), isDisposable, new AvailabilityZoneDTO(instance.getAvailabilityZone().getName(), instance.getRegion().getId(), instance.getAvailabilityZone().getId())); |
|
| 302 | 305 | } |
| 303 | 306 | |
| 304 | 307 | @Override |
| ... | ... | @@ -377,9 +380,13 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem |
| 377 | 380 | } |
| 378 | 381 | |
| 379 | 382 | private AwsInstanceDTO convertToAwsInstanceDTO(Host host) { |
| 380 | - return new AwsInstanceDTO(host.getId().toString(), host.getPrivateAddress().getHostAddress(), host.getPublicAddress() == null ? null : host.getPublicAddress().getHostAddress(), |
|
| 381 | - host.getRegion().getId(), |
|
| 382 | - host.getLaunchTimePoint(), host.isSharedHost(), new AvailabilityZoneDTO(host.getAvailabilityZone().getName(), host.getRegion().getId(), host.getAvailabilityZone().getId())); |
|
| 383 | + return new AwsInstanceDTO(host.getId().toString(), |
|
| 384 | + (host instanceof AwsInstance<?>) ? ((AwsInstance<?>) host).getInstanceType().name() : null, |
|
| 385 | + host.getPrivateAddress().getHostAddress(), |
|
| 386 | + host.getPublicAddress() == null ? null : host.getPublicAddress().getHostAddress(), |
|
| 387 | + host.getRegion().getId(), host.getLaunchTimePoint(), host.isSharedHost(), |
|
| 388 | + new AvailabilityZoneDTO(host.getAvailabilityZone().getName(), host.getRegion().getId(), |
|
| 389 | + host.getAvailabilityZone().getId())); |
|
| 383 | 390 | } |
| 384 | 391 | |
| 385 | 392 | @Override |
| ... | ... | @@ -676,6 +683,46 @@ public class LandscapeManagementWriteServiceImpl extends ResultCachingProxiedRem |
| 676 | 683 | optionalMemoryTotalSizeFactorOrNull, optionalIgtimiRiotPort, optionalPreferredInstanceToDeployUnmanagedReplicaTo); |
| 677 | 684 | } |
| 678 | 685 | |
| 686 | + @Override |
|
| 687 | + public void createArchiveReplicaSet(String regionId, SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade, |
|
| 688 | + String instanceType, String releaseNameOrNullForLatestMaster, String optionalKeyName, |
|
| 689 | + byte[] privateKeyEncryptionPassphrase, String securityReplicationBearerToken, |
|
| 690 | + String replicaReplicationBearerToken, Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull) throws Exception { |
|
| 691 | + checkLandscapeManageAwsPermission(); |
|
| 692 | + final String userSetOrArchiveServerSecurityReplicationBearerToken; |
|
| 693 | + final AwsRegion region = new AwsRegion(regionId, getLandscape()); |
|
| 694 | + final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> awsReplicaSet = |
|
| 695 | + convertFromApplicationReplicaSetDTO(region, archiveReplicaSetToUpgrade, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 696 | + final SailingAnalyticsProcess<String> master = awsReplicaSet.getMaster(); |
|
| 697 | + if (Util.hasLength(securityReplicationBearerToken)) { |
|
| 698 | + userSetOrArchiveServerSecurityReplicationBearerToken = securityReplicationBearerToken; |
|
| 699 | + } else { |
|
| 700 | + userSetOrArchiveServerSecurityReplicationBearerToken = master.getEnvShValueFor( |
|
| 701 | + DefaultProcessConfigurationVariables.REPLICATE_MASTER_BEARER_TOKEN, |
|
| 702 | + Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.of(optionalKeyName), privateKeyEncryptionPassphrase); |
|
| 703 | + } |
|
| 704 | + final String replicaSetName = SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME; |
|
| 705 | + final String domainName = AwsLandscape.getHostedZoneName(archiveReplicaSetToUpgrade.getHostname()); |
|
| 706 | + final Database databaseConfiguration = master.getDatabaseConfiguration(region, |
|
| 707 | + Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), |
|
| 708 | + privateKeyEncryptionPassphrase); |
|
| 709 | + final URL requestURL = new URL(getThreadLocalRequest().getRequestURL().toString()); |
|
| 710 | + final URL continuationBaseURL = new URL(requestURL.getProtocol(), requestURL.getHost(), requestURL.getPort(), "/"); |
|
| 711 | + getLandscapeService() |
|
| 712 | + .createArchiveReplicaSet(regionId, replicaSetName, instanceType, releaseNameOrNullForLatestMaster, |
|
| 713 | + databaseConfiguration, optionalKeyName, privateKeyEncryptionPassphrase, |
|
| 714 | + replicaReplicationBearerToken, domainName, optionalMemoryInMegabytesOrNull, |
|
| 715 | + userSetOrArchiveServerSecurityReplicationBearerToken, optionalMemoryTotalSizeFactorOrNull, |
|
| 716 | + /* optionalIgtimiRiotPort */ null, continuationBaseURL); |
|
| 717 | + } |
|
| 718 | + |
|
| 719 | + @Override |
|
| 720 | + public void makeCandidateArchiveServerGoLive(String regionId, SailingApplicationReplicaSetDTO<String> archiveReplicaSetToUpgrade, |
|
| 721 | + String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 722 | + final String domainName = AwsLandscape.getHostedZoneName(archiveReplicaSetToUpgrade.getHostname()); |
|
| 723 | + getLandscapeService().makeCandidateArchiveServerGoLive(regionId, optionalKeyName, privateKeyEncryptionPassphrase, domainName); |
|
| 724 | + } |
|
| 725 | + |
|
| 679 | 726 | /** |
| 680 | 727 | * Starts a first master process of a new replica set whose name is provided by the {@code replicaSetName} |
| 681 | 728 | * parameter. The process is started on the host identified by the {@code hostToDeployTo} parameter. A set of |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/AwsInstanceDTO.java
| ... | ... | @@ -5,6 +5,7 @@ import com.sap.sse.common.TimePoint; |
| 5 | 5 | |
| 6 | 6 | public class AwsInstanceDTO implements IsSerializable { |
| 7 | 7 | private String instanceId; |
| 8 | + private String instanceType; |
|
| 8 | 9 | private AvailabilityZoneDTO availabilityZoneDTO; |
| 9 | 10 | private String privateIpAddress; |
| 10 | 11 | private String publicIpAddress; |
| ... | ... | @@ -14,9 +15,10 @@ public class AwsInstanceDTO implements IsSerializable { |
| 14 | 15 | @Deprecated |
| 15 | 16 | AwsInstanceDTO() {} // for GWT RPC serialization only |
| 16 | 17 | |
| 17 | - public AwsInstanceDTO(String instanceId, String privateIpAddress, String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, AvailabilityZoneDTO azDTO) { |
|
| 18 | + public AwsInstanceDTO(String instanceId, String instanceType, String privateIpAddress, String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, AvailabilityZoneDTO azDTO) { |
|
| 18 | 19 | super(); |
| 19 | 20 | this.instanceId = instanceId; |
| 21 | + this.instanceType = instanceType; |
|
| 20 | 22 | this.availabilityZoneDTO = azDTO; |
| 21 | 23 | this.privateIpAddress = privateIpAddress; |
| 22 | 24 | this.publicIpAddress = publicIpAddress; |
| ... | ... | @@ -27,10 +29,12 @@ public class AwsInstanceDTO implements IsSerializable { |
| 27 | 29 | public String getAvailabilityZoneId() { |
| 28 | 30 | return availabilityZoneDTO.getAzId(); |
| 29 | 31 | } |
| 32 | + public String getInstanceType() { |
|
| 33 | + return instanceType; |
|
| 34 | + } |
|
| 30 | 35 | public String getInstanceId() { |
| 31 | 36 | return instanceId; |
| 32 | 37 | } |
| 33 | - |
|
| 34 | 38 | public AvailabilityZoneDTO getAvailabilityZoneDTO() { |
| 35 | 39 | return availabilityZoneDTO; |
| 36 | 40 | } |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/ReverseProxyDTO.java
| ... | ... | @@ -14,10 +14,10 @@ public class ReverseProxyDTO extends AwsInstanceDTO implements Named { |
| 14 | 14 | private String health; |
| 15 | 15 | private boolean isDisposable = false; |
| 16 | 16 | |
| 17 | - public ReverseProxyDTO(String instanceId, String privateIpAddress, String publicIpAddress, |
|
| 18 | - String region, TimePoint launchTimePoint, boolean shared, String name, String imageId, String healthInTargetGroup, |
|
| 19 | - boolean isDisposable, AvailabilityZoneDTO availabilityZoneDTO) { |
|
| 20 | - super(instanceId, privateIpAddress, publicIpAddress, region, launchTimePoint, shared, availabilityZoneDTO); |
|
| 17 | + public ReverseProxyDTO(String instanceId, String instanceType, String privateIpAddress, |
|
| 18 | + String publicIpAddress, String region, TimePoint launchTimePoint, boolean shared, String name, String imageId, |
|
| 19 | + String healthInTargetGroup, boolean isDisposable, AvailabilityZoneDTO availabilityZoneDTO) { |
|
| 20 | + super(instanceId, instanceType, privateIpAddress, publicIpAddress, region, launchTimePoint, shared, availabilityZoneDTO); |
|
| 21 | 21 | this.name = name; |
| 22 | 22 | this.amiId = imageId; |
| 23 | 23 | this.health = healthInTargetGroup; |
java/com.sap.sailing.landscape.ui/src/com/sap/sailing/landscape/ui/shared/SailingApplicationReplicaSetDTO.java
| ... | ... | @@ -18,6 +18,9 @@ public class SailingApplicationReplicaSetDTO<ShardingKey> implements Named, IsSe |
| 18 | 18 | private String version; |
| 19 | 19 | private String releaseNotesLink; |
| 20 | 20 | private String hostname; |
| 21 | + /** |
|
| 22 | + * Originates from load balancer rule and therefore it can be null of not managed by load balancer rule |
|
| 23 | + */ |
|
| 21 | 24 | private String defaultRedirectPath; |
| 22 | 25 | private String autoScalingGroupAmiId; |
| 23 | 26 | |
| ... | ... | @@ -74,6 +77,9 @@ public class SailingApplicationReplicaSetDTO<ShardingKey> implements Named, IsSe |
| 74 | 77 | return hostname; |
| 75 | 78 | } |
| 76 | 79 | |
| 80 | + /** |
|
| 81 | + * @return may be null if not managed by load balancer rule |
|
| 82 | + */ |
|
| 77 | 83 | public String getDefaultRedirectPath() { |
| 78 | 84 | return defaultRedirectPath; |
| 79 | 85 | } |
java/com.sap.sailing.landscape/resources/stringmessages/SailingLandscape_StringMessages.properties
| ... | ... | @@ -1,12 +1,20 @@ |
| 1 | 1 | MasterUnavailableMailSubject=Primary server of replica set {0} temporarily unavailable |
| 2 | -MasterUnavailableMailBody=The primary server of replica set {0} is temporarily unavailable.\nModifying access will be suspended.\nModifications incurred by a replica will be queued and will be applied when the primary is available again.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore. |
|
| 2 | +MasterUnavailableMailBody=The primary server of replica set {0} is temporarily unavailable.\nModifying access will be suspended.\nModifications incurred by a replica will be queued and will be applied when the primary is available again.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore. |
|
| 3 | 3 | MasterAvailableMailSubject=Primary server of replica set {0} available again |
| 4 | 4 | MasterAvailableMailBody=The primary server of replica set {0} is available again.\nModifying access is possible again.\nQueued modifications will now process.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore. |
| 5 | +StartingNewArchiveCandidateSubject=Starting new {0} candidate |
|
| 6 | +StartingNewArchiveCandidateBody=Starting new {0} candidate.\nFirst, archived content is loaded from the MongoDB; afterwards wind estimations and mark passings are calculated.\nThe whole process can take up to two days.\nYou will receive another e-mail when this phase has finished.\nAlso check the status of the candidate at https://archive-canidate.sapsailing.com/gwt/status |
|
| 7 | +NewArchiveServerLiveSubject=New {0} server is live |
|
| 8 | +NewArchiveServerLiveBody=New {0} server is live.\nYou can now archive content to {0}.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore. |
|
| 5 | 9 | StartingToArchiveReplicaSetIntoSubject=Starting to archive a replica set to {0} |
| 6 | -StartingToArchiveReplicaSetIntoBody=Starting to archive a replica set to {0}.\nWhile this is going on, refrain from archiving another replica set into the same archive {0}.\nAfter the content has been imported it will be compared to the original.\nYou should receive another e-mail when this has completed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore. |
|
| 10 | +StartingToArchiveReplicaSetIntoBody=Starting to archive a replica set to {0}.\nWhile this is going on, refrain from archiving another replica set into the same archive {0}.\nAfter the content has been imported it will be compared to the original.\nYou should receive another e-mail when this has completed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore. |
|
| 7 | 11 | StartingToArchiveReplicaSetSubject=Starting to archive replica set {0} |
| 8 | -StartingToArchiveReplicaSetBody=Starting to archive a replica set {0}.\nWhile this is ongoing please do not make any modifications to {0}.\nYou will receive another e-mail when this process has finished.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore. |
|
| 12 | +StartingToArchiveReplicaSetBody=Starting to archive a replica set {0}.\nWhile this is ongoing please do not make any modifications to {0}.\nYou will receive another e-mail when this process has finished.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore. |
|
| 9 | 13 | FinishedToArchiveReplicaSetIntoSubject=Archiving a replica set to {0} finished |
| 10 | -FinishedToArchiveReplicaSetIntoBody=Archiving a replica set to {0} has finished.\nYou can now archive other content to {0} if you want.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore. |
|
| 14 | +FinishedToArchiveReplicaSetIntoBody=Archiving a replica set to {0} has finished.\nYou can now archive other content to {0} if you want.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore. |
|
| 11 | 15 | FinishedToArchiveReplicaSetSubject=Archiving replica set {0} finished |
| 12 | -FinishedToArchiveReplicaSetBody=Archiving replica set {0} has finished.\nIf you requested so, the original replica set has been removed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace: if you do not like to receive these messages anymore. |
|
| ... | ... | \ No newline at end of file |
| 0 | +FinishedToArchiveReplicaSetBody=Archiving replica set {0} has finished.\nIf you requested so, the original replica set has been removed.\n\nYou are receiving this mail because you have administrative permissions for {0}.\nRemove those permissions at <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:> if you do not like to receive these messages anymore. |
|
| 1 | +NewArchiveCandidateReadyForSpotChecksAndRotationSubject=The new {0} candidate is ready for spot checks |
|
| 2 | +NewArchiveCandidateReadyForSpotChecksAndRotationBody=We''ve run the following checks:\n{3}.\nThe new {0} candidate is ready for spot checks and, if OK, rotation to become the new production {0}.\nRun your spot checks at <https://{1}/gwt/Home.html#EventsPlace:> and compare to <https://{2}/gwt/Home.html#EventsPlace:>.\nStart the rotation to the new production server at <{4}/gwt/AdminConsole.html#LandscapeManagementPlace:> after successful checks. |
|
| 3 | +NewArchiveCandidateFailedSubject=Check {1} failed during {0} startup |
|
| 4 | +NewArchiveCandidateFailedBody=The check "{1}" failed with message "{2}" while starting up {0}. Please check the landscape and fix manually. |
|
| ... | ... | \ No newline at end of file |
java/com.sap.sailing.landscape/resources/stringmessages/SailingLandscape_StringMessages_de.properties
| ... | ... | @@ -1,12 +1,20 @@ |
| 1 | 1 | MasterUnavailableMailSubject=Primär-Server des Anwendungs-Clusters {0} vorübergehend nicht verfügbar |
| 2 | -MasterUnavailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist vorübergehend nicht verfügbar.\nVerändernde Zugriffe sind derzeit nicht möglich.\nModifikationen, die durch eine Replika ausgeüfhrt werden, werden gepuffert und werden angewandt, wenn der primäre Server wieder verfügbar ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 2 | +MasterUnavailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist vorübergehend nicht verfügbar.\nVerändernde Zugriffe sind derzeit nicht möglich.\nModifikationen, die durch eine Replika ausgeüfhrt werden, werden gepuffert und werden angewandt, wenn der primäre Server wieder verfügbar ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 3 | 3 | MasterAvailableMailSubject=Primär-Server des Anwendungs-Clusters {0} wieder verfügbar |
| 4 | -MasterAvailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist wieder verfügbar.\nVerändernde Zugriffe sind jetzt wieder möglich.\nGepufferte Modifikationen werden jetzt abgearbeitet.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 4 | +MasterAvailableMailBody=Der Primär-Server des Anwendungs-Clusters {0} ist wieder verfügbar.\nVerändernde Zugriffe sind jetzt wieder möglich.\nGepufferte Modifikationen werden jetzt abgearbeitet.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 5 | +StartingNewArchiveCandidateSubject=Neuer {0}-Kandidat wird gestartet |
|
| 6 | +StartingNewArchiveCandidateBody=Neuer {0}-Kandidat wird gestartet.\nZuerst werden dabei die archivierten Veranstaltungen von der Datenbank geladen. Danach werden Manöver, Windschätzungen und Zwischenzeiten berechnet.\nDieser Prozess kann bis zu zwei vollen Tagen dauern.\nEine weitere e-Mail benachrichtigt über den Abschluss dieser Phase.\nUnter https://archive-canidate.sapsailing.com/gwt/status kann der Status des Kandidaten verfolgt werden. |
|
| 7 | +NewArchiveServerLiveSubject=Neuer {0}-Server ist live |
|
| 8 | +NewArchiveServerLiveBody=Neuer {0}-Server ist live.\nAb jetzt dürfen wieder Anwendungs-Cluster nach {0} archiviert werden.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 5 | 9 | StartingToArchiveReplicaSetIntoSubject=Anwendungs-Cluster wird nach {0} archiviert |
| 6 | -StartingToArchiveReplicaSetIntoBody=Ein Anwendungs-Cluster wird nach {0} archiviert.\nWährend dieser Vorgang läuft, dürfen keine weitere Anwendungs-Cluster nach {0} archiviert werden.\nNach der Archivierung werden die Inhalte verglichen.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 10 | +StartingToArchiveReplicaSetIntoBody=Ein Anwendungs-Cluster wird nach {0} archiviert.\nWährend dieser Vorgang läuft, dürfen keine weitere Anwendungs-Cluster nach {0} archiviert werden.\nNach der Archivierung werden die Inhalte verglichen.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 7 | 11 | StartingToArchiveReplicaSetSubject=Anwendungs-Cluster {0} wird archiviert |
| 8 | -StartingToArchiveReplicaSetBody=Das Anwendungs-Cluster {0} wird jetzt archiviert.\nWährenddessen dürfen keine Veränderungen an {0} vorgenommen werden.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 12 | +StartingToArchiveReplicaSetBody=Das Anwendungs-Cluster {0} wird jetzt archiviert.\nWährenddessen dürfen keine Veränderungen an {0} vorgenommen werden.\nEine e-Mail folgt, wenn der Vorgang abgeschlossen ist.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 9 | 13 | FinishedToArchiveReplicaSetIntoSubject=Archivierung des Anwendungs-Clusters nach {0} beendet |
| 10 | -FinishedToArchiveReplicaSetIntoBody=Die Archivierung des Anwendungs-Clusters nach {0} ist beendet.\nAb jetzt sind bei Bedarf weitere Archivierungen nach {0} möglich.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 14 | +FinishedToArchiveReplicaSetIntoBody=Die Archivierung des Anwendungs-Clusters nach {0} ist beendet.\nAb jetzt sind bei Bedarf weitere Archivierungen nach {0} möglich.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 11 | 15 | FinishedToArchiveReplicaSetSubject=Archivierung des Anwendungs-Clusters {0} beendet |
| 12 | -FinishedToArchiveReplicaSetBody=Die Archivierung des Anwendungs-Clusters {0} ist beendet.\nFalls angefragt, wurde das Original Anwendungs-Cluster entfernt.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| ... | ... | \ No newline at end of file |
| 0 | +FinishedToArchiveReplicaSetBody=Die Archivierung des Anwendungs-Clusters {0} ist beendet.\nFalls angefragt, wurde das Original Anwendungs-Cluster entfernt.\n\nDiese Nachricht wurde versandt, weil Du über administrative Rechte für {0} verfügst.\nUm das zu ändern, besuche <https://sapsailing.com/gwt/AdminConsole.html#UserManagementPlace:>, um diese Rechte für Dein Benutzerkonto zu entfernen. |
|
| 1 | +NewArchiveCandidateReadyForSpotChecksAndRotationSubject=Der neue {0} Kandidat ist bereit für einen stichprobenartigen Vergleich |
|
| 2 | +NewArchiveCandidateReadyForSpotChecksAndRotationBody=Es wurden die folgenden Prüfungen durchgeführt:\n{3}.\nDer neue {0} Kandidat ist bereit für einen stichprobenartigen Vergleich\nund, falls OK, Rotation zum neuen Produktiv-Server für {0}.\nStichprobenartiger Vergleich unter https://{1}/gwt/Home.html#EventsPlace: und https://{2}/gwt/Home.html#EventsPlace:.\nRotation nach erfolgreichen Prüfungen unter <{4}/gwt/AdminConsole.html#LandscapeManagementPlace:> starten. |
|
| 3 | +NewArchiveCandidateFailedSubject=Überprüfung {1} beim Start von {0} fehlgeschlagen |
|
| 4 | +NewArchiveCandidateFailedBody=Die Überprüfung "{1}" zum Start von {0} ist mit der Meldung "{2}" fehlgeschlagen. Bitte die Landschaft inspizieren und manuell korrigieren. |
|
| ... | ... | \ No newline at end of file |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/LandscapeService.java
| ... | ... | @@ -2,6 +2,7 @@ package com.sap.sailing.landscape; |
| 2 | 2 | |
| 3 | 3 | import java.io.IOException; |
| 4 | 4 | import java.net.MalformedURLException; |
| 5 | +import java.net.URL; |
|
| 5 | 6 | import java.util.Map; |
| 6 | 7 | import java.util.Optional; |
| 7 | 8 | import java.util.concurrent.ExecutionException; |
| ... | ... | @@ -17,8 +18,10 @@ import com.sap.sailing.landscape.procedures.SailingProcessConfigurationVariables |
| 17 | 18 | import com.sap.sailing.landscape.procedures.StartMultiServer; |
| 18 | 19 | import com.sap.sailing.server.gateway.interfaces.CompareServersResult; |
| 19 | 20 | import com.sap.sailing.server.gateway.interfaces.SailingServer; |
| 21 | +import com.sap.sailing.server.gateway.interfaces.SailingServerFactory; |
|
| 20 | 22 | import com.sap.sse.common.Duration; |
| 21 | 23 | import com.sap.sse.common.Util.Triple; |
| 24 | +import com.sap.sse.common.mail.MailException; |
|
| 22 | 25 | import com.sap.sse.landscape.Release; |
| 23 | 26 | import com.sap.sse.landscape.application.ApplicationReplicaSet; |
| 24 | 27 | import com.sap.sse.landscape.aws.AmazonMachineImage; |
| ... | ... | @@ -26,8 +29,11 @@ import com.sap.sse.landscape.aws.AwsApplicationReplicaSet; |
| 26 | 29 | import com.sap.sse.landscape.aws.AwsAvailabilityZone; |
| 27 | 30 | import com.sap.sse.landscape.aws.AwsLandscape; |
| 28 | 31 | import com.sap.sse.landscape.aws.impl.AwsRegion; |
| 32 | +import com.sap.sse.landscape.mongodb.Database; |
|
| 29 | 33 | import com.sap.sse.landscape.mongodb.MongoEndpoint; |
| 30 | 34 | import com.sap.sse.security.SecurityService; |
| 35 | +import com.sap.sse.security.shared.HasPermissions.Action; |
|
| 36 | +import com.sap.sse.security.shared.impl.User; |
|
| 31 | 37 | |
| 32 | 38 | import software.amazon.awssdk.services.autoscaling.model.AutoScalingGroup; |
| 33 | 39 | import software.amazon.awssdk.services.ec2.model.InstanceType; |
| ... | ... | @@ -151,6 +157,35 @@ public interface LandscapeService { |
| 151 | 157 | Integer optionalMemoryInMegabytesOrNull, Integer optionalMemoryTotalSizeFactorOrNull, |
| 152 | 158 | Integer optionalIgtimiRiotPort, Optional<Integer> minimumAutoScalingGroupSize, Optional<Integer> maximumAutoScalingGroupSize) |
| 153 | 159 | throws Exception; |
| 160 | + |
|
| 161 | + /** |
|
| 162 | + * Runs phase 1 of an ARCHIVE server upgrade. This includes launching the new instance in a favorable availability |
|
| 163 | + * zone where ideally we have a reverse proxy and that ideally is different from the AZ in which the current |
|
| 164 | + * production ARCHIVE server runs. It then installs a {@link ArchiveCandidateMonitoringBackgroundTask background |
|
| 165 | + * task} that keeps applying a sequence of checks. When any of the checks keeps failing beyond a timeout, the |
|
| 166 | + * activity is aborted, and the user who triggered it receives an e-mail about this. If all checks pass, the user |
|
| 167 | + * receives an e-mail that asks for manual spot checks and a confirmation about the rotation. A link embedded in the |
|
| 168 | + * e-mail grants the user easy access to the |
|
| 169 | + * {@link #makeCandidateArchiveServerGoLive(String, String, byte[], String)} method which then performs phase 2. |
|
| 170 | + * |
|
| 171 | + * @param continuationBaseURL |
|
| 172 | + * the base URL to which to direct the user for continuation of the ARCHIVE upgrade process (phase 2) |
|
| 173 | + * after this first phase has completed successfully |
|
| 174 | + */ |
|
| 175 | + void createArchiveReplicaSet( |
|
| 176 | + String regionId, String name, String instanceType, String releaseNameOrNullForLatestMaster, Database databaseConfiguration, |
|
| 177 | + String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicaReplicationBearerToken, |
|
| 178 | + String optionalDomainName, Integer optionalMemoryInMegabytesOrNull, String securityServiceReplicationBearerToken, |
|
| 179 | + Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, URL continuationBaseURL) throws Exception; |
|
| 180 | + |
|
| 181 | + /** |
|
| 182 | + * Phase 2 of an ARCHIVE server upgrade. This is to be triggered ideally after a "human in the loop" step |
|
| 183 | + * where a user makes some spot checks and then confirms that the archive candidate can be installed as the |
|
| 184 | + * new production server, with the previous production server then becoming the failover, and the old failover |
|
| 185 | + * instance being terminated. |
|
| 186 | + */ |
|
| 187 | + void makeCandidateArchiveServerGoLive(String regionId, String optionalKeyName, |
|
| 188 | + byte[] privateKeyEncryptionPassphrase, String optionalDomainName) throws Exception; |
|
| 154 | 189 | |
| 155 | 190 | /** |
| 156 | 191 | * Starts a first master process of a new replica set whose name is provided by the {@code replicaSetName} |
| ... | ... | @@ -485,4 +520,27 @@ public interface LandscapeService { |
| 485 | 520 | String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
| 486 | 521 | |
| 487 | 522 | String getHostname(String replicaSetName, String optionalDomainName); |
| 523 | + |
|
| 524 | + /** |
|
| 525 | + * @param subjectMessageKey |
|
| 526 | + * must have a single placeholder argument representing the name of the replica set |
|
| 527 | + * @param bodyMessageKey |
|
| 528 | + * must have a single placeholder argument representing the name of the replica set |
|
| 529 | + * @param alsoSendToAllUsersWithThisPermissionOnReplicaSet |
|
| 530 | + * when not empty, all users that have permission to this {@link SecuredSecurityTypes#SERVER SERVER} |
|
| 531 | + * action on the {@code replicaSet} will receive the e-mail in addition to the server owner. No user |
|
| 532 | + * will receive the e-mail twice. |
|
| 533 | + */ |
|
| 534 | + void sendMailToReplicaSetOwner( |
|
| 535 | + AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet, |
|
| 536 | + String subjectMessageKey, String bodyMessageKey, |
|
| 537 | + Optional<Action> alsoSendToAllUsersWithThisPermissionOnReplicaSet) throws MailException; |
|
| 538 | + |
|
| 539 | + void sendMailToCurrentUser(String messageSubjectKey, String messageBodyKey, String... messageParameters) |
|
| 540 | + throws MailException; |
|
| 541 | + |
|
| 542 | + void sendMailToUser(User user, String messageSubjectKey, String messageBodyKey, String... messageParameters) |
|
| 543 | + throws MailException; |
|
| 544 | + |
|
| 545 | + SailingServerFactory getSailingServerFactory(); |
|
| 488 | 546 | } |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/SailingAnalyticsProcess.java
| ... | ... | @@ -2,6 +2,7 @@ package com.sap.sailing.landscape; |
| 2 | 2 | |
| 3 | 3 | import java.io.IOException; |
| 4 | 4 | import java.util.Optional; |
| 5 | +import java.util.concurrent.TimeoutException; |
|
| 5 | 6 | import java.util.logging.Logger; |
| 6 | 7 | |
| 7 | 8 | import com.jcraft.jsch.JSchException; |
| ... | ... | @@ -9,6 +10,7 @@ import com.sap.sailing.landscape.procedures.SailingProcessConfigurationVariables |
| 9 | 10 | import com.sap.sse.common.Duration; |
| 10 | 11 | import com.sap.sse.landscape.Release; |
| 11 | 12 | import com.sap.sse.landscape.aws.AwsApplicationProcess; |
| 13 | +import com.sap.sse.util.ThreadPoolUtil; |
|
| 12 | 14 | |
| 13 | 15 | public interface SailingAnalyticsProcess<ShardingKey> extends AwsApplicationProcess<ShardingKey, SailingAnalyticsMetrics, SailingAnalyticsProcess<ShardingKey>> { |
| 14 | 16 | static Logger logger = Logger.getLogger(SailingAnalyticsProcess.class.getName()); |
| ... | ... | @@ -36,4 +38,42 @@ public interface SailingAnalyticsProcess<ShardingKey> extends AwsApplicationProc |
| 36 | 38 | */ |
| 37 | 39 | void refreshToRelease(Release release, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) |
| 38 | 40 | throws IOException, InterruptedException, JSchException, Exception; |
| 41 | + |
|
| 42 | + double getLastMinuteSystemLoadAverage(Optional<Duration> optionalTimeout) throws TimeoutException, Exception; |
|
| 43 | + |
|
| 44 | + /** |
|
| 45 | + * The count of <em>all</em> tasks in the default background thread pool executor's queue, including those scheduled |
|
| 46 | + * with a delay, such as rate limiter and cache clean-up tasks or periodic fetching of remote server content. |
|
| 47 | + * |
|
| 48 | + * @see ThreadPoolUtil#getDefaultBackgroundTaskThreadPoolExecutor() |
|
| 49 | + */ |
|
| 50 | + int getDefaultBackgroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception; |
|
| 51 | + |
|
| 52 | + /** |
|
| 53 | + * The count of <em>all</em> tasks in the default foreground thread pool executor's queue, including those scheduled |
|
| 54 | + * with a delay, such as rate limiter and cache clean-up tasks or periodic fetching of remote server content. |
|
| 55 | + * |
|
| 56 | + * @see ThreadPoolUtil#getDefaultForegroundTaskThreadPoolExecutor() |
|
| 57 | + */ |
|
| 58 | + int getDefaultForegroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception; |
|
| 59 | + |
|
| 60 | + /** |
|
| 61 | + * The count of only those tasks in the default background thread pool executor's queue that have no scheduling |
|
| 62 | + * delay or a delay below {@link Duration#ONE_SECOND one second}. This helps exclude from the count those rate |
|
| 63 | + * limiter and cache clean-up tasks or periodic fetching of remote server content. It is as such as good approximation |
|
| 64 | + * of the "immediate" tasks that will keep the server busy in the immediate future. |
|
| 65 | + * |
|
| 66 | + * @see ThreadPoolUtil#getDefaultBackgroundTaskThreadPoolExecutor() |
|
| 67 | + */ |
|
| 68 | + int getDefaultBackgroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception; |
|
| 69 | + |
|
| 70 | + /** |
|
| 71 | + * The count of only those tasks in the default foreground thread pool executor's queue that have no scheduling |
|
| 72 | + * delay or a delay below {@link Duration#ONE_SECOND one second}. This helps exclude from the count those rate |
|
| 73 | + * limiter and cache clean-up tasks or periodic fetching of remote server content. It is as such as good approximation |
|
| 74 | + * of the "immediate" tasks that will keep the server busy in the immediate future. |
|
| 75 | + * |
|
| 76 | + * @see ThreadPoolUtil#getDefaultForegroundTaskThreadPoolExecutor() |
|
| 77 | + */ |
|
| 78 | + int getDefaultForegroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception; |
|
| 39 | 79 | } |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/ArchiveCandidateMonitoringBackgroundTask.java
| ... | ... | @@ -0,0 +1,327 @@ |
| 1 | +package com.sap.sailing.landscape.impl; |
|
| 2 | + |
|
| 3 | +import java.net.URL; |
|
| 4 | +import java.util.Arrays; |
|
| 5 | +import java.util.Iterator; |
|
| 6 | +import java.util.Optional; |
|
| 7 | +import java.util.concurrent.ExecutionException; |
|
| 8 | +import java.util.concurrent.ScheduledExecutorService; |
|
| 9 | +import java.util.concurrent.TimeUnit; |
|
| 10 | +import java.util.logging.Logger; |
|
| 11 | + |
|
| 12 | +import com.sap.sailing.landscape.LandscapeService; |
|
| 13 | +import com.sap.sailing.landscape.SailingAnalyticsMetrics; |
|
| 14 | +import com.sap.sailing.landscape.SailingAnalyticsProcess; |
|
| 15 | +import com.sap.sailing.server.gateway.interfaces.CompareServersResult; |
|
| 16 | +import com.sap.sailing.server.gateway.interfaces.SailingServer; |
|
| 17 | +import com.sap.sse.common.Duration; |
|
| 18 | +import com.sap.sse.common.Named; |
|
| 19 | +import com.sap.sse.common.TimePoint; |
|
| 20 | +import com.sap.sse.common.Util; |
|
| 21 | +import com.sap.sse.common.impl.NamedImpl; |
|
| 22 | +import com.sap.sse.common.mail.MailException; |
|
| 23 | +import com.sap.sse.landscape.Landscape; |
|
| 24 | +import com.sap.sse.landscape.aws.AwsApplicationReplicaSet; |
|
| 25 | +import com.sap.sse.security.shared.impl.User; |
|
| 26 | + |
|
| 27 | +/** |
|
| 28 | + * A stateful monitoring task that can be {@link #run run} to observe an {@code ARCHIVE} candidate process and wait for |
|
| 29 | + * it to be ready for comparing its contents with a production {@code ARCHIVE} instance. When run for the first time, |
|
| 30 | + * the task check the {@code /gwt/status} end point on {@code candidateHostname} to see whether it is already serving |
|
| 31 | + * requests. If not, it will re-schedule itself after some delay to check again until either the candidate becomes |
|
| 32 | + * healthy or a timeout is reached. |
|
| 33 | + * <p> |
|
| 34 | + * |
|
| 35 | + * If the candidate was seen serving a {@code /gwt/status} request, this task changes state and now looks at the |
|
| 36 | + * contents of the status response. Four conditions must be fulfilled for the candidate to be considered ready for |
|
| 37 | + * comparison: |
|
| 38 | + * |
|
| 39 | + * <ol> |
|
| 40 | + * <li>the overall status must be {@code available: true}.</li> |
|
| 41 | + * <li>the one-minute system load average must be below 2 (per cent)</li> |
|
| 42 | + * <li>the default foreground thread pool queue must contain less than 10 tasks</li> |
|
| 43 | + * <li>the default background thread pool queue must contain less than 10 tasks</li> |
|
| 44 | + * <li>the old and new ARCHIVE must compare equal with the {@link SailingServer#compareServers(Optional, SailingServer, Optional)} method</li> |
|
| 45 | + * </ol> |
|
| 46 | + * |
|
| 47 | + * When any of these conditions is not fulfilled, the task will re-schedule itself after some delay to check again until |
|
| 48 | + * either the candidate fulfills all conditions or a timeout is reached. |
|
| 49 | + * <p> |
|
| 50 | + * |
|
| 51 | + * @author Axel Uhl (d043530) |
|
| 52 | + * |
|
| 53 | + */ |
|
| 54 | +public class ArchiveCandidateMonitoringBackgroundTask implements Runnable { |
|
| 55 | + private interface Check extends Named { |
|
| 56 | + boolean runCheck() throws Exception; |
|
| 57 | + void setLastFailureMessage(String lastFailureMessage); |
|
| 58 | + boolean hasTimedOut(); |
|
| 59 | + Duration getDelayAfterFailure(); |
|
| 60 | + String getLastFailureMessage(); |
|
| 61 | + } |
|
| 62 | + |
|
| 63 | + private abstract class AbstractCheck extends NamedImpl implements Check { |
|
| 64 | + private static final long serialVersionUID = -8809199091635882129L; |
|
| 65 | + private final TimePoint creationTime; |
|
| 66 | + private final Duration timeout; |
|
| 67 | + private final Duration delayAfterFailure; |
|
| 68 | + private String lastFailureMessage; |
|
| 69 | + |
|
| 70 | + public AbstractCheck(String name, Duration timeout, Duration delayAfterFailure) { |
|
| 71 | + super(name); |
|
| 72 | + this.creationTime = TimePoint.now(); |
|
| 73 | + this.timeout = timeout; |
|
| 74 | + this.delayAfterFailure = delayAfterFailure; |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + @Override |
|
| 78 | + public boolean hasTimedOut() { |
|
| 79 | + return creationTime.until(TimePoint.now()).compareTo(timeout) > 0; |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + @Override |
|
| 83 | + public Duration getDelayAfterFailure() { |
|
| 84 | + return delayAfterFailure; |
|
| 85 | + } |
|
| 86 | + |
|
| 87 | + @Override |
|
| 88 | + public String getLastFailureMessage() { |
|
| 89 | + return lastFailureMessage; |
|
| 90 | + } |
|
| 91 | + |
|
| 92 | + @Override |
|
| 93 | + public void setLastFailureMessage(String lastFailureMessage) { |
|
| 94 | + this.lastFailureMessage = lastFailureMessage; |
|
| 95 | + } |
|
| 96 | + } |
|
| 97 | + |
|
| 98 | + private static final Logger logger = Logger.getLogger(ArchiveCandidateMonitoringBackgroundTask.class.getName()); |
|
| 99 | + |
|
| 100 | + private final static Duration DELAY_BETWEEN_CHECKS = Duration.ONE_MINUTE.times(5); |
|
| 101 | + private final static Duration LONG_TIMEOUT = Duration.ONE_DAY.times(3); |
|
| 102 | + private final static double MAXIMUM_ONE_MINUTE_SYSTEM_LOAD_AVERAGE = 2.0; |
|
| 103 | + private final static int MAXIMUM_THREAD_POOL_QUEUE_SIZE = 10; |
|
| 104 | + private final static Optional<Duration> TIMEOUT_FIRST_CONTACT = Optional.of(Landscape.WAIT_FOR_PROCESS_TIMEOUT.get().plus(Landscape.WAIT_FOR_HOST_TIMEOUT.get())); |
|
| 105 | + private final static Duration SERVER_COMPARISON_TIMEOUT = Duration.ONE_MINUTE.times(10); // good for two or three attempts, usually |
|
| 106 | + private final static Duration DELAY_BETWEEN_COMPARISON_CHECKS = Duration.ONE_MINUTE; |
|
| 107 | + |
|
| 108 | + /** |
|
| 109 | + * The user on whose behalf the monitoring is performed; this is used for sending notifications about the monitoring |
|
| 110 | + * result to the user and for performing the monitoring with the same permissions as the user (e.g. when accessing |
|
| 111 | + * the candidate's REST API) |
|
| 112 | + */ |
|
| 113 | + private final User currentUser; |
|
| 114 | + private final String candidateHostname; |
|
| 115 | + private final LandscapeService landscapeService; |
|
| 116 | + private final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet; |
|
| 117 | + private final URL continuationBaseURL; |
|
| 118 | + private final ScheduledExecutorService executor; |
|
| 119 | + |
|
| 120 | + /** |
|
| 121 | + * A bearer token expected to authenticate the {@link #currentUser} against the candidate and production ARCHIVE |
|
| 122 | + * servers |
|
| 123 | + */ |
|
| 124 | + private final String effectiveBearerToken; |
|
| 125 | + |
|
| 126 | + private Iterable<Check> checks; |
|
| 127 | + private Iterator<Check> checksIterator; |
|
| 128 | + private Check currentCheck; |
|
| 129 | + |
|
| 130 | + public ArchiveCandidateMonitoringBackgroundTask(User currentUser, LandscapeService landscapeService, |
|
| 131 | + AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet, |
|
| 132 | + String candidateHostname, |
|
| 133 | + ScheduledExecutorService executor, |
|
| 134 | + String effectiveBearerToken, URL continuationBaseURL) { |
|
| 135 | + this.currentUser = currentUser; |
|
| 136 | + this.landscapeService = landscapeService; |
|
| 137 | + this.continuationBaseURL = continuationBaseURL; |
|
| 138 | + this.replicaSet = replicaSet; |
|
| 139 | + this.candidateHostname = candidateHostname; |
|
| 140 | + this.executor = executor; |
|
| 141 | + this.effectiveBearerToken = effectiveBearerToken; |
|
| 142 | + this.checks = Arrays.asList( |
|
| 143 | + new IsAlive(), |
|
| 144 | + new IsReady(), |
|
| 145 | + new HasLowEnoughSystemLoad(), |
|
| 146 | + new HasShortEnoughDefaultBackgroundThreadPoolExecutorQueue(), |
|
| 147 | + new HasShortEnoughDefaultForegroundThreadPoolExecutorQueue(), |
|
| 148 | + new CompareServersWithRestAPI()); |
|
| 149 | + this.checksIterator = this.checks.iterator(); |
|
| 150 | + this.currentCheck = checksIterator.next(); |
|
| 151 | + } |
|
| 152 | + |
|
| 153 | + @Override |
|
| 154 | + public void run() { |
|
| 155 | + try { |
|
| 156 | + if (currentCheck.runCheck()) { |
|
| 157 | + logger.info("Check \""+currentCheck+"\" passed."); |
|
| 158 | + // the check passed; proceed to next check, if any |
|
| 159 | + currentCheck = checksIterator.hasNext() ? checksIterator.next() : null; |
|
| 160 | + if (currentCheck != null) { |
|
| 161 | + logger.info("More checks to do; re-scheduling to run next check \""+currentCheck+"\""); |
|
| 162 | + // re-schedule this task to run next check immediately |
|
| 163 | + executor.submit(this); |
|
| 164 | + } else { |
|
| 165 | + // all checks passed; candidate is ready for production; nothing more to do here |
|
| 166 | + logger.info("Done with all checks; candidate is ready for production."); |
|
| 167 | + notifyProcessOwnerCandidateIsReadyForSpotChecksAndRotation(); // this ends the re-scheduling loop |
|
| 168 | + } |
|
| 169 | + } else { |
|
| 170 | + rescheduleCurrentCheckAfterFailureOrTimeout(); |
|
| 171 | + } |
|
| 172 | + } catch (Exception e) { |
|
| 173 | + logger.warning("Exception while running check \"" + currentCheck + "\" for candidate " + replicaSet.getMaster().getHost().getHostname() + ": " + e.getMessage()); |
|
| 174 | + currentCheck.setLastFailureMessage(e.getMessage()); |
|
| 175 | + try { |
|
| 176 | + rescheduleCurrentCheckAfterFailureOrTimeout(); |
|
| 177 | + } catch (MailException e1) { |
|
| 178 | + logger.severe("Issue while trying to send mail: "+e.getMessage()+"; user may not know what to do next!"); |
|
| 179 | + } |
|
| 180 | + } |
|
| 181 | + } |
|
| 182 | + |
|
| 183 | + private void rescheduleCurrentCheckAfterFailureOrTimeout() throws MailException { |
|
| 184 | + if (currentCheck.hasTimedOut()) { |
|
| 185 | + logger.severe("Check \""+currentCheck+"\" failed and has timed out; giving up on candidate "+replicaSet.getMaster().getHost().getHostname()); |
|
| 186 | + notifyProcessOwnerCandidateFailedToBecomeReadyForProduction(); // this ends the re-scheduling loop |
|
| 187 | + } else { |
|
| 188 | + logger.info("Check \"" + currentCheck + "\" failed with message \"" + currentCheck.getLastFailureMessage() |
|
| 189 | + + "\" but has not yet timed out; re-scheduling to check again after " |
|
| 190 | + + currentCheck.getDelayAfterFailure()); |
|
| 191 | + executor.schedule(this, currentCheck.getDelayAfterFailure().asMillis(), TimeUnit.MILLISECONDS); |
|
| 192 | + } |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + private class IsAlive extends AbstractCheck { |
|
| 196 | + private static final long serialVersionUID = -4265303532881568290L; |
|
| 197 | + |
|
| 198 | + private IsAlive() { |
|
| 199 | + super("is alive", TIMEOUT_FIRST_CONTACT.get(), DELAY_BETWEEN_CHECKS); |
|
| 200 | + } |
|
| 201 | + |
|
| 202 | + @Override |
|
| 203 | + public boolean runCheck() throws Exception { |
|
| 204 | + final boolean result = replicaSet.getMaster().isAlive(Landscape.WAIT_FOR_PROCESS_TIMEOUT); |
|
| 205 | + if (!result) { |
|
| 206 | + setLastFailureMessage("Candidate at "+replicaSet.getMaster().getHost().getPrivateAddress()+" not alive yet"); |
|
| 207 | + } |
|
| 208 | + return result; |
|
| 209 | + } |
|
| 210 | + } |
|
| 211 | + |
|
| 212 | + private class IsReady extends AbstractCheck { |
|
| 213 | + private static final long serialVersionUID = -4265303532881568290L; |
|
| 214 | + |
|
| 215 | + private IsReady() { |
|
| 216 | + super("is ready", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS); |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + @Override |
|
| 220 | + public boolean runCheck() throws Exception { |
|
| 221 | + final boolean result = replicaSet.getMaster().isReady(Landscape.WAIT_FOR_PROCESS_TIMEOUT); |
|
| 222 | + if (!result) { |
|
| 223 | + setLastFailureMessage("Candidate at "+replicaSet.getMaster().getHost().getPrivateAddress()+" not ready yet"); |
|
| 224 | + } |
|
| 225 | + return result; |
|
| 226 | + } |
|
| 227 | + } |
|
| 228 | + |
|
| 229 | + private class HasLowEnoughSystemLoad extends AbstractCheck { |
|
| 230 | + private static final long serialVersionUID = -7931266212387969287L; |
|
| 231 | + |
|
| 232 | + public HasLowEnoughSystemLoad() { |
|
| 233 | + super("has low enough system load", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS); |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + @Override |
|
| 237 | + public boolean runCheck() throws Exception { |
|
| 238 | + final double lastMinuteSystemLoadAverage = replicaSet.getMaster().getLastMinuteSystemLoadAverage(Landscape.WAIT_FOR_PROCESS_TIMEOUT); |
|
| 239 | + final boolean result = lastMinuteSystemLoadAverage < MAXIMUM_ONE_MINUTE_SYSTEM_LOAD_AVERAGE; |
|
| 240 | + if (!result) { |
|
| 241 | + setLastFailureMessage("Candidate at " + replicaSet.getMaster().getHost().getPrivateAddress() |
|
| 242 | + + " has too high system load average of " + lastMinuteSystemLoadAverage |
|
| 243 | + + " which is still above the maximum of " + MAXIMUM_ONE_MINUTE_SYSTEM_LOAD_AVERAGE); |
|
| 244 | + } |
|
| 245 | + return result; |
|
| 246 | + } |
|
| 247 | + } |
|
| 248 | + |
|
| 249 | + private class HasShortEnoughDefaultBackgroundThreadPoolExecutorQueue extends AbstractCheck { |
|
| 250 | + private static final long serialVersionUID = 3482148861663152178L; |
|
| 251 | + |
|
| 252 | + public HasShortEnoughDefaultBackgroundThreadPoolExecutorQueue() { |
|
| 253 | + super("has short enough default background thread pool executor queue", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS); |
|
| 254 | + } |
|
| 255 | + |
|
| 256 | + @Override |
|
| 257 | + public boolean runCheck() throws Exception { |
|
| 258 | + final int defaultBackgroundThreadPoolExecutorQueueSize = replicaSet.getMaster().getDefaultBackgroundThreadPoolExecutorQueueSizeNondelayed(Landscape.WAIT_FOR_PROCESS_TIMEOUT); |
|
| 259 | + final boolean result = defaultBackgroundThreadPoolExecutorQueueSize < MAXIMUM_THREAD_POOL_QUEUE_SIZE; |
|
| 260 | + if (!result) { |
|
| 261 | + setLastFailureMessage("Candidate at " + replicaSet.getMaster().getHost().getPrivateAddress() |
|
| 262 | + + " has too many tasks in default background thread pool executor queue: "+defaultBackgroundThreadPoolExecutorQueueSize+ |
|
| 263 | + " which is still above the maximum of "+MAXIMUM_THREAD_POOL_QUEUE_SIZE); |
|
| 264 | + } |
|
| 265 | + return result; |
|
| 266 | + } |
|
| 267 | + } |
|
| 268 | + |
|
| 269 | + private class HasShortEnoughDefaultForegroundThreadPoolExecutorQueue extends AbstractCheck { |
|
| 270 | + private static final long serialVersionUID = 5194383164577435150L; |
|
| 271 | + |
|
| 272 | + public HasShortEnoughDefaultForegroundThreadPoolExecutorQueue() { |
|
| 273 | + super("has short enough default foreground thread pool executor queue", LONG_TIMEOUT, DELAY_BETWEEN_CHECKS); |
|
| 274 | + } |
|
| 275 | + |
|
| 276 | + @Override |
|
| 277 | + public boolean runCheck() throws Exception { |
|
| 278 | + final int defaultForegroundThreadPoolExecutorQueueSize = replicaSet.getMaster().getDefaultForegroundThreadPoolExecutorQueueSizeNondelayed(Landscape.WAIT_FOR_PROCESS_TIMEOUT); |
|
| 279 | + final boolean result = defaultForegroundThreadPoolExecutorQueueSize < MAXIMUM_THREAD_POOL_QUEUE_SIZE; |
|
| 280 | + if (!result) { |
|
| 281 | + setLastFailureMessage("Candidate at "+replicaSet.getMaster().getHost().getPrivateAddress() |
|
| 282 | + + " has too many tasks in default foreground thread pool executor queue: "+defaultForegroundThreadPoolExecutorQueueSize+ |
|
| 283 | + " which is still above the maximum of "+MAXIMUM_THREAD_POOL_QUEUE_SIZE); |
|
| 284 | + } |
|
| 285 | + return result; |
|
| 286 | + } |
|
| 287 | + } |
|
| 288 | + |
|
| 289 | + private class CompareServersWithRestAPI extends AbstractCheck { |
|
| 290 | + private static final long serialVersionUID = -5271988056894947109L; |
|
| 291 | + |
|
| 292 | + public CompareServersWithRestAPI() { |
|
| 293 | + super("compare servers with REST API", SERVER_COMPARISON_TIMEOUT, DELAY_BETWEEN_COMPARISON_CHECKS); |
|
| 294 | + } |
|
| 295 | + |
|
| 296 | + @Override |
|
| 297 | + public boolean runCheck() throws Exception { |
|
| 298 | + final SailingServer productionServer = landscapeService.getSailingServerFactory().getSailingServer(new URL("https", replicaSet.getHostname(), "/"), effectiveBearerToken); |
|
| 299 | + final SailingServer candidateServer = landscapeService.getSailingServerFactory().getSailingServer(new URL("https", candidateHostname, "/"), effectiveBearerToken); |
|
| 300 | + final CompareServersResult comparisonResult = candidateServer.compareServers(Optional.empty(), productionServer, Optional.empty()); |
|
| 301 | + if (comparisonResult.hasDiffs()) { |
|
| 302 | + setLastFailureMessage( |
|
| 303 | + "Candidate server does not match production server according to REST API comparison." |
|
| 304 | + + "\nDifferences on candidate side: " + comparisonResult.getADiffs() |
|
| 305 | + + "\nDifferences on production side: " + comparisonResult.getBDiffs() |
|
| 306 | + + "\nNot proceeding further. You need to resolve the issues manually." |
|
| 307 | + + "\nCheck https://"+candidateHostname+"/sailingserver/v1/compareservers?server2="+replicaSet.getHostname() |
|
| 308 | + + "\nafter you have tried to resolve the differences." |
|
| 309 | + + "\nThen, run your smoke checks and trigger the rotation if everything looks good."); |
|
| 310 | + } |
|
| 311 | + return !comparisonResult.hasDiffs(); |
|
| 312 | + } |
|
| 313 | + } |
|
| 314 | + |
|
| 315 | + private void notifyProcessOwnerCandidateFailedToBecomeReadyForProduction() throws MailException { |
|
| 316 | + landscapeService.sendMailToUser(currentUser, "NewArchiveCandidateFailedSubject", |
|
| 317 | + "NewArchiveCandidateFailedBody", replicaSet.getServerName(), currentCheck.getName(), |
|
| 318 | + currentCheck.getLastFailureMessage()); |
|
| 319 | + } |
|
| 320 | + |
|
| 321 | + private void notifyProcessOwnerCandidateIsReadyForSpotChecksAndRotation() throws MailException, InterruptedException, ExecutionException { |
|
| 322 | + landscapeService.sendMailToUser(currentUser, "NewArchiveCandidateReadyForSpotChecksAndRotationSubject", |
|
| 323 | + "NewArchiveCandidateReadyForSpotChecksAndRotationBody", replicaSet.getName(), candidateHostname, |
|
| 324 | + replicaSet.getHostname(), " - "+Util.joinStrings("\n - ", Util.map(checks, Check::getName)), |
|
| 325 | + continuationBaseURL.toString()); |
|
| 326 | + } |
|
| 327 | +} |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/LandscapeServiceImpl.java
| ... | ... | @@ -18,6 +18,7 @@ import java.util.Set; |
| 18 | 18 | import java.util.UUID; |
| 19 | 19 | import java.util.concurrent.CompletableFuture; |
| 20 | 20 | import java.util.concurrent.ExecutionException; |
| 21 | +import java.util.concurrent.ScheduledExecutorService; |
|
| 21 | 22 | import java.util.concurrent.TimeoutException; |
| 22 | 23 | import java.util.function.Function; |
| 23 | 24 | import java.util.logging.Level; |
| ... | ... | @@ -83,6 +84,7 @@ import com.sap.sse.landscape.aws.AwsLandscape; |
| 83 | 84 | import com.sap.sse.landscape.aws.AwsShard; |
| 84 | 85 | import com.sap.sse.landscape.aws.HostSupplier; |
| 85 | 86 | import com.sap.sse.landscape.aws.ReverseProxy; |
| 87 | +import com.sap.sse.landscape.aws.ReverseProxyCluster; |
|
| 86 | 88 | import com.sap.sse.landscape.aws.Tags; |
| 87 | 89 | import com.sap.sse.landscape.aws.TargetGroup; |
| 88 | 90 | import com.sap.sse.landscape.aws.common.shared.RedirectDTO; |
| ... | ... | @@ -132,7 +134,7 @@ import software.amazon.awssdk.services.sts.model.Credentials; |
| 132 | 134 | public class LandscapeServiceImpl implements LandscapeService { |
| 133 | 135 | private static final Logger logger = Logger.getLogger(LandscapeServiceImpl.class.getName()); |
| 134 | 136 | |
| 135 | - private static final String STRING_MESSAGES_BASE_NAME = "stringmessages/SailingLandscape_StringMessages"; |
|
| 137 | + public static final String STRING_MESSAGES_BASE_NAME = "stringmessages/SailingLandscape_StringMessages"; |
|
| 136 | 138 | |
| 137 | 139 | private static final String TEMPORARY_UPGRADE_REPLICA_NAME_SUFFIX = " (Upgrade Replica)"; |
| 138 | 140 | |
| ... | ... | @@ -183,7 +185,7 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 183 | 185 | newSharedMasterInstance ? optionalMemoryTotalSizeFactorOrNull : null, optionalIgtimiRiotPort, region, release); |
| 184 | 186 | final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken); |
| 185 | 187 | final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get(); |
| 186 | - establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(name, bearerTokenUsedByReplicas, |
|
| 188 | + establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(name, bearerTokenUsedByReplicas, |
|
| 187 | 189 | inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort()); |
| 188 | 190 | final com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost.Builder<?, String> masterHostBuilder = StartSailingAnalyticsMasterHost.masterHostBuilder(masterConfigurationBuilder); |
| 189 | 191 | masterHostBuilder |
| ... | ... | @@ -236,6 +238,160 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 236 | 238 | } |
| 237 | 239 | |
| 238 | 240 | @Override |
| 241 | + public void createArchiveReplicaSet( |
|
| 242 | + String regionId, String replicaSetName, String instanceType, String releaseNameOrNullForLatestMaster, Database databaseConfiguration, |
|
| 243 | + String optionalKeyName, byte[] privateKeyEncryptionPassphrase, String replicaReplicationBearerToken, String optionalDomainName, |
|
| 244 | + Integer optionalMemoryInMegabytesOrNull, String securityServiceReplicationBearerToken, |
|
| 245 | + Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, URL continuationBaseURL) throws Exception { |
|
| 246 | + assert getSecurityService().getCurrentUser() != null; |
|
| 247 | + final AwsLandscape<String> landscape = getLandscape(); |
|
| 248 | + final String candidateHostname = getHostname(SharedLandscapeConstants.ARCHIVE_CANDIDATE_SUBDOMAIN, optionalDomainName); |
|
| 249 | + final Iterable<ResourceRecordSet> existingDNSRulesForHostname = landscape.getResourceRecordSets(candidateHostname); |
|
| 250 | + // Failing early in case DNS record already exists (see also bug 5826): |
|
| 251 | + if (existingDNSRulesForHostname != null && !Util.isEmpty(existingDNSRulesForHostname)) { |
|
| 252 | + throw new IllegalArgumentException("DNS record for "+candidateHostname+" already exists"); |
|
| 253 | + } |
|
| 254 | + final AwsRegion region = new AwsRegion(regionId, landscape); |
|
| 255 | + final Release release = getRelease(releaseNameOrNullForLatestMaster); |
|
| 256 | + final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> oldArchiveReplicaSet = getApplicationReplicaSet( |
|
| 257 | + region, SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME, |
|
| 258 | + Landscape.WAIT_FOR_PROCESS_TIMEOUT.map(Duration::asMillis).orElse(null), |
|
| 259 | + optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 260 | + final Integer oldArchiveMemoryInMB = getMemoryInMegabytes(optionalKeyName, privateKeyEncryptionPassphrase, oldArchiveReplicaSet.getMaster()); |
|
| 261 | + final com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration.Builder<?, String> masterConfigurationBuilder = |
|
| 262 | + createArchiveConfigurationBuilder(replicaSetName, databaseConfiguration, securityServiceReplicationBearerToken, |
|
| 263 | + // if no memory size is specified, use that of existing production ARCHIVE server |
|
| 264 | + optionalMemoryInMegabytesOrNull == null && optionalMemoryTotalSizeFactorOrNull == null ? oldArchiveMemoryInMB : optionalMemoryInMegabytesOrNull, |
|
| 265 | + optionalMemoryTotalSizeFactorOrNull, optionalIgtimiRiotPort, region, release); |
|
| 266 | + final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken); |
|
| 267 | + final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get(); |
|
| 268 | + establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas, |
|
| 269 | + inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort()); |
|
| 270 | + final ReverseProxyCluster<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster = |
|
| 271 | + getLandscape().getReverseProxyCluster(region); |
|
| 272 | + final com.sap.sailing.landscape.procedures.StartSailingAnalyticsMasterHost.Builder<?, String> masterHostBuilder = StartSailingAnalyticsMasterHost.masterHostBuilder(masterConfigurationBuilder); |
|
| 273 | + masterHostBuilder |
|
| 274 | + .setAvailabilityZone(getBestAvailabilityZoneForArchiveCandidate(region, landscape, oldArchiveReplicaSet.getMaster(), reverseProxyCluster, optionalKeyName, privateKeyEncryptionPassphrase)) |
|
| 275 | + .setInstanceName(SharedLandscapeConstants.ARCHIVE_SERVER_NEW_CANDIDATE_INSTANCE_NAME) |
|
| 276 | + .setInstanceType(InstanceType.valueOf(instanceType)) |
|
| 277 | + .setOptionalTimeout(Landscape.WAIT_FOR_HOST_TIMEOUT) |
|
| 278 | + .setLandscape(landscape) |
|
| 279 | + .setRegion(region) |
|
| 280 | + .setPrivateKeyEncryptionPassphrase(privateKeyEncryptionPassphrase); |
|
| 281 | + if (optionalKeyName != null) { |
|
| 282 | + masterHostBuilder.setKeyName(optionalKeyName); |
|
| 283 | + } |
|
| 284 | + final StartSailingAnalyticsMasterHost<String> masterHostStartProcedure = masterHostBuilder.build(); |
|
| 285 | + masterHostStartProcedure.run(); |
|
| 286 | + final SailingAnalyticsProcess<String> master = masterHostStartProcedure.getSailingAnalyticsProcess(); |
|
| 287 | + master.getHost().setTerminationProtection(true); |
|
| 288 | + master.waitUntilAlive(Optional.of(Landscape.WAIT_FOR_HOST_TIMEOUT.get().plus(Landscape.WAIT_FOR_PROCESS_TIMEOUT.get()))); |
|
| 289 | + final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet = landscape |
|
| 290 | + .getApplicationReplicaSet(region, replicaSetName, master, /* replicas */ Collections.emptySet(), |
|
| 291 | + Optional.of(Landscape.WAIT_FOR_HOST_TIMEOUT.get().plus(Landscape.WAIT_FOR_PROCESS_TIMEOUT.get())), |
|
| 292 | + Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase); |
|
| 293 | + final String privateIpAdress = master.getHost().getPrivateAddress().getHostAddress(); |
|
| 294 | + logger.info("Adding reverse proxy rule for archive candidate with hostname "+ candidateHostname + " and private ip address " + privateIpAdress); |
|
| 295 | + reverseProxyCluster.setPlainRedirect(candidateHostname, master, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase); |
|
| 296 | + sendMailAboutNewArchiveCandidate(replicaSet); |
|
| 297 | + final ScheduledExecutorService monitorTaskExecutor = ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(); |
|
| 298 | + final ArchiveCandidateMonitoringBackgroundTask monitoringTask = new ArchiveCandidateMonitoringBackgroundTask( |
|
| 299 | + getSecurityService().getCurrentUser(), this, replicaSet, candidateHostname, monitorTaskExecutor, |
|
| 300 | + bearerTokenUsedByReplicas, continuationBaseURL); |
|
| 301 | + monitorTaskExecutor.execute(monitoringTask); |
|
| 302 | + } |
|
| 303 | + |
|
| 304 | + private AwsAvailabilityZone getBestAvailabilityZoneForArchiveCandidate(AwsRegion region, AwsLandscape<String> landscape, |
|
| 305 | + SailingAnalyticsProcess<String> oldArchivePrimary, ReverseProxyCluster<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster, |
|
| 306 | + String optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 307 | + final AwsAvailabilityZone oldArchiveAZ = oldArchivePrimary.getHost().getAvailabilityZone(); |
|
| 308 | + AwsAvailabilityZone result = null; |
|
| 309 | + for (final AwsInstance<String> reverseProxyHost : reverseProxyCluster.getHosts()) { |
|
| 310 | + final AwsAvailabilityZone az = reverseProxyHost.getAvailabilityZone(); |
|
| 311 | + if (!az.equals(oldArchiveAZ)) { |
|
| 312 | + result = az; |
|
| 313 | + logger.info("Identified availability zone (AZ) " + result |
|
| 314 | + + " for new ARCHIVE server; it differs from current ARCHIVE's AZ " + oldArchiveAZ |
|
| 315 | + + " and has reverse proxy host " + reverseProxyHost.getInstanceId() + " in that AZ."); |
|
| 316 | + break; |
|
| 317 | + } |
|
| 318 | + } |
|
| 319 | + if (result == null) { |
|
| 320 | + logger.warning( |
|
| 321 | + "Couldn't find a reverse proxy in an availabililty zone different from that of the current ARCHIVE server (" |
|
| 322 | + + oldArchiveAZ + "). Reverse proxies in AZs " + Util.joinStrings(", ", |
|
| 323 | + Util.map(reverseProxyCluster.getHosts(), host -> host.getAvailabilityZone()))); |
|
| 324 | + // no AZ found that is not the same as for the current ARCHIVE server and also has a reverse proxy; |
|
| 325 | + // now we have to choose between "a rock and a hard place:" either launch in an AZ where we don't have |
|
| 326 | + // a reverse proxy and hence will see slightly less throughput and some additional cost for cross-AZ |
|
| 327 | + // traffic; or launch in the same AZ the current ARCHIVE runs in; this will put the new failover (the |
|
| 328 | + // current production ARCHIVE) and the new production ARCHIVE into the same AZ, not benefiting from |
|
| 329 | + // the availability improvements incurred by running in multiple AZs. |
|
| 330 | + result = Util.first(reverseProxyCluster.getHosts()).getAvailabilityZone(); |
|
| 331 | + logger.info("Choosing the AZ of the first reverse proxy to avoid cost and performance reduction by cross-AZ traffic: "+result); |
|
| 332 | + } |
|
| 333 | + return result; |
|
| 334 | + } |
|
| 335 | + |
|
| 336 | + @Override |
|
| 337 | + public void makeCandidateArchiveServerGoLive(String regionId, String optionalKeyNameOrNull, |
|
| 338 | + byte[] privateKeyEncryptionPassphrase, String optionalDomainName) |
|
| 339 | + throws Exception { |
|
| 340 | + final AwsLandscape<String> landscape = getLandscape(); |
|
| 341 | + final AwsRegion region = new AwsRegion(regionId, landscape); |
|
| 342 | + final String candidateHostname = getHostname(SharedLandscapeConstants.ARCHIVE_CANDIDATE_SUBDOMAIN, optionalDomainName); |
|
| 343 | + final AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> archiveReplicaSet = getApplicationReplicaSet( |
|
| 344 | + region, SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME, |
|
| 345 | + Landscape.WAIT_FOR_PROCESS_TIMEOUT.map(Duration::asMillis).orElse(null), |
|
| 346 | + optionalKeyNameOrNull, privateKeyEncryptionPassphrase); |
|
| 347 | + if (archiveReplicaSet == null) { |
|
| 348 | + throw new IllegalArgumentException("Couldn't find candidate replica set with name " |
|
| 349 | + + SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME + " in region " + regionId); |
|
| 350 | + } |
|
| 351 | + final SailingAnalyticsProcess<String> candidate = archiveReplicaSet.getMaster(); |
|
| 352 | + final ReverseProxy<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>, RotatingFileBasedLog> reverseProxyCluster = |
|
| 353 | + getLandscape().getCentralReverseProxy(region); |
|
| 354 | + final Optional<String> optionalKeyName = Optional.ofNullable(optionalKeyNameOrNull); |
|
| 355 | + final Pair<String, String> archiveAndFailoverIPs = reverseProxyCluster |
|
| 356 | + .getArchiveAndFailoverIPs(optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 357 | + logger.info("Found new candidate " + candidate.getHost() |
|
| 358 | + .getInstanceId() + " with internal IP " |
|
| 359 | + + candidate.getHost().getPrivateAddress() + " and current production ARCHIVE " |
|
| 360 | + + archiveAndFailoverIPs.getA() + ". Turning production into failover and candidate into production."); |
|
| 361 | + reverseProxyCluster.setArchiveAndFailoverIPs(candidate.getHost().getPrivateAddress().getHostAddress(), |
|
| 362 | + archiveAndFailoverIPs.getA(), optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 363 | + try { |
|
| 364 | + final SailingAnalyticsHost<String> oldProductionArchive = getLandscape().getHostByPrivateDnsNameOrIpAddress(region, archiveAndFailoverIPs.getA(), new SailingAnalyticsHostSupplier<>()); |
|
| 365 | + getLandscape().setInstanceName(oldProductionArchive, SharedLandscapeConstants.ARCHIVE_SERVER_FAILOVER_INSTANCE_NAME); |
|
| 366 | + } catch (Exception e) { |
|
| 367 | + logger.warning("Couldn't find old production ARCHIVE with IP "+archiveAndFailoverIPs.getA()+", so couldn't update its Name tag"); |
|
| 368 | + } |
|
| 369 | + getLandscape().setInstanceName(candidate.getHost(), SharedLandscapeConstants.ARCHIVE_SERVER_INSTANCE_NAME); |
|
| 370 | + logger.info("Removing reverse proxy rule for archive candidate with hostname "+ candidateHostname); |
|
| 371 | + reverseProxyCluster.removeRedirect(candidateHostname, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 372 | + try { |
|
| 373 | + final SailingAnalyticsHost<String> oldFailover = getLandscape().getHostByPrivateDnsNameOrIpAddress(region, |
|
| 374 | + archiveAndFailoverIPs.getB(), new SailingAnalyticsHostSupplier<>()); |
|
| 375 | + oldFailover.setTerminationProtection(false); |
|
| 376 | + logger.info("Terminating old failover process, and hence probably host " + oldFailover.getInstanceId() |
|
| 377 | + + " with internal IP " + oldFailover.getPrivateAddress()); |
|
| 378 | + for (final SailingAnalyticsProcess<String> applicationProcessOnOldFailover : oldFailover |
|
| 379 | + .getApplicationProcesses(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName, |
|
| 380 | + privateKeyEncryptionPassphrase)) { |
|
| 381 | + if (applicationProcessOnOldFailover |
|
| 382 | + .getServerName(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName, |
|
| 383 | + privateKeyEncryptionPassphrase) |
|
| 384 | + .equals(SharedLandscapeConstants.ARCHIVE_SERVER_APPLICATION_REPLICA_SET_NAME)) { |
|
| 385 | + applicationProcessOnOldFailover.stopAndTerminateIfLast(Landscape.WAIT_FOR_PROCESS_TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 386 | + } |
|
| 387 | + } |
|
| 388 | + } catch (Exception e) { |
|
| 389 | + logger.warning("Issue trying to clean up old failover instance: "+e.getMessage()); |
|
| 390 | + } |
|
| 391 | + sendMailAboutNewArchiveServerLive(archiveReplicaSet); |
|
| 392 | + } |
|
| 393 | + |
|
| 394 | + @Override |
|
| 239 | 395 | public AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> deployApplicationToExistingHost(String replicaSetName, |
| 240 | 396 | SailingAnalyticsHost<String> hostToDeployTo, String replicaInstanceType, boolean dynamicLoadBalancerMapping, |
| 241 | 397 | String releaseNameOrNullForLatestMaster, String optionalKeyName, byte[] privateKeyEncryptionPassphrase, |
| ... | ... | @@ -352,7 +508,7 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 352 | 508 | optionalIgtimiRiotPort, region, release); |
| 353 | 509 | final InboundReplicationConfiguration inboundMasterReplicationConfiguration = masterConfigurationBuilder.getInboundReplicationConfiguration().get(); |
| 354 | 510 | final String bearerTokenUsedByReplicas = getEffectiveBearerToken(replicaReplicationBearerToken); |
| 355 | - establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas, |
|
| 511 | + establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(replicaSetName, bearerTokenUsedByReplicas, |
|
| 356 | 512 | inboundMasterReplicationConfiguration.getMasterHostname(), inboundMasterReplicationConfiguration.getMasterHttpPort()); |
| 357 | 513 | final SailingAnalyticsProcess<String> master = deployProcessToSharedInstance(hostToDeployTo, |
| 358 | 514 | masterConfigurationBuilder, optionalKeyName, privateKeyEncryptionPassphrase); |
| ... | ... | @@ -805,7 +961,7 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 805 | 961 | : SailingReleaseRepository.INSTANCE.getRelease(releaseNameOrNullForLatestMaster); |
| 806 | 962 | } |
| 807 | 963 | |
| 808 | - private void establishServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(String serverName, |
|
| 964 | + private void establishServerAndServerGroupAndTryToMakeCurrentUserItsOwnerAndMember(String serverName, |
|
| 809 | 965 | String bearerTokenUsedByReplicas, String securityServiceHostname, |
| 810 | 966 | Integer securityServicePort) |
| 811 | 967 | throws MalformedURLException, ClientProtocolException, IOException, ParseException, IllegalAccessException { |
| ... | ... | @@ -815,6 +971,7 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 815 | 971 | RemoteServerUtil.getBaseServerUrl(securityServiceHostname, securityServicePort==null?443:securityServicePort), bearerTokenUsedByReplicas); |
| 816 | 972 | final UUID userGroupId = securityServiceServer.getUserGroupIdByName(serverGroupName); |
| 817 | 973 | final UUID groupId; |
| 974 | + final String securityServiceServerUsername = securityServiceServer.getUsername(); |
|
| 818 | 975 | if (userGroupId != null) { |
| 819 | 976 | groupId = userGroupId; |
| 820 | 977 | final TypeRelativeObjectIdentifier serverGroupTypeRelativeObjectId = new TypeRelativeObjectIdentifier(userGroupId.toString()); |
| ... | ... | @@ -829,7 +986,7 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 829 | 986 | SecuredSecurityTypes.SERVER.getPermissionForTypeRelativeIdentifier(DefaultActions.DELETE, serverGroupTypeRelativeObjectId))); |
| 830 | 987 | for (final Pair<WildcardPermission, Boolean> permission : permissions) { |
| 831 | 988 | if (!permission.getB()) { |
| 832 | - final String msg = "Subject "+securityServiceServer.getUsername()+" on server "+securityServiceHostname+ |
|
| 989 | + final String msg = "Subject "+securityServiceServerUsername+" on server "+securityServiceHostname+ |
|
| 833 | 990 | " is not allowed "+permission.getA()+". Not allowing to create application replica set for "+serverName; |
| 834 | 991 | logger.warning(msg); |
| 835 | 992 | throw new AuthorizationException(msg); |
| ... | ... | @@ -841,12 +998,12 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 841 | 998 | } else { |
| 842 | 999 | groupId = securityServiceServer.createUserGroupAndAddCurrentUser(serverGroupName); |
| 843 | 1000 | try { |
| 844 | - securityServiceServer.addRoleToUser(ServerAdminRole.getInstance().getId(), securityServiceServer.getUsername(), |
|
| 1001 | + securityServiceServer.addRoleToUser(ServerAdminRole.getInstance().getId(), securityServiceServerUsername, |
|
| 845 | 1002 | /* qualified for server group: */ groupId, null, /* transitive */ true); |
| 846 | 1003 | } catch (Exception e) { |
| 847 | 1004 | // this didn't work, but it's not the end of the world if we cannot grant the requesting user the |
| 848 | 1005 | // event_manager:{group-name} role; the user may end up not having SERVER:CREATE_OBJECT... |
| 849 | - logger.warning("Couldn't grant role "+ServerAdminRole.getInstance().getName()+" to user "+securityServiceServer.getUsername()+": "+e.getMessage()); |
|
| 1006 | + logger.warning("Couldn't grant role "+ServerAdminRole.getInstance().getName()+" to user "+securityServiceServerUsername+": "+e.getMessage()); |
|
| 850 | 1007 | } |
| 851 | 1008 | try { |
| 852 | 1009 | // try to set the group owner of the new group to the group itself, allowing all users with role user:{group-name} to |
| ... | ... | @@ -861,6 +1018,14 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 861 | 1018 | } |
| 862 | 1019 | } |
| 863 | 1020 | ensureGroupMembersCanReadGroup(securityServiceServer, groupId); |
| 1021 | + final TypeRelativeObjectIdentifier serverTypeRelativeObjectId = new TypeRelativeObjectIdentifier(serverName); |
|
| 1022 | + final Pair<UUID, String> serverOwningGroupIdAndUsername = securityServiceServer.getGroupAndUserOwner(SecuredSecurityTypes.SERVER, serverTypeRelativeObjectId); |
|
| 1023 | + if (serverOwningGroupIdAndUsername == null || serverOwningGroupIdAndUsername.getA() == null && serverOwningGroupIdAndUsername.getB() == null) { |
|
| 1024 | + logger.info("Setting ownership for SERVER object "+serverName+" to group "+serverGroupName+" and user "+securityServiceServerUsername); |
|
| 1025 | + securityServiceServer.setGroupAndUserOwner(SecuredSecurityTypes.SERVER, serverTypeRelativeObjectId, |
|
| 1026 | + Optional.of(SecuredSecurityTypes.SERVER.getName()+"/"+serverName), |
|
| 1027 | + Optional.of(groupId), Optional.of(securityServiceServerUsername)); |
|
| 1028 | + } |
|
| 864 | 1029 | } |
| 865 | 1030 | |
| 866 | 1031 | private void ensureGroupMembersCanReadGroup(SailingServer securityServiceServer, UUID groupId) throws ClientProtocolException, IOException, ParseException { |
| ... | ... | @@ -895,6 +1060,29 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 895 | 1060 | applyMemoryConfigurationToApplicationConfigurationBuilder(masterConfigurationBuilder, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull); |
| 896 | 1061 | return masterConfigurationBuilder; |
| 897 | 1062 | } |
| 1063 | + |
|
| 1064 | + private <AppConfigBuilderT extends com.sap.sailing.landscape.procedures.SailingAnalyticsMasterConfiguration.Builder<AppConfigBuilderT, String>> AppConfigBuilderT createArchiveConfigurationBuilder( |
|
| 1065 | + String replicaSetName, Database databaseConfiguration, String optionalMasterReplicationBearerTokenOrNull, Integer optionalMemoryInMegabytesOrNull, |
|
| 1066 | + Integer optionalMemoryTotalSizeFactorOrNull, Integer optionalIgtimiRiotPort, final AwsRegion region, final Release release) { |
|
| 1067 | + final AppConfigBuilderT masterConfigurationBuilder = SailingAnalyticsMasterConfiguration.masterBuilder(); |
|
| 1068 | + final String bearerTokenUsedByMaster = getEffectiveBearerToken(optionalMasterReplicationBearerTokenOrNull); |
|
| 1069 | + final User currentUser = getSecurityService().getCurrentUser(); |
|
| 1070 | + if (currentUser != null && currentUser.isEmailValidated() && currentUser.getEmail() != null) { |
|
| 1071 | + masterConfigurationBuilder.setCommaSeparatedEmailAddressesToNotifyOfStartup(currentUser.getEmail()); |
|
| 1072 | + } |
|
| 1073 | + masterConfigurationBuilder |
|
| 1074 | + .setDatabaseConfiguration(databaseConfiguration) |
|
| 1075 | + .setLandscape(getLandscape()) |
|
| 1076 | + .setServerName(replicaSetName) |
|
| 1077 | + .setRelease(release) |
|
| 1078 | + .setRegion(region) |
|
| 1079 | + .setInboundReplicationConfiguration(InboundReplicationConfiguration.builder().setCredentials(new BearerTokenReplicationCredentials(bearerTokenUsedByMaster)).build()); |
|
| 1080 | + if (optionalIgtimiRiotPort != null) { |
|
| 1081 | + masterConfigurationBuilder.setIgtimiRiotPort(optionalIgtimiRiotPort); |
|
| 1082 | + } |
|
| 1083 | + applyMemoryConfigurationToApplicationConfigurationBuilder(masterConfigurationBuilder, optionalMemoryInMegabytesOrNull, optionalMemoryTotalSizeFactorOrNull); |
|
| 1084 | + return masterConfigurationBuilder; |
|
| 1085 | + } |
|
| 898 | 1086 | |
| 899 | 1087 | /** |
| 900 | 1088 | * No specific memory configuration is made here; replicas are mostly launched on a dedicated host and hence can |
| ... | ... | @@ -1648,6 +1836,32 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 1648 | 1836 | return getLandscape().getApplicationReplicaSet(region, replicaSet.getServerName(), newMaster, replicaSet.getReplicas(), |
| 1649 | 1837 | Landscape.WAIT_FOR_PROCESS_TIMEOUT, Optional.ofNullable(optionalKeyName), privateKeyEncryptionPassphrase); |
| 1650 | 1838 | } |
| 1839 | + |
|
| 1840 | + private void sendMailAboutNewArchiveCandidate( |
|
| 1841 | + AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException { |
|
| 1842 | + sendMailToCurrentUser("StartingNewArchiveCandidateSubject", "StartingNewArchiveCandidateBody", replicaSet.getServerName()); |
|
| 1843 | + sendMailToReplicaSetOwner(replicaSet, "RefrainFromArchivingSubject", "RefrainFromArchivingBody", Optional.empty()); |
|
| 1844 | + } |
|
| 1845 | + |
|
| 1846 | + @Override |
|
| 1847 | + public void sendMailToCurrentUser(String messageSubjectKey, String messageBodyKey, String... messageParameters) throws MailException { |
|
| 1848 | + final User currentUser = getSecurityService().getCurrentUser(); |
|
| 1849 | + sendMailToUser(currentUser, messageSubjectKey, messageBodyKey, messageParameters); |
|
| 1850 | + } |
|
| 1851 | + |
|
| 1852 | + @Override |
|
| 1853 | + public void sendMailToUser(final User user, String messageSubjectKey, String messageBodyKey, |
|
| 1854 | + String... messageParameters) throws MailException { |
|
| 1855 | + final ResourceBundleStringMessages stringMessages = ResourceBundleStringMessages.create(STRING_MESSAGES_BASE_NAME, getClass().getClassLoader(), StandardCharsets.UTF_8.name()); |
|
| 1856 | + if (user != null && user.isEmailValidated()) { |
|
| 1857 | + final String subject = stringMessages.get(user.getLocaleOrDefault(), messageSubjectKey, messageParameters); |
|
| 1858 | + final String body = stringMessages.get(user.getLocaleOrDefault(), messageBodyKey, messageParameters); |
|
| 1859 | + getSecurityService().sendMail(user.getName(), subject, body); |
|
| 1860 | + } else { |
|
| 1861 | + logger.warning("Not sending e-mail about new archive candidate to current user because no user is logged in or email address of logged in user "+ |
|
| 1862 | + (user == null ? "" : user.getName()+" ")+"is not validated"); |
|
| 1863 | + } |
|
| 1864 | + } |
|
| 1651 | 1865 | |
| 1652 | 1866 | private void sendMailAboutMasterUnavailable( |
| 1653 | 1867 | AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException { |
| ... | ... | @@ -1669,7 +1883,8 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 1669 | 1883 | * action on the {@code replicaSet} will receive the e-mail in addition to the server owner. No user |
| 1670 | 1884 | * will receive the e-mail twice. |
| 1671 | 1885 | */ |
| 1672 | - private void sendMailToReplicaSetOwner( |
|
| 1886 | + @Override |
|
| 1887 | + public void sendMailToReplicaSetOwner( |
|
| 1673 | 1888 | AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet, |
| 1674 | 1889 | final String subjectMessageKey, final String bodyMessageKey, Optional<Action> alsoSendToAllUsersWithThisPermissionOnReplicaSet) throws MailException { |
| 1675 | 1890 | final Iterable<User> usersToSendMailTo = getSecurityService().getUsersToInformAboutReplicaSet(replicaSet.getServerName(), alsoSendToAllUsersWithThisPermissionOnReplicaSet); |
| ... | ... | @@ -1687,6 +1902,15 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 1687 | 1902 | } |
| 1688 | 1903 | } |
| 1689 | 1904 | } |
| 1905 | + |
|
| 1906 | + /** |
|
| 1907 | + * This is to be called after the rotation of candidate/production/failover ARCHIVE has happend, to inform |
|
| 1908 | + */ |
|
| 1909 | + private void sendMailAboutNewArchiveServerLive( |
|
| 1910 | + AwsApplicationReplicaSet<String, SailingAnalyticsMetrics, SailingAnalyticsProcess<String>> replicaSet) throws MailException { |
|
| 1911 | + sendMailToReplicaSetOwner(replicaSet, "NewArchiveServerLiveSubject", "NewArchiveServerLiveBody", |
|
| 1912 | + Optional.of(ServerActions.CONFIGURE_LOCAL_SERVER)); |
|
| 1913 | + } |
|
| 1690 | 1914 | |
| 1691 | 1915 | /** |
| 1692 | 1916 | * If a non-{@code null}, non-{@link String#isEmpty() empty} bearer token is provided by the |
| ... | ... | @@ -2031,4 +2255,9 @@ public class LandscapeServiceImpl implements LandscapeService { |
| 2031 | 2255 | final Integer memoryInMegabytes = JvmUtils.getMegabytesFromJvmSize(memoryInMegabytesAsString).orElse(null); |
| 2032 | 2256 | return memoryInMegabytes; |
| 2033 | 2257 | } |
| 2258 | + |
|
| 2259 | + @Override |
|
| 2260 | + public SailingServerFactory getSailingServerFactory() { |
|
| 2261 | + return sailingServerFactoryTracker.getService(); |
|
| 2262 | + } |
|
| 2034 | 2263 | } |
java/com.sap.sailing.landscape/src/com/sap/sailing/landscape/impl/SailingAnalyticsProcessImpl.java
| ... | ... | @@ -51,6 +51,11 @@ implements SailingAnalyticsProcess<ShardingKey> { |
| 51 | 51 | private static final String STATUS_SERVERDIRECTORY_PROPERTY_NAME = "serverdirectory"; |
| 52 | 52 | private static final String STATUS_RELEASE_PROPERTY_NAME = "release"; |
| 53 | 53 | private static final String MONGODB_CONFIGURATION_PROPERTY_NAME = "mongoDbConfiguration"; |
| 54 | + private static final String SYSTEM_LOAD_AVERAGE_LAST_MINUTE_NAME = "systemloadaveragelastminute"; |
|
| 55 | + private static final String DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME = "defaultbackgroundthreadpoolexecutorqueuelength"; |
|
| 56 | + private static final String DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME = "defaultforegroundthreadpoolexecutorqueuelength"; |
|
| 57 | + private static final String DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME = "defaultbackgroundthreadpoolexecutorqueuelengthnondelayed"; |
|
| 58 | + private static final String DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME = "defaultforegroundthreadpoolexecutorqueuelengthnondelayed"; |
|
| 54 | 59 | private Integer expeditionUdpPort; |
| 55 | 60 | private Integer igtimiRiotPort; |
| 56 | 61 | private Release release; |
| ... | ... | @@ -127,6 +132,36 @@ implements SailingAnalyticsProcess<ShardingKey> { |
| 127 | 132 | } |
| 128 | 133 | |
| 129 | 134 | @Override |
| 135 | + public double getLastMinuteSystemLoadAverage(Optional<Duration> optionalTimeout) throws TimeoutException, Exception { |
|
| 136 | + final JSONObject status = getStatus(optionalTimeout); |
|
| 137 | + return ((Number) status.get(SYSTEM_LOAD_AVERAGE_LAST_MINUTE_NAME)).doubleValue(); |
|
| 138 | + } |
|
| 139 | + |
|
| 140 | + @Override |
|
| 141 | + public int getDefaultBackgroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception { |
|
| 142 | + final JSONObject status = getStatus(optionalTimeout); |
|
| 143 | + return ((Number) status.get(DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME)).intValue(); |
|
| 144 | + } |
|
| 145 | + |
|
| 146 | + @Override |
|
| 147 | + public int getDefaultForegroundThreadPoolExecutorQueueSize(Optional<Duration> optionalTimeout) throws TimeoutException, Exception { |
|
| 148 | + final JSONObject status = getStatus(optionalTimeout); |
|
| 149 | + return ((Number) status.get(DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NAME)).intValue(); |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + @Override |
|
| 153 | + public int getDefaultBackgroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception { |
|
| 154 | + final JSONObject status = getStatus(optionalTimeout); |
|
| 155 | + return ((Number) status.get(DEFAULT_BACKGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME)).intValue(); |
|
| 156 | + } |
|
| 157 | + |
|
| 158 | + @Override |
|
| 159 | + public int getDefaultForegroundThreadPoolExecutorQueueSizeNondelayed(Optional<Duration> optionalTimeout) throws TimeoutException, Exception { |
|
| 160 | + final JSONObject status = getStatus(optionalTimeout); |
|
| 161 | + return ((Number) status.get(DEFAULT_FOREGROUND_THREAD_POOL_EXECUTOR_QUEUE_LENGTH_NONDELAYED_NAME)).intValue(); |
|
| 162 | + } |
|
| 163 | + |
|
| 164 | + @Override |
|
| 130 | 165 | public Database getDatabaseConfiguration(Region region, Optional<Duration> optionalTimeout, |
| 131 | 166 | Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 132 | 167 | final JSONObject mongoDBConfiguration = (JSONObject) getStatus(optionalTimeout).get(MONGODB_CONFIGURATION_PROPERTY_NAME); |
java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/CompareServersResource.java
| ... | ... | @@ -19,9 +19,11 @@ import java.util.UUID; |
| 19 | 19 | import java.util.logging.Logger; |
| 20 | 20 | |
| 21 | 21 | import javax.ws.rs.FormParam; |
| 22 | +import javax.ws.rs.GET; |
|
| 22 | 23 | import javax.ws.rs.POST; |
| 23 | 24 | import javax.ws.rs.Path; |
| 24 | 25 | import javax.ws.rs.Produces; |
| 26 | +import javax.ws.rs.QueryParam; |
|
| 25 | 27 | import javax.ws.rs.core.Context; |
| 26 | 28 | import javax.ws.rs.core.Response; |
| 27 | 29 | import javax.ws.rs.core.Response.Status; |
| ... | ... | @@ -49,8 +51,8 @@ public class CompareServersResource extends AbstractSailingServerResource { |
| 49 | 51 | |
| 50 | 52 | public static final Logger logger = Logger.getLogger(CompareServersResource.class.getName()); |
| 51 | 53 | |
| 52 | - private static final String LEADERBOARDGROUPSPATH = "/sailingserver/api/v1/leaderboardgroups"; |
|
| 53 | - private static final String LEADERBOARDGROUPSIDENTIFIABLEPATH = LEADERBOARDGROUPSPATH+"/identifiable"; |
|
| 54 | + private static final String LEADERBOARDGROUPSPATH = "/sailingserver/api"+LeaderboardGroupsResource.V1_LEADERBOARDGROUPS; |
|
| 55 | + private static final String LEADERBOARDGROUPSIDENTIFIABLEPATH = LEADERBOARDGROUPSPATH+LeaderboardGroupsResource.IDENTIFIABLE; |
|
| 54 | 56 | public static final String SERVER1_FORM_PARAM = "server1"; |
| 55 | 57 | public static final String SERVER2_FORM_PARAM = "server2"; |
| 56 | 58 | public static final String USER1_FORM_PARAM = "user1"; |
| ... | ... | @@ -106,6 +108,25 @@ public class CompareServersResource extends AbstractSailingServerResource { |
| 106 | 108 | UriInfo uriInfo; |
| 107 | 109 | |
| 108 | 110 | /** |
| 111 | + * Forwards to the POST method; user authentication is taken from the request's authentication. Therefore, no |
|
| 112 | + * separate authentications for the two servers to compare can be provided. The server receiving this request will |
|
| 113 | + * act as the default for "server1". This is a convenience method for quick comparisons of two |
|
| 114 | + * servers without needing to provide authentication information, assuming that the server receiving this request |
|
| 115 | + * shares its security service with the server specified as "server2" through replication, and that the user |
|
| 116 | + * authenticated for this request has access to both servers. For more complex use cases, use the POST method |
|
| 117 | + * directly. |
|
| 118 | + */ |
|
| 119 | + @GET |
|
| 120 | + @Produces("application/json;charset=UTF-8") |
|
| 121 | + public Response compareServersGet( |
|
| 122 | + @QueryParam(SERVER1_FORM_PARAM) String server1, |
|
| 123 | + @QueryParam(SERVER2_FORM_PARAM) String server2, |
|
| 124 | + @QueryParam(LEADERBOARDGROUP_UUID_FORM_PARAM) Set<String> uuidset) throws MalformedURLException { |
|
| 125 | + return compareServers(server1, server2, uuidset, /* user1 */ null, /* user2 */ null, /* password1 */ null, |
|
| 126 | + /* password2 */ null, /* bearer1 */ null, /* bearer2 */ null); |
|
| 127 | + } |
|
| 128 | + |
|
| 129 | + /** |
|
| 109 | 130 | * @param server1 |
| 110 | 131 | * optional; if not provided, the server receiving this request will act as the default for "server1" |
| 111 | 132 | * @param user1 |
| ... | ... | @@ -204,7 +225,7 @@ public class CompareServersResource extends AbstractSailingServerResource { |
| 204 | 225 | } |
| 205 | 226 | } |
| 206 | 227 | } |
| 207 | - JSONObject json = new JSONObject(); |
|
| 228 | + final JSONObject json = new JSONObject(); |
|
| 208 | 229 | for (Entry<String, Set<Object>> entry : result.entrySet()) { |
| 209 | 230 | json.put(entry.getKey(), entry.getValue()); |
| 210 | 231 | } |
java/com.sap.sailing.server.gateway/webservices/api/v1/compareServers.html
| ... | ... | @@ -33,7 +33,7 @@ |
| 33 | 33 | <td>json</td> |
| 34 | 34 | </tr> |
| 35 | 35 | <tr> |
| 36 | - <td>Mandatory query parameters:</td> |
|
| 36 | + <td>Mandatory form parameters:</td> |
|
| 37 | 37 | <td> |
| 38 | 38 | <div>server2, the hostname for the second server.</div> |
| 39 | 39 | </td> |
| ... | ... | @@ -146,6 +146,48 @@ |
| 146 | 146 | </pre> |
| 147 | 147 | </td> |
| 148 | 148 | </tr> |
| 149 | + <tr> |
|
| 150 | + <td>Webservice Type:</td> |
|
| 151 | + <td>GET</td> |
|
| 152 | + </tr> |
|
| 153 | + <tr> |
|
| 154 | + <td>Output format:</td> |
|
| 155 | + <td>json</td> |
|
| 156 | + </tr> |
|
| 157 | + <tr> |
|
| 158 | + <td>Mandatory query parameters:</td> |
|
| 159 | + <td> |
|
| 160 | + <div>server2, the hostname for the second server.</div> |
|
| 161 | + </td> |
|
| 162 | + </tr> |
|
| 163 | + <tr> |
|
| 164 | + <td>Optional query parameters:</td> |
|
| 165 | + <td> |
|
| 166 | + <div><tt>server1</tt>, the hostname for the first server. Defaults to the target of your request</div> |
|
| 167 | + <div><tt>leaderboardgroupUUID[]</tt>, the leaderboardgroup UUID you want to compare. |
|
| 168 | + Use multiple times if you need to compare more than one leaderboardgroup. Don't specify if you'd like |
|
| 169 | + to compare <em>all</em> leaderboard groups of both servers with each other.</div> |
|
| 170 | + </td> |
|
| 171 | + The GET method can easily be used from a Browser UI. It uses any session/cookie credentials or may be used |
|
| 172 | + with basic authentication. No authentication information can be provided as query parameters with the GET method. |
|
| 173 | + </tr> |
|
| 174 | + <tr> |
|
| 175 | + <td>Request method:</td> |
|
| 176 | + <td>GET</td> |
|
| 177 | + </tr> |
|
| 178 | + <tr> |
|
| 179 | + <td>Example:</td> |
|
| 180 | + <td>curl http://127.0.0.1:8888/sailingserver/api/v1/compareservers?server2=127.0.0.1:8889&leaderboardgroupUUID[]=0334e85e-ebc9-4835-b44d-5db71245b9e9&leaderboardgroupUUID[]=78476ac8-5519-4c3f-9c6d-670468edf4fc" -H "Authorization: Basic YWRtaW46YWRtaW4="<br> |
|
| 181 | + produces output:<pre> |
|
| 182 | + { |
|
| 183 | + 127.0.0.1:8889 [] |
|
| 184 | + 127.0.0.1:8888 [] |
|
| 185 | + } |
|
| 186 | + </pre> |
|
| 187 | + </td> |
|
| 188 | + </tr> |
|
| 189 | + </td> |
|
| 190 | + </tr> |
|
| 149 | 191 | </table> |
| 150 | 192 | <div style="height: 1em;"></div> |
| 151 | 193 | <a href="index.html">Back to Web Service Overview</a> |
java/com.sap.sailing.www/release_notes_admin.html
| ... | ... | @@ -23,6 +23,18 @@ |
| 23 | 23 | <div class="mainContent"> |
| 24 | 24 | <h2 class="releaseHeadline">Release Notes - Administration Console</h2> |
| 25 | 25 | <div class="innerContent"> |
| 26 | + <h2 class="articleSubheadline">February 2026</h2> |
|
| 27 | + <ul class="bulletList"> |
|
| 28 | + <li>Implemented an automated procedure for upgrading the ARCHIVE server. This is now available |
|
| 29 | + through the "Upgrade" action icon in the landscape management panel for "phase 1" (launching, |
|
| 30 | + setting https://archive-candidate.sapsailing.com as redirect, and scheduling periodic |
|
| 31 | + checks in the background for completion of loading and calculations as well as a final |
|
| 32 | + "compare servers"), and the "Activate ARCHIVE Candidate" action icon for "phase 2" which |
|
| 33 | + will then, after the user has confirmed having done some manual spot checks, rotate the |
|
| 34 | + new candidate to the live server, and the former live server to the "fail-over" role, |
|
| 35 | + renaming instances, terminating the old "fail-over" instance and updating all |
|
| 36 | + reverse proxies in the landscape accordingly. User information is sent via e-mail.</li> |
|
| 37 | + </ul> |
|
| 26 | 38 | <h2 class="articleSubheadline">January 2026</h2> |
| 27 | 39 | <ul class="bulletList"> |
| 28 | 40 | <li>Extended RabbitMQ channel heartbeat interval to 1h. This will suffice even for |
java/com.sap.sse.gwt/resources/com/sap/sse/gwt/client/images/check.png
| ... | ... | Binary files /dev/null and b/java/com.sap.sse.gwt/resources/com/sap/sse/gwt/client/images/check.png differ |
java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/IconResources.java
| ... | ... | @@ -85,4 +85,7 @@ public interface IconResources extends ClientBundle { |
| 85 | 85 | |
| 86 | 86 | @Source("images/command_symbol.png") |
| 87 | 87 | ImageResource commandSymbol(); |
| 88 | + |
|
| 89 | + @Source("images/check.png") |
|
| 90 | + ImageResource check(); |
|
| 88 | 91 | } |
java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java
| ... | ... | @@ -281,7 +281,7 @@ public abstract class DataEntryDialog<T> { |
| 281 | 281 | * |
| 282 | 282 | * @see #createSuggestBox(SuggestOracle) |
| 283 | 283 | */ |
| 284 | - protected SuggestBox createSuggestBox(Iterable<String> suggestValues) { |
|
| 284 | + public SuggestBox createSuggestBox(Iterable<String> suggestValues) { |
|
| 285 | 285 | List<String> suggestValuesAsCollection = new ArrayList<>(); |
| 286 | 286 | Util.addAll(suggestValues, suggestValuesAsCollection); |
| 287 | 287 | final MultiWordSuggestOracle oracle = new MultiWordSuggestOracle(); |
| ... | ... | @@ -299,7 +299,7 @@ public abstract class DataEntryDialog<T> { |
| 299 | 299 | * |
| 300 | 300 | * @see SuggestBox#SuggestBox(SuggestOracle) |
| 301 | 301 | */ |
| 302 | - protected SuggestBox createSuggestBox(SuggestOracle suggestOracle) { |
|
| 302 | + public SuggestBox createSuggestBox(SuggestOracle suggestOracle) { |
|
| 303 | 303 | final SuggestBox result = new SuggestBox(suggestOracle); |
| 304 | 304 | ensureHasValueIsValidated(result.getValueBox()); |
| 305 | 305 | ensureChangeableIsValidated(result.getValueBox()); |
java/com.sap.sse.landscape.aws.test/src/com/sap/sse/landscape/aws/MongoUriParserTest.java
| ... | ... | @@ -36,12 +36,12 @@ public class MongoUriParserTest { |
| 36 | 36 | when(host1.getPublicAddress()).thenReturn(wwwExampleCom); |
| 37 | 37 | when(host1.getPrivateAddress()).thenReturn(wwwExampleCom); |
| 38 | 38 | when(host1.getHostname()).thenReturn(WWW_EXAMPLE_COM); |
| 39 | - when(landscape.getHostByPrivateIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(wwwExampleCom.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host1); |
|
| 39 | + when(landscape.getHostByPrivateDnsNameOrIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(wwwExampleCom.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host1); |
|
| 40 | 40 | final InetAddress loopback = InetAddress.getLoopbackAddress(); |
| 41 | 41 | final AwsInstance<String> host2 = mock(AwsInstance.class); |
| 42 | 42 | when(host2.getPrivateAddress()).thenReturn(loopback); |
| 43 | 43 | when(host2.getHostname()).thenReturn(loopback.getHostAddress()); |
| 44 | - when(landscape.getHostByPrivateIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(loopback.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host2); |
|
| 44 | + when(landscape.getHostByPrivateDnsNameOrIpAddress(ArgumentMatchers.any(Region.class), ArgumentMatchers.contains(loopback.getHostAddress()), ArgumentMatchers.any(HostSupplier.class))).thenReturn(host2); |
|
| 45 | 45 | parser = new MongoUriParser<String>(landscape, new AwsRegion("eu-west-2", landscape)); |
| 46 | 46 | } |
| 47 | 47 |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsInstance.java
| ... | ... | @@ -53,6 +53,8 @@ public interface AwsInstance<ShardingKey> extends Host { |
| 53 | 53 | default String getId() { |
| 54 | 54 | return getInstanceId(); |
| 55 | 55 | } |
| 56 | + |
|
| 57 | + void setTerminationProtection(boolean terminationProtection); |
|
| 56 | 58 | |
| 57 | 59 | void terminate(); |
| 58 | 60 |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/AwsLandscape.java
| ... | ... | @@ -185,7 +185,7 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 185 | 185 | * private key; see also {@link #getKeyPairInfo(Region, String)} |
| 186 | 186 | * @param userData |
| 187 | 187 | * zero or more strings representing the user data to be passed to the instance; multiple strings will be |
| 188 | - * concatenated, using the line separator to join them. The instance is able to read the user data throuh |
|
| 188 | + * concatenated, using the line separator to join them. The instance is able to read the user data through |
|
| 189 | 189 | * the AWS SDK installed on the instance. |
| 190 | 190 | */ |
| 191 | 191 | default <HostT extends AwsInstance<ShardingKey>> HostT launchHost( |
| ... | ... | @@ -262,7 +262,7 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 262 | 262 | */ |
| 263 | 263 | <HostT extends AwsInstance<ShardingKey>> Iterable<HostT> getRunningHostsWithTag(Region region, String tagName, HostSupplier<ShardingKey, HostT> hostSupplier); |
| 264 | 264 | |
| 265 | - <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateIpAddress(Region region, String publicIpAddress, |
|
| 265 | + <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateDnsNameOrIpAddress(Region region, String privateDnsNameOrIpAddress, |
|
| 266 | 266 | HostSupplier<ShardingKey, HostT> hostSupplier); |
| 267 | 267 | |
| 268 | 268 | <HostT extends AwsInstance<ShardingKey>> HostT getHostByPublicIpAddress(Region region, String publicIpAddress, |
| ... | ... | @@ -288,6 +288,8 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 288 | 288 | */ |
| 289 | 289 | SSHKeyPair importKeyPair(Region region, byte[] publicKey, byte[] encryptedPrivateKey, String keyName) throws JSchException; |
| 290 | 290 | |
| 291 | + void setTerminationProtection(AwsInstance<ShardingKey> host, boolean terminationProtection); |
|
| 292 | + |
|
| 291 | 293 | void terminate(AwsInstance<ShardingKey> host); |
| 292 | 294 | |
| 293 | 295 | /** |
| ... | ... | @@ -329,7 +331,7 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 329 | 331 | |
| 330 | 332 | Instance getInstanceByPublicIpAddress(Region region, String publicIpAddress); |
| 331 | 333 | |
| 332 | - Instance getInstanceByPrivateIpAddress(Region region, String publicIpAddress); |
|
| 334 | + Instance getInstanceByPrivateDnsNameOrIpAddress(Region region, String privateDnsNameOrIpAddress); |
|
| 333 | 335 | |
| 334 | 336 | /** |
| 335 | 337 | * @param hostname |
| ... | ... | @@ -890,4 +892,6 @@ public interface AwsLandscape<ShardingKey> extends Landscape<ShardingKey> { |
| 890 | 892 | * Removes hosts from an IP-based target group. |
| 891 | 893 | */ |
| 892 | 894 | void removeIpTargetFromTargetGroup(TargetGroup<ShardingKey> targetGroup, Iterable<AwsInstance<ShardingKey>> hosts); |
| 895 | + |
|
| 896 | + void setInstanceName(AwsInstance<ShardingKey> host, String newInstanceName); |
|
| 893 | 897 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/MongoUriParser.java
| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | package com.sap.sse.landscape.aws; |
| 2 | 2 | |
| 3 | -import java.net.InetAddress; |
|
| 4 | 3 | import java.net.URI; |
| 5 | 4 | import java.net.URISyntaxException; |
| 6 | 5 | import java.net.UnknownHostException; |
| ... | ... | @@ -156,8 +155,7 @@ public class MongoUriParser<ShardingKey> { |
| 156 | 155 | * If the host isn't found in the landscape, the {@link Pair#getA()} component of the pair returned will be {@code null}. |
| 157 | 156 | */ |
| 158 | 157 | private Pair<AwsInstance<ShardingKey>, Integer> getHostAndPort(String hostname, Integer optionalPort) throws UnknownHostException { |
| 159 | - final InetAddress address = InetAddress.getByName(hostname); |
|
| 160 | - final AwsInstance<ShardingKey> hostByPrivateIp = landscape.getHostByPrivateIpAddress(region, address.getHostAddress(), AwsInstanceImpl::new); |
|
| 161 | - return new Pair<>(hostByPrivateIp, optionalPort); |
|
| 158 | + final AwsInstance<ShardingKey> hostByPrivateDnsOrIp = landscape.getHostByPrivateDnsNameOrIpAddress(region, hostname, AwsInstanceImpl::new); |
|
| 159 | + return new Pair<>(hostByPrivateDnsOrIp, optionalPort); |
|
| 162 | 160 | } |
| 163 | 161 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/ReverseProxy.java
| ... | ... | @@ -2,6 +2,8 @@ package com.sap.sse.landscape.aws; |
| 2 | 2 | |
| 3 | 3 | import java.util.Optional; |
| 4 | 4 | import java.util.UUID; |
| 5 | + |
|
| 6 | +import com.sap.sse.common.Util.Pair; |
|
| 5 | 7 | import com.sap.sse.landscape.Host; |
| 6 | 8 | import com.sap.sse.landscape.Log; |
| 7 | 9 | import com.sap.sse.landscape.application.ApplicationProcess; |
| ... | ... | @@ -124,4 +126,13 @@ public interface ReverseProxy<ShardingKey, MetricsT extends ApplicationProcessMe |
| 124 | 126 | * member instances and their availability zones. |
| 125 | 127 | */ |
| 126 | 128 | String getTargetGroupHealthCheckPath(String targetGroupArn); |
| 129 | + |
|
| 130 | + /** |
|
| 131 | + * Fetches the ARCHIVE_IP and ARCHIVE_FAILOVER_IP definitions from the 000-macros.conf file in the reverse proxy and returns |
|
| 132 | + * them, in this order, as strings. |
|
| 133 | + */ |
|
| 134 | + Pair<String, String> getArchiveAndFailoverIPs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
|
| 135 | + |
|
| 136 | + void setArchiveAndFailoverIPs(String productionArchiveServerInternalIPAddress, String failoverArchiveServerInternalIPAddress, |
|
| 137 | + Optional<String> ofNullable, byte[] privateKeyEncryptionPassphrase) throws Exception; |
|
| 127 | 138 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxy.java
| ... | ... | @@ -6,7 +6,9 @@ import java.util.Optional; |
| 6 | 6 | import java.util.UUID; |
| 7 | 7 | import java.util.logging.Level; |
| 8 | 8 | import java.util.logging.Logger; |
| 9 | + |
|
| 9 | 10 | import com.sap.sse.common.Duration; |
| 11 | +import com.sap.sse.common.Util.Pair; |
|
| 10 | 12 | import com.sap.sse.landscape.Host; |
| 11 | 13 | import com.sap.sse.landscape.RotatingFileBasedLog; |
| 12 | 14 | import com.sap.sse.landscape.application.ApplicationProcess; |
| ... | ... | @@ -76,6 +78,20 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 76 | 78 | private static final String HOME_ARCHIVE_REDIRECT_MACRO = "Home-ARCHIVE"; |
| 77 | 79 | private static final String EVENT_ARCHIVE_REDIRECT_MACRO = "Event-ARCHIVE"; |
| 78 | 80 | private static final String SERIES_ARCHIVE_REDIRECT_MACRO = "Series-ARCHIVE"; |
| 81 | + private static final String CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION = "000-macros"+CONFIG_FILE_EXTENSION; |
|
| 82 | + |
|
| 83 | + /** |
|
| 84 | + * Name of the "macro"/variable definition used in the file identified by {@link #CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION} |
|
| 85 | + * that specifies the internal IP address of the primary ARCHIVE server to use. |
|
| 86 | + */ |
|
| 87 | + private static final String ARCHIVE_IP = "ARCHIVE_IP"; |
|
| 88 | + |
|
| 89 | + /** |
|
| 90 | + * Name of the "macro"/variable definition used in the file identified by {@link #CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION} |
|
| 91 | + * that specifies the internal IP address of the fail-over ARCHIVE server to use. |
|
| 92 | + */ |
|
| 93 | + private static final String ARCHIVE_FAILOVER_IP = "ARCHIVE_FAILOVER_IP"; |
|
| 94 | + |
|
| 79 | 95 | private final AwsInstance<ShardingKey> host; |
| 80 | 96 | |
| 81 | 97 | public ApacheReverseProxy(AwsLandscape<ShardingKey> landscape, AwsInstance<ShardingKey> host) { |
| ... | ... | @@ -101,10 +117,35 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 101 | 117 | */ |
| 102 | 118 | public void rotateLogs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 103 | 119 | final String command = "logrotate --force -v /etc/logrotate.d/httpd 2>&1; echo \"logrotate done\""; |
| 104 | - logger.info("Standard output from forced log rotate on " + this.getHostname() + ": " + runCommandAndReturnStdoutAndStderr(command, "Standard error from logrotate ", |
|
| 120 | + logger.info("Standard output from forced log rotate on " + this.getHostname() + ": " + runCommandAndReturnStdoutAndLogStderr(command, "Standard error from logrotate ", |
|
| 105 | 121 | Level.ALL, optionalKeyName, privateKeyEncryptionPassphrase)); |
| 106 | 122 | } |
| 107 | 123 | |
| 124 | + @Override |
|
| 125 | + public Pair<String, String> getArchiveAndFailoverIPs(Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 126 | + final String absolute000MacrosConfigFilePath = getAbsoluteConfigFilePath(CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION); |
|
| 127 | + final String command = "cat "+absolute000MacrosConfigFilePath+" | grep \"^Define "+ARCHIVE_IP+"\" | sed -e 's/^Define "+ARCHIVE_IP+" //'; " |
|
| 128 | + + "cat "+absolute000MacrosConfigFilePath+" | grep \"^Define "+ARCHIVE_FAILOVER_IP+"\" | sed -e 's/^Define "+ARCHIVE_FAILOVER_IP+" //'"; |
|
| 129 | + final String[] archiveAndFailoverIPs = runCommandAndReturnStdoutAndLogStderr(command, |
|
| 130 | + "Standard error from getting "+ARCHIVE_IP+" and "+ARCHIVE_FAILOVER_IP+": ", |
|
| 131 | + Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase).split("\n"); |
|
| 132 | + return new Pair<>(archiveAndFailoverIPs[0], archiveAndFailoverIPs[1]); |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + @Override |
|
| 136 | + public void setArchiveAndFailoverIPs(String productionArchiveIP, String failoverArchiveIP, Optional<String> optionalKeyName, |
|
| 137 | + byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 138 | + final String absolute000MacrosConfigFilePath = getAbsoluteConfigFilePath(CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION); |
|
| 139 | + final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 140 | + String patch000MacrosCommand = "su - " + CONFIG_USER + " -c 'cd " + CONFIG_REPO_PATH + " && git checkout " |
|
| 141 | + + CONFIG_REPO_MAIN_BRANCH_NAME |
|
| 142 | + + " && sed -i -e \"s/^Define "+ARCHIVE_IP+" .*$/Define "+ARCHIVE_IP+" "+productionArchiveIP+"/\" -e \"s/^Define "+ARCHIVE_FAILOVER_IP+" .*$/Define "+ARCHIVE_FAILOVER_IP+" "+failoverArchiveIP+"/\" "+absolute000MacrosConfigFilePath |
|
| 143 | + + " && " + createCommitAndPushString(CONFIG_FILE_FOR_ARCHIVE_AND_FAILOVER_DEFINITION, "Switching to new ARCHIVE server", /* performPush */ true) |
|
| 144 | + + "'"; // concludes the "su"; re-loading is expected to happen through the post-receive hook triggered by the push |
|
| 145 | + final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr(patch000MacrosCommand, "Standard error from switching to new ARCHIVE server", Level.WARNING); |
|
| 146 | + logger.info("Stdout from upgrading to new ARCHIVE: "+stdout); |
|
| 147 | + } |
|
| 148 | + |
|
| 108 | 149 | /** |
| 109 | 150 | * Creates a redirect file and updates the git repo. |
| 110 | 151 | * |
| ... | ... | @@ -132,33 +173,33 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 132 | 173 | + CONFIG_REPO_MAIN_BRANCH_NAME + " && echo \"Use " + macroName + " " + hostname + " " |
| 133 | 174 | + String.join(" ", macroArguments) + "\" > " + getAbsoluteConfigFilePath(configFileNameForHostname); |
| 134 | 175 | if (doCommit) { |
| 135 | - command = command + " && cd " |
|
| 176 | + command = command + " && cd " |
|
| 136 | 177 | + CONFIG_REPO_PATH + " && " + createCommitAndPushString(configFileNameForHostname, |
| 137 | 178 | "Set " + configFileNameForHostname + " redirect", doPush); |
| 138 | 179 | } |
| 139 | 180 | command = command + "'; service httpd reload"; // Concludes the su. And reloads as the root user. |
| 140 | 181 | logger.info("Standard output from setting up the re-direct for " + hostname |
| 141 | 182 | + " and reloading the Apache httpd server: " |
| 142 | - + runCommandAndReturnStdoutAndStderr(command, |
|
| 183 | + + runCommandAndReturnStdoutAndLogStderr(command, |
|
| 143 | 184 | "Standard error from setting up the re-direct for " + hostname |
| 144 | 185 | + " and reloading the Apache httpd server: ", |
| 145 | 186 | Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase)); |
| 146 | 187 | } |
| 147 | - |
|
| 188 | + |
|
| 148 | 189 | /** |
| 149 | 190 | * Overloads {@link #setRedirect(String, String, String, Optional, byte[], boolean, boolean, String...)} and |
| 150 | 191 | * defaults to {@code true} and {@code true} for committing and pushing. |
| 151 | 192 | * |
| 152 | 193 | * @see #setRedirect(String, String, String, Optional, byte[], boolean, boolean, String...) |
| 153 | 194 | */ |
| 154 | - public void setRedirect(String configFileNameForHostname, String macroName, String hostname, |
|
| 195 | + private void setRedirect(String configFileNameForHostname, String macroName, String hostname, |
|
| 155 | 196 | Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase, String... macroArguments) |
| 156 | 197 | throws Exception { |
| 157 | 198 | setRedirect(configFileNameForHostname, macroName, hostname, optionalKeyName, privateKeyEncryptionPassphrase, |
| 158 | 199 | /* doCommit */ true, /* doPush */ true, macroArguments); |
| 159 | 200 | } |
| 160 | 201 | |
| 161 | - private String runCommandAndReturnStdoutAndStderr(String command, String stderrLogPrefix, Level stderrLogLevel, |
|
| 202 | + private String runCommandAndReturnStdoutAndLogStderr(String command, String stderrLogPrefix, Level stderrLogLevel, |
|
| 162 | 203 | Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 163 | 204 | final SshCommandChannel sshChannel = getHost().createRootSshChannel(TIMEOUT, optionalKeyName, privateKeyEncryptionPassphrase); |
| 164 | 205 | final String stdout = sshChannel.runCommandAndReturnStdoutAndLogStderr(command, stderrLogPrefix, stderrLogLevel); |
| ... | ... | @@ -166,10 +207,16 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 166 | 207 | } |
| 167 | 208 | |
| 168 | 209 | /** |
| 169 | - * Creates a command, that can be ran on an instance to commit, and optionally push, changes to a file (within a git repository). ASSUMES the command is ran from within the repository. |
|
| 170 | - * @param editedFileName The file name edited, created or deleted to commit. This includes the {@link #CONFIG_FILE_EXTENSION}, but not a path. The method appends the relative path. |
|
| 171 | - * @param commitMsg The commit message, without escaped speech marks. |
|
| 172 | - * @param performPush Boolean indicating whether to push changes or not. True for performing a push. |
|
| 210 | + * Creates a command, that can be ran on an instance to commit, and optionally push, changes to a file (within a git |
|
| 211 | + * repository). ASSUMES the command is ran from within the repository. |
|
| 212 | + * |
|
| 213 | + * @param editedFileName |
|
| 214 | + * The file name edited, created or deleted to commit. This includes the {@link #CONFIG_FILE_EXTENSION}, |
|
| 215 | + * but not a path. The method appends the relative path. |
|
| 216 | + * @param commitMsg |
|
| 217 | + * The commit message, without escaped speech marks. |
|
| 218 | + * @param performPush |
|
| 219 | + * Boolean indicating whether to push changes or not. True for performing a push. |
|
| 173 | 220 | * @return Returns the created command (in String form) to perform a commit and optional push. |
| 174 | 221 | */ |
| 175 | 222 | private String createCommitAndPushString(String editedFileName, String commitMsg, boolean performPush) { |
| ... | ... | @@ -264,7 +311,6 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 264 | 311 | removeRedirect(configFileName, hostname, optionalKeyName, privateKeyEncryptionPassphrase); |
| 265 | 312 | } |
| 266 | 313 | |
| 267 | - |
|
| 268 | 314 | /** |
| 269 | 315 | * @param configFileName The name of the file to remove. |
| 270 | 316 | * @param hostname The hostname which was removed. |
| ... | ... | @@ -283,7 +329,7 @@ implements com.sap.sse.landscape.Process<RotatingFileBasedLog, MetricsT> { |
| 283 | 329 | command.append("'; service httpd reload;"); // ' closes the su. The reload must be run as the root user. |
| 284 | 330 | logger.info("Standard output from removing the re-direct for " + hostname |
| 285 | 331 | + " and reloading the Apache httpd server: " |
| 286 | - + runCommandAndReturnStdoutAndStderr(command.toString(), |
|
| 332 | + + runCommandAndReturnStdoutAndLogStderr(command.toString(), |
|
| 287 | 333 | "Standard error from removing the re-direct for " + hostname |
| 288 | 334 | + " and reloading the Apache httpd server: ", |
| 289 | 335 | Level.INFO, optionalKeyName, privateKeyEncryptionPassphrase)); |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/ApacheReverseProxyCluster.java
| ... | ... | @@ -14,6 +14,7 @@ import java.util.logging.Logger; |
| 14 | 14 | import com.sap.sse.landscape.aws.LandscapeConstants; |
| 15 | 15 | import com.sap.sse.common.Duration; |
| 16 | 16 | import com.sap.sse.common.Util; |
| 17 | +import com.sap.sse.common.Util.Pair; |
|
| 17 | 18 | import com.sap.sse.concurrent.ConsumerWithException; |
| 18 | 19 | import com.sap.sse.landscape.Landscape; |
| 19 | 20 | import com.sap.sse.landscape.Log; |
| ... | ... | @@ -252,4 +253,19 @@ public class ApacheReverseProxyCluster<ShardingKey, MetricsT extends Application |
| 252 | 253 | byte[] privateKeyEncryptionPassphrase) throws Exception { |
| 253 | 254 | setRedirect(proxy -> proxy.removeRedirect(scope, optionalKeyName, privateKeyEncryptionPassphrase)); |
| 254 | 255 | } |
| 256 | + |
|
| 257 | + @Override |
|
| 258 | + public Pair<String, String> getArchiveAndFailoverIPs(Optional<String> optionalKeyName, |
|
| 259 | + byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 260 | + return getReverseProxies().iterator().next().getArchiveAndFailoverIPs(optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 261 | + } |
|
| 262 | + |
|
| 263 | + @Override |
|
| 264 | + public void setArchiveAndFailoverIPs(String productionArchiveServerInternalIPAddress, String failoverArchiveServerInternalIPAddress, |
|
| 265 | + Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception { |
|
| 266 | + if (getReverseProxies().iterator().hasNext()) { |
|
| 267 | + final ApacheReverseProxy<ShardingKey, MetricsT, ProcessT> proxy = getReverseProxies().iterator().next(); |
|
| 268 | + proxy.setArchiveAndFailoverIPs(productionArchiveServerInternalIPAddress, failoverArchiveServerInternalIPAddress, optionalKeyName, privateKeyEncryptionPassphrase); |
|
| 269 | + } |
|
| 270 | + } |
|
| 255 | 271 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsApplicationProcessImpl.java
| ... | ... | @@ -155,7 +155,7 @@ implements AwsApplicationProcess<ShardingKey, MetricsT, ProcessT> { |
| 155 | 155 | } catch (Exception e) { |
| 156 | 156 | logger.info("Unable to find master by public IP "+ipAddressOrHostname+" ("+e.getMessage()+"); trying to look up master assuming "+ipAddressOrHostname+" is the private IP"); |
| 157 | 157 | try { |
| 158 | - host = landscape.getHostByPrivateIpAddress(getHost().getRegion(), ipAddressOrHostname, hostSupplier); |
|
| 158 | + host = landscape.getHostByPrivateDnsNameOrIpAddress(getHost().getRegion(), ipAddressOrHostname, hostSupplier); |
|
| 159 | 159 | } catch (Exception f) { |
| 160 | 160 | logger.info("Unable to find master by private IP "+ipAddressOrHostname+" ("+f.getMessage()+") either. Returning null."); |
| 161 | 161 | host = null; |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsInstanceImpl.java
| ... | ... | @@ -334,6 +334,11 @@ public class AwsInstanceImpl<ShardingKey> implements AwsInstance<ShardingKey> { |
| 334 | 334 | } |
| 335 | 335 | |
| 336 | 336 | @Override |
| 337 | + public void setTerminationProtection(boolean terminationProtection) { |
|
| 338 | + landscape.setTerminationProtection(this, terminationProtection); |
|
| 339 | + } |
|
| 340 | + |
|
| 341 | + @Override |
|
| 337 | 342 | public void terminate() { |
| 338 | 343 | landscape.terminate(this); |
| 339 | 344 | } |
java/com.sap.sse.landscape.aws/src/com/sap/sse/landscape/aws/impl/AwsLandscapeImpl.java
| ... | ... | @@ -645,24 +645,59 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 645 | 645 | public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPublicIpAddress(com.sap.sse.landscape.Region region, String publicIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) { |
| 646 | 646 | return getHost(region, getInstanceByPublicIpAddress(region, publicIpAddress), hostSupplier); |
| 647 | 647 | } |
| 648 | - |
|
| 648 | + |
|
| 649 | + private Instance getFirstInstance(DescribeInstancesResponse response) { |
|
| 650 | + for (Reservation reservation : response.reservations()) { |
|
| 651 | + for (Instance instance : reservation.instances()) { |
|
| 652 | + return instance; |
|
| 653 | + } |
|
| 654 | + } |
|
| 655 | + return null; |
|
| 656 | + } |
|
| 657 | + |
|
| 658 | + /** |
|
| 659 | + * Searches for an instance that matches the private dns name if this fails it tries the to resolve the private ip address |
|
| 660 | + */ |
|
| 649 | 661 | @Override |
| 650 | - public Instance getInstanceByPrivateIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress) { |
|
| 662 | + public Instance getInstanceByPrivateDnsNameOrIpAddress(com.sap.sse.landscape.Region region, String privateDnsNameOrIpAddress) { |
|
| 651 | 663 | InetAddress inetAddress; |
| 652 | 664 | try { |
| 653 | - inetAddress = InetAddress.getByName(privateIpAddress); |
|
| 654 | - return getEc2Client(getRegion(region)) |
|
| 655 | - .describeInstances(b->b.filters(Filter.builder().name("private-ip-address").values(inetAddress.getHostAddress()).build())).reservations() |
|
| 656 | - .iterator().next().instances().iterator().next(); |
|
| 665 | + DescribeInstancesResponse response = getEc2Client(getRegion(region)) |
|
| 666 | + .describeInstances(b -> b.filters( |
|
| 667 | + Filter.builder().name("private-dns-name").values(privateDnsNameOrIpAddress).build()) |
|
| 668 | + ); |
|
| 669 | + Instance instance = getFirstInstance(response); |
|
| 670 | + if (instance != null) { |
|
| 671 | + return instance; |
|
| 672 | + } |
|
| 673 | + |
|
| 674 | + inetAddress = InetAddress.getByName(privateDnsNameOrIpAddress); |
|
| 675 | + response = getEc2Client(getRegion(region)) |
|
| 676 | + .describeInstances(b -> b.filters( |
|
| 677 | + Filter.builder().name("private-ip-address").values(inetAddress.getHostAddress()).build()) |
|
| 678 | + ); |
|
| 679 | + instance = getFirstInstance(response); |
|
| 680 | + if (instance != null) { |
|
| 681 | + return instance; |
|
| 682 | + } |
|
| 657 | 683 | } catch (UnknownHostException | NoSuchElementException e) { |
| 658 | - logger.warning("IP address for "+privateIpAddress+" not found"); |
|
| 659 | - return null; |
|
| 684 | + logger.severe("An error occurred while trying to find the instance: " + e.getMessage()); |
|
| 660 | 685 | } |
| 661 | - } |
|
| 662 | 686 | |
| 687 | + logger.warning("Instance for " + privateDnsNameOrIpAddress + " not found"); |
|
| 688 | + return null; |
|
| 689 | + } |
|
| 690 | + |
|
| 691 | + /** |
|
| 692 | + * Returns a host first by private dns name and alternatively by private ip address |
|
| 693 | + */ |
|
| 663 | 694 | @Override |
| 664 | - public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) { |
|
| 665 | - return getHost(region, getInstanceByPrivateIpAddress(region, privateIpAddress), hostSupplier); |
|
| 695 | + public <HostT extends AwsInstance<ShardingKey>> HostT getHostByPrivateDnsNameOrIpAddress(com.sap.sse.landscape.Region region, String privateIpAddress, HostSupplier<ShardingKey, HostT> hostSupplier) { |
|
| 696 | + final Instance instanceByPrivateDnsNameOrIpAddress = getInstanceByPrivateDnsNameOrIpAddress(region, privateIpAddress); |
|
| 697 | + if (instanceByPrivateDnsNameOrIpAddress == null) { |
|
| 698 | + throw new IllegalArgumentException("Couldn't find instance with IP/hostname "+privateIpAddress+" in region "+region); |
|
| 699 | + } |
|
| 700 | + return getHost(region, instanceByPrivateDnsNameOrIpAddress, hostSupplier); |
|
| 666 | 701 | } |
| 667 | 702 | |
| 668 | 703 | private Route53Client getRoute53Client() { |
| ... | ... | @@ -1038,6 +1073,21 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 1038 | 1073 | } |
| 1039 | 1074 | |
| 1040 | 1075 | @Override |
| 1076 | + public void setTerminationProtection(AwsInstance<ShardingKey> host, boolean terminationProtection) { |
|
| 1077 | + logger.info("Setting termination protection for instance "+host+" to "+terminationProtection); |
|
| 1078 | + getEc2Client(getRegion(host.getAvailabilityZone().getRegion())).modifyInstanceAttribute(b->b |
|
| 1079 | + .instanceId(host.getInstanceId()) |
|
| 1080 | + .disableApiTermination(a->a.value(terminationProtection))); |
|
| 1081 | + } |
|
| 1082 | + |
|
| 1083 | + @Override |
|
| 1084 | + public void setInstanceName(AwsInstance<ShardingKey> host, String newInstanceName) { |
|
| 1085 | + logger.info("Setting Name tag for instance "+host+" to "+newInstanceName); |
|
| 1086 | + getEc2Client(getRegion(host.getAvailabilityZone().getRegion())) |
|
| 1087 | + .createTags(b -> b.resources(host.getInstanceId()).tags(Tag.builder().key("Name").value(newInstanceName).build())); |
|
| 1088 | + } |
|
| 1089 | + |
|
| 1090 | + @Override |
|
| 1041 | 1091 | public void terminate(AwsInstance<ShardingKey> host) { |
| 1042 | 1092 | logger.info("Terminating instance "+host); |
| 1043 | 1093 | getEc2Client(getRegion(host.getAvailabilityZone().getRegion())).terminateInstances( |
| ... | ... | @@ -1195,7 +1245,7 @@ public class AwsLandscapeImpl<ShardingKey> implements AwsLandscape<ShardingKey> |
| 1195 | 1245 | DescribeTargetHealthRequest.builder().targetGroupArn(targetGroup.getTargetGroupArn()).build()) |
| 1196 | 1246 | .targetHealthDescriptions().forEach(targetHealthDescription -> { |
| 1197 | 1247 | if (targetHealthDescription.target().id().matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+")) { |
| 1198 | - AwsInstance<ShardingKey> awsInstance = getHostByPrivateIpAddress(targetGroup.getRegion(), targetHealthDescription.target().id().trim(), |
|
| 1248 | + AwsInstance<ShardingKey> awsInstance = getHostByPrivateDnsNameOrIpAddress(targetGroup.getRegion(), targetHealthDescription.target().id().trim(), |
|
| 1199 | 1249 | AwsInstanceImpl::new); |
| 1200 | 1250 | result.put(awsInstance, targetHealthDescription.targetHealth()); |
| 1201 | 1251 | } else { |
java/com.sap.sse.landscape/src/com/sap/sse/landscape/Landscape.java
| ... | ... | @@ -13,10 +13,12 @@ public interface Landscape<ShardingKey> { |
| 13 | 13 | * constant ("image-type"). The tag value then must match what the subclass wants. |
| 14 | 14 | */ |
| 15 | 15 | String IMAGE_TYPE_TAG_NAME = "image-type"; |
| 16 | + |
|
| 16 | 17 | /** |
| 17 | 18 | * The timeout for a host to come up |
| 18 | 19 | */ |
| 19 | 20 | Optional<Duration> WAIT_FOR_HOST_TIMEOUT = Optional.of(Duration.ONE_HOUR.times(2)); |
| 21 | + |
|
| 20 | 22 | /** |
| 21 | 23 | * The timeout for a running process to respond |
| 22 | 24 | */ |
java/com.sap.sse.landscape/src/com/sap/sse/landscape/application/ApplicationProcess.java
| ... | ... | @@ -165,6 +165,11 @@ extends Process<RotatingFileBasedLog, MetricsT> { |
| 165 | 165 | return Wait.wait(()->isReady(optionalTimeout), optionalTimeout, Duration.ONE_SECOND.times(5), Level.INFO, ""+this+" not yet ready"); |
| 166 | 166 | } |
| 167 | 167 | |
| 168 | + default boolean waitUntilAlive(Optional<Duration> optionalTimeout) throws TimeoutException, Exception { |
|
| 169 | + return Wait.wait(()->isAlive(optionalTimeout), success->success, /* retryOnException */ true, |
|
| 170 | + optionalTimeout, Duration.ONE_SECOND.times(5), Level.INFO, ""+this+" not yet alive"); |
|
| 171 | + } |
|
| 172 | + |
|
| 168 | 173 | Release getVersion(Optional<Duration> optionalTimeout, Optional<String> optionalKeyName, byte[] privateKeyEncryptionPassphrase) throws Exception; |
| 169 | 174 | |
| 170 | 175 | TimePoint getStartTimePoint(Optional<Duration> optionalTimeout) throws Exception; |
java/com.sap.sse.landscape/src/com/sap/sse/landscape/ssh/SshCommandChannelImpl.java
| ... | ... | @@ -37,10 +37,10 @@ public class SshCommandChannelImpl implements SshCommandChannel { |
| 37 | 37 | final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); |
| 38 | 38 | try { |
| 39 | 39 | sendCommandLineSynchronously(commandLine, stderr); |
| 40 | + final String result = getStreamContentsAsString(); |
|
| 40 | 41 | if (stderrLogLevel != null && stderr.size() > 0) { |
| 41 | 42 | logger.log(stderrLogLevel, (stderrLogPrefix==null?"":(stderrLogPrefix+": "))+stderr.toString()); |
| 42 | 43 | } |
| 43 | - final String result = getStreamContentsAsString(); |
|
| 44 | 44 | return result; |
| 45 | 45 | } finally { |
| 46 | 46 | disconnect(); |
| ... | ... | @@ -51,7 +51,7 @@ public class SshCommandChannelImpl implements SshCommandChannel { |
| 51 | 51 | public InputStream sendCommandLineSynchronously(String commandLine, OutputStream stderr) throws IOException, InterruptedException, JSchException { |
| 52 | 52 | stdout = channel.getInputStream(); |
| 53 | 53 | channel.setCommand(commandLine); |
| 54 | - channel.setExtOutputStream(stderr); |
|
| 54 | + channel.setErrStream(stderr); |
|
| 55 | 55 | channel.connect(/* timeout in milliseconds */ 5000); |
| 56 | 56 | return stdout; |
| 57 | 57 | } |
java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java
| ... | ... | @@ -1411,17 +1411,18 @@ implements ReplicableSecurityService, ClearStateTestSupport { |
| 1411 | 1411 | } |
| 1412 | 1412 | |
| 1413 | 1413 | /** |
| 1414 | - * Schedule a clean-up task to avoid leaking memory for the TimedLock objects; schedule it in two times the |
|
| 1415 | - * locking expiry of {@code timedLock}, but at least one hour, because if no authentication failure occurs |
|
| 1416 | - * for that IP/user agent combination, we will entirely remove the {@link TimedLock} from the map, |
|
| 1417 | - * effectively resetting that IP to a short default locking duration again; this way, if during the double |
|
| 1418 | - * expiration time another failed attempt is registered, we can still grow the locking duration because we have kept |
|
| 1419 | - * the {@link TimedLock} object available for a bit longer. Furthermore, for authentication requests, the |
|
| 1420 | - * responsible {@link Realm} will let authentication requests get to here only if not locked, so if we were to |
|
| 1421 | - * expunge entries immediately as they unlock, the locking duration could never grow.<p> |
|
| 1414 | + * Schedule a clean-up task to avoid leaking memory for the {@link TimedLock} objects; schedule it in two times the |
|
| 1415 | + * locking expiry of {@code timedLock}, but at least one hour, because if no authentication failure occurs for that |
|
| 1416 | + * IP/user agent combination, we will entirely remove the {@link TimedLock} from the map, effectively resetting that |
|
| 1417 | + * IP to a short default locking duration again; this way, if during the double expiration time another failed |
|
| 1418 | + * attempt is registered, we can still grow the locking duration because we have kept the {@link TimedLock} object |
|
| 1419 | + * available for a bit longer. Furthermore, for authentication requests, the responsible {@link Realm} will let |
|
| 1420 | + * authentication requests get to here only if not locked, so if we were to expunge entries immediately as they |
|
| 1421 | + * unlock, the locking duration could never grow. |
|
| 1422 | + * <p> |
|
| 1422 | 1423 | * |
| 1423 | - * With the minimum of one hour, we ensure that failing requests done at a slower rate still grow the locking |
|
| 1424 | - * expiry duration. |
|
| 1424 | + * With the minimum of one hour, we ensure that failing requests done at a slower rate still grow the locking expiry |
|
| 1425 | + * duration. |
|
| 1425 | 1426 | */ |
| 1426 | 1427 | private void scheduleCleanUpTask(final String clientIPOrNull, |
| 1427 | 1428 | final TimedLock timedLock, |
java/com.sap.sse.test/src/com/sap/sse/test/ThreadPoolNonDelayedTasksCountTest.java
| ... | ... | @@ -0,0 +1,70 @@ |
| 1 | +package com.sap.sse.test; |
|
| 2 | + |
|
| 3 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
|
| 4 | + |
|
| 5 | +import java.util.concurrent.ScheduledFuture; |
|
| 6 | +import java.util.concurrent.ScheduledThreadPoolExecutor; |
|
| 7 | +import java.util.concurrent.TimeUnit; |
|
| 8 | + |
|
| 9 | +import org.junit.jupiter.api.AfterEach; |
|
| 10 | +import org.junit.jupiter.api.BeforeEach; |
|
| 11 | +import org.junit.jupiter.api.Test; |
|
| 12 | + |
|
| 13 | +import com.sap.sse.common.Duration; |
|
| 14 | +import com.sap.sse.common.Util; |
|
| 15 | +import com.sap.sse.util.ThreadPoolUtil; |
|
| 16 | + |
|
| 17 | +/** |
|
| 18 | + * Tests the |
|
| 19 | + * {@link ThreadPoolUtil#getTasksDelayedByLessThan(java.util.concurrent.ScheduledExecutorService, com.sap.sse.common.Duration)} |
|
| 20 | + * method. |
|
| 21 | + * |
|
| 22 | + * @author Axel Uhl (d043530) |
|
| 23 | + * |
|
| 24 | + */ |
|
| 25 | +public class ThreadPoolNonDelayedTasksCountTest { |
|
| 26 | + private ScheduledThreadPoolExecutor defaultBackgroundThreadPoolExecutor; |
|
| 27 | + private int corePoolSize; |
|
| 28 | + |
|
| 29 | + @BeforeEach |
|
| 30 | + public void setUp() { |
|
| 31 | + defaultBackgroundThreadPoolExecutor = (ScheduledThreadPoolExecutor) ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor(); |
|
| 32 | + corePoolSize = defaultBackgroundThreadPoolExecutor.getCorePoolSize(); |
|
| 33 | + } |
|
| 34 | + |
|
| 35 | + @AfterEach |
|
| 36 | + public void tearDown() { |
|
| 37 | + for (final Runnable task : defaultBackgroundThreadPoolExecutor.getQueue()) { |
|
| 38 | + final ScheduledFuture<?> scheduledFuture = (ScheduledFuture<?>) task; |
|
| 39 | + scheduledFuture.cancel(/* mayInterruptIfRunning */ true); |
|
| 40 | + } |
|
| 41 | + } |
|
| 42 | + |
|
| 43 | + @Test |
|
| 44 | + public void testAddingDelayedAndUndelayedThenCounting() throws InterruptedException { |
|
| 45 | + final int IMMEDIATE_TASK_COUNT = 10000; |
|
| 46 | + final int DELAYED_TASK_COUNT = 100; |
|
| 47 | + final Duration DELAY = Duration.ONE_HOUR.times(2); |
|
| 48 | + for (int i=0; i<IMMEDIATE_TASK_COUNT; i++) { |
|
| 49 | + defaultBackgroundThreadPoolExecutor.submit(()->{ |
|
| 50 | + try { |
|
| 51 | + Thread.sleep(IMMEDIATE_TASK_COUNT); |
|
| 52 | + } catch (InterruptedException e) { |
|
| 53 | + e.printStackTrace(); |
|
| 54 | + } |
|
| 55 | + }); |
|
| 56 | + } |
|
| 57 | + for (int i=0; i<DELAYED_TASK_COUNT; i++) { |
|
| 58 | + defaultBackgroundThreadPoolExecutor.schedule(()->{ |
|
| 59 | + try { |
|
| 60 | + Thread.sleep(IMMEDIATE_TASK_COUNT); |
|
| 61 | + } catch (InterruptedException e) { |
|
| 62 | + e.printStackTrace(); |
|
| 63 | + } |
|
| 64 | + }, DELAY.asMillis(), TimeUnit.MILLISECONDS); |
|
| 65 | + } |
|
| 66 | + Thread.sleep(100); // wait for non-delayed tasks to get scheduled |
|
| 67 | + assertEquals(IMMEDIATE_TASK_COUNT-corePoolSize, Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan(defaultBackgroundThreadPoolExecutor, DELAY.divide(2)))); |
|
| 68 | + assertEquals(IMMEDIATE_TASK_COUNT+DELAYED_TASK_COUNT-corePoolSize, Util.size(ThreadPoolUtil.INSTANCE.getTasksDelayedByLessThan(defaultBackgroundThreadPoolExecutor, DELAY.times(2)))); |
|
| 69 | + } |
|
| 70 | +} |
java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java
| ... | ... | @@ -5,9 +5,12 @@ import java.util.concurrent.Callable; |
| 5 | 5 | import java.util.concurrent.ExecutorService; |
| 6 | 6 | import java.util.concurrent.Future; |
| 7 | 7 | import java.util.concurrent.ScheduledExecutorService; |
| 8 | +import java.util.concurrent.ScheduledFuture; |
|
| 8 | 9 | import java.util.concurrent.ScheduledThreadPoolExecutor; |
| 10 | +import java.util.concurrent.ThreadPoolExecutor; |
|
| 9 | 11 | import java.util.logging.Level; |
| 10 | 12 | |
| 13 | +import com.sap.sse.common.Duration; |
|
| 11 | 14 | import com.sap.sse.util.impl.ThreadPoolUtilImpl; |
| 12 | 15 | |
| 13 | 16 | public interface ThreadPoolUtil { |
| ... | ... | @@ -118,4 +121,11 @@ public interface ThreadPoolUtil { |
| 118 | 121 | Runnable associateWithSubjectIfAny(Runnable runnable); |
| 119 | 122 | |
| 120 | 123 | <T> Callable<T> associateWithSubjectIfAny(Callable<T> callable); |
| 124 | + |
|
| 125 | + /** |
|
| 126 | + * In the {@code executor}'s queue filters tasks for those with a delay less than {@code delayLessThan} and |
|
| 127 | + * returns the corresponding tasks. This can be used, e.g., to judge an executor's immediate workload or |
|
| 128 | + * give an estimate of the future workload mapped over time. |
|
| 129 | + */ |
|
| 130 | + Iterable<ScheduledFuture<?>> getTasksDelayedByLessThan(ThreadPoolExecutor executor, Duration delayLessThan); |
|
| 121 | 131 | } |
java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java
| ... | ... | @@ -8,6 +8,9 @@ import java.util.concurrent.ExecutionException; |
| 8 | 8 | import java.util.concurrent.ExecutorService; |
| 9 | 9 | import java.util.concurrent.Future; |
| 10 | 10 | import java.util.concurrent.ScheduledExecutorService; |
| 11 | +import java.util.concurrent.ScheduledFuture; |
|
| 12 | +import java.util.concurrent.ThreadPoolExecutor; |
|
| 13 | +import java.util.concurrent.TimeUnit; |
|
| 11 | 14 | import java.util.logging.Level; |
| 12 | 15 | import java.util.logging.Logger; |
| 13 | 16 | |
| ... | ... | @@ -15,6 +18,7 @@ import org.apache.shiro.SecurityUtils; |
| 15 | 18 | import org.apache.shiro.UnavailableSecurityManagerException; |
| 16 | 19 | import org.apache.shiro.subject.Subject; |
| 17 | 20 | |
| 21 | +import com.sap.sse.common.Duration; |
|
| 18 | 22 | import com.sap.sse.common.Util; |
| 19 | 23 | import com.sap.sse.util.ThreadPoolUtil; |
| 20 | 24 | |
| ... | ... | @@ -130,4 +134,11 @@ public class ThreadPoolUtilImpl implements ThreadPoolUtil { |
| 130 | 134 | public <T> Callable<T> associateWithSubjectIfAny(Callable<T> callable) { |
| 131 | 135 | return getSubjectOrNull().map(subject->subject.associateWith(callable)).orElse(callable); |
| 132 | 136 | } |
| 137 | + |
|
| 138 | + @Override |
|
| 139 | + public Iterable<ScheduledFuture<?>> getTasksDelayedByLessThan(ThreadPoolExecutor executor, |
|
| 140 | + Duration delayLessThan) { |
|
| 141 | + return Util.map(Util.filter(executor.getQueue(), task->((ScheduledFuture<?>) task).getDelay(TimeUnit.MILLISECONDS) < delayLessThan.asMillis()), |
|
| 142 | + task->(ScheduledFuture<?>) task); |
|
| 143 | + } |
|
| 133 | 144 | } |