• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanopub-query / 21000107044

14 Jan 2026 03:40PM UTC coverage: 70.874% (-0.4%) from 71.251%
21000107044

push

github

tkuhn
fix(StatusController): Report original exception when rollback fails

214 of 326 branches covered (65.64%)

Branch coverage included in aggregate %.

589 of 807 relevant lines covered (72.99%)

11.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.44
src/main/java/com/knowledgepixels/query/StatusController.java
1
package com.knowledgepixels.query;
2

3
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
4
import org.eclipse.rdf4j.model.Literal;
5
import org.eclipse.rdf4j.repository.RepositoryConnection;
6
import org.nanopub.vocabulary.NPA;
7

8
import java.util.Objects;
9

10
/**
11
 * Class to control the load status of the database.
12
 */
13
public class StatusController {
6✔
14

15
    /**
16
     * The load states in which the database can be.
17
     */
18
    public enum State {
9✔
19
        /**
20
         * The service is launching.
21
         */
22
        LAUNCHING,
18✔
23
        /**
24
         * The service is loading.
25
         */
26
        LOADING_INITIAL,
18✔
27
        /**
28
         * The service is loading updates.
29
         */
30
        LOADING_UPDATES,
18✔
31
        /**
32
         * The service is ready to serve requests.
33
         */
34
        READY,
18✔
35
    }
36

37
    /**
38
     * Get the singleton instance of the StatusController.
39
     *
40
     * @return the StatusController instance
41
     */
42
    public static StatusController get() {
43
        return instance;
6✔
44
    }
45

46
    private final static StatusController instance = new StatusController();
15✔
47

48
    private boolean initialized = false;
9✔
49
    private State state = null;
9✔
50
    private long lastCommittedCounter = -1;
12✔
51
    private RepositoryConnection adminRepoConn;
52

53
    /**
54
     * Represents the current status of the service, including the load counter.
55
     */
56
    public static class LoadingStatus {
57

58
        /**
59
         * The current state of the service.
60
         */
61
        public final State state;
62

63
        /**
64
         * The current load counter.
65
         */
66
        public final long loadCounter;
67

68
        private LoadingStatus(State state, long loadCounter) {
6✔
69
            this.state = state;
9✔
70
            this.loadCounter = loadCounter;
9✔
71
        }
3✔
72

73
        /**
74
         * Create a new LoadingStatus instance.
75
         *
76
         * @param state       the current state of the service
77
         * @param loadCounter the current load counter
78
         * @return a new LoadingStatus instance
79
         */
80
        public static LoadingStatus of(State state, long loadCounter) {
81
            return new LoadingStatus(state, loadCounter);
18✔
82
        }
83

84
        @Override
85
        public boolean equals(Object o) {
86
            if (o == null || getClass() != o.getClass()) {
21✔
87
                return false;
6✔
88
            }
89
            LoadingStatus that = (LoadingStatus) o;
9✔
90
            return loadCounter == that.loadCounter && state == that.state;
45✔
91
        }
92

93
        @Override
94
        public int hashCode() {
95
            return Objects.hash(state, loadCounter);
45✔
96
        }
97

98
    }
99

100
    /**
101
     * Initialize the StatusController, fetching the last known state from the DB.
102
     * This must be called right after service startup, before loading any nanopubs.
103
     *
104
     * @return the current state and the last committed counter
105
     */
106
    public LoadingStatus initialize() {
107
        synchronized (this) {
12✔
108
            if (initialized) {
9✔
109
                throw new IllegalStateException("Already initialized");
15✔
110
            }
111
            state = State.LAUNCHING;
9✔
112
            adminRepoConn = TripleStore.get().getAdminRepoConnection();
12✔
113
            // Serializable, as the service state needs to be strictly consistent
114
            adminRepoConn.begin(IsolationLevels.SERIALIZABLE);
12✔
115
            // Fetch the state from the DB
116
            try (var statements = adminRepoConn.getStatements(NPA.THIS_REPO, NPA.HAS_STATUS, null, NPA.GRAPH)) {
39✔
117
                if (!statements.hasNext()) {
9!
118
                    adminRepoConn.add(NPA.THIS_REPO, NPA.HAS_STATUS, stateAsLiteral(state), NPA.GRAPH);
48✔
119
                } else {
120
                    var stateStatement = statements.next();
×
121
                    state = State.valueOf(stateStatement.getObject().stringValue());
×
122
                }
123
            }
124
            // Fetch the load counter from the DB
125
            try (var statements = adminRepoConn.getStatements(NPA.THIS_REPO, NPA.HAS_REGISTRY_LOAD_COUNTER, null, NPA.GRAPH)) {
39✔
126
                if (!statements.hasNext()) {
9!
127
                    adminRepoConn.add(NPA.THIS_REPO, NPA.HAS_REGISTRY_LOAD_COUNTER, adminRepoConn.getValueFactory().createLiteral(-1L), NPA.GRAPH);
51✔
128
                } else {
129
                    var counterStatement = statements.next();
×
130
                    var stringVal = counterStatement.getObject().stringValue();
×
131
                    lastCommittedCounter = Long.parseLong(stringVal);
×
132
                }
133
                adminRepoConn.commit();
9✔
134
            } catch (Exception e) {
×
135
                if (adminRepoConn.isActive()) {
×
136
                    try {
137
                        adminRepoConn.rollback();
×
138
                    } catch (Exception rollbackException) {
×
139
                        // Transaction may not be registered on server (e.g., already committed, timed out, or connection reset)
140
                        // Log the rollback failure but don't mask the original exception
141
                    }
×
142
                }
143
                throw new RuntimeException(e);
×
144
            }
3✔
145
            initialized = true;
9✔
146
            return getState();
15✔
147
        }
148
    }
149

150
    /**
151
     * Get the current state of the service.
152
     *
153
     * @return the current state and the last committed counter
154
     */
155
    public LoadingStatus getState() {
156
        synchronized (this) {
12✔
157
            return LoadingStatus.of(state, lastCommittedCounter);
24✔
158
        }
159
    }
160

161
    /**
162
     * Transition the service to the LOADING_INITIAL state and update the load counter.
163
     * This should be called in two situations:
164
     * - By the main loading thread (after calling initialize()) to start loading the initial nanopubs.
165
     * - By the initial nanopub loader, as it processes the initial nanopubs.
166
     *
167
     * @param loadCounter the new load counter
168
     */
169
    public void setLoadingInitial(long loadCounter) {
170
        synchronized (this) {
12✔
171
            if (state != State.LAUNCHING && state != State.LOADING_INITIAL) {
24✔
172
                throw new IllegalStateException("Cannot transition to LOADING_INITIAL, as the " + "current state is " + state);
24✔
173
            }
174
            if (lastCommittedCounter > loadCounter) {
15✔
175
                throw new IllegalStateException("Cannot update the load counter from " + lastCommittedCounter + " to " + loadCounter);
24✔
176
            }
177
            updateState(State.LOADING_INITIAL, loadCounter);
12✔
178
        }
9✔
179
    }
3✔
180

181
    /**
182
     * Transition the service to the LOADING_UPDATES state and update the load counter.
183
     * This should be called by the updates loader, when it starts processing new nanopubs, or
184
     * when it finishes processing a batch of nanopubs.
185
     *
186
     * @param loadCounter the new load counter
187
     */
188
    public void setLoadingUpdates(long loadCounter) {
189
        synchronized (this) {
12✔
190
            if (state != State.LAUNCHING && state != State.LOADING_UPDATES && state != State.READY) {
36✔
191
                throw new IllegalStateException("Cannot transition to LOADING_UPDATES, as the " + "current state is " + state);
24✔
192
            }
193
            if (lastCommittedCounter > loadCounter) {
15✔
194
                throw new IllegalStateException("Cannot update the load counter from " + lastCommittedCounter + " to " + loadCounter);
24✔
195
            }
196
            updateState(State.LOADING_UPDATES, loadCounter);
12✔
197
        }
9✔
198
    }
3✔
199

200
    /**
201
     * Transition the service to the READY state.
202
     * This should be called by the loaders, after they finish their work.
203
     */
204
    public void setReady() {
205
        synchronized (this) {
12✔
206
            if (state != State.READY) {
12✔
207
                updateState(State.READY, lastCommittedCounter);
15✔
208
            }
209
        }
9✔
210
    }
3✔
211

212
    void updateState(State newState, long loadCounter) {
213
        synchronized (this) {
12✔
214
            try {
215
                // Serializable, as the service state needs to be strictly consistent
216
                adminRepoConn.begin(IsolationLevels.SERIALIZABLE);
12✔
217
                adminRepoConn.remove(NPA.THIS_REPO, NPA.HAS_STATUS, null, NPA.GRAPH);
36✔
218
                adminRepoConn.add(NPA.THIS_REPO, NPA.HAS_STATUS, stateAsLiteral(newState), NPA.GRAPH);
42✔
219
                adminRepoConn.remove(NPA.THIS_REPO, NPA.HAS_REGISTRY_LOAD_COUNTER, null, NPA.GRAPH);
36✔
220
                adminRepoConn.add(NPA.THIS_REPO, NPA.HAS_REGISTRY_LOAD_COUNTER, adminRepoConn.getValueFactory().createLiteral(loadCounter), NPA.GRAPH);
48✔
221
                adminRepoConn.commit();
9✔
222
                state = newState;
9✔
223
                lastCommittedCounter = loadCounter;
9✔
224
            } catch (Exception e) {
×
225
                if (adminRepoConn.isActive()) {
×
226
                    try {
227
                        adminRepoConn.rollback();
×
228
                    } catch (Exception rollbackException) {
×
229
                        // Transaction may not be registered on server (e.g., already committed, timed out, or connection reset)
230
                        // Log the rollback failure but don't mask the original exception
231
                    }
×
232
                }
233
                throw new RuntimeException(e);
×
234
            }
3✔
235
        }
9✔
236
    }
3✔
237

238
    private Literal stateAsLiteral(State s) {
239
        return adminRepoConn.getValueFactory().createLiteral(s.toString());
21✔
240
    }
241

242
    /**
243
     * Reset the StatusController for testing purposes.
244
     * This will clear the state and allow re-initialization.
245
     */
246
    void resetForTest() {
247
        synchronized (this) {
12✔
248
            initialized = false;
9✔
249
            state = null;
9✔
250
            lastCommittedCounter = -1;
9✔
251
            adminRepoConn = null;
9✔
252
        }
9✔
253
    }
3✔
254

255
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc