|
1 | 1 | package gov.nasa.jpl.aerie.merlin.worker.postgres;
|
2 | 2 |
|
3 |
| -import gov.nasa.jpl.aerie.json.JsonParser; |
4 |
| -import gov.nasa.jpl.aerie.merlin.driver.resources.ResourceProfile; |
5 | 3 | import gov.nasa.jpl.aerie.merlin.driver.resources.ResourceProfiles;
|
6 |
| -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; |
7 |
| -import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; |
8 |
| -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; |
9 | 4 | import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.DatabaseException;
|
10 |
| -import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.FailedInsertException; |
11 |
| -import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.FailedUpdateException; |
12 |
| -import gov.nasa.jpl.aerie.merlin.server.remotes.postgres.PreparedStatements; |
13 |
| -import org.apache.commons.lang3.tuple.Pair; |
14 | 5 |
|
15 | 6 | import javax.sql.DataSource;
|
16 |
| -import java.sql.Connection; |
17 |
| -import java.sql.PreparedStatement; |
18 | 7 | import java.sql.SQLException;
|
19 |
| -import java.sql.Statement; |
20 |
| -import java.util.HashMap; |
| 8 | +import java.util.concurrent.ExecutorService; |
| 9 | +import java.util.concurrent.Executors; |
21 | 10 | import java.util.function.Consumer;
|
22 | 11 |
|
23 |
| -import static gov.nasa.jpl.aerie.merlin.server.remotes.postgres.PostgresParsers.discreteProfileTypeP; |
24 |
| -import static gov.nasa.jpl.aerie.merlin.server.remotes.postgres.PostgresParsers.realProfileTypeP; |
25 |
| - |
26 |
| -import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; |
27 |
| -import static gov.nasa.jpl.aerie.merlin.server.http.ProfileParsers.realDynamicsP; |
28 |
| - |
29 | 12 | public class PostgresProfileStreamer implements Consumer<ResourceProfiles>, AutoCloseable {
|
30 |
| - private final Connection connection; |
31 |
| - private final HashMap<String, Integer> profileIds; |
32 |
| - private final HashMap<String, Duration> profileDurations; |
33 |
| - |
34 |
| - private final PreparedStatement postProfileStatement; |
35 |
| - private final PreparedStatement postSegmentsStatement; |
36 |
| - private final PreparedStatement updateDurationStatement; |
| 13 | + private final ExecutorService queryQueue; |
| 14 | + private final PostgresProfileQueryHandler queryHandler; |
37 | 15 |
|
38 | 16 | public PostgresProfileStreamer(DataSource dataSource, long datasetId) throws SQLException {
|
39 |
| - this.connection = dataSource.getConnection(); |
40 |
| - profileIds = new HashMap<>(); |
41 |
| - profileDurations = new HashMap<>(); |
42 |
| - |
43 |
| - final String postProfilesSql = |
44 |
| - //language=sql |
45 |
| - """ |
46 |
| - insert into merlin.profile (dataset_id, name, type, duration) |
47 |
| - values (%d, ?, ?::jsonb, ?::interval) |
48 |
| - on conflict (dataset_id, name) do nothing |
49 |
| - """.formatted(datasetId); |
50 |
| - final String postSegmentsSql = |
51 |
| - //language=sql |
52 |
| - """ |
53 |
| - insert into merlin.profile_segment (dataset_id, profile_id, start_offset, dynamics, is_gap) |
54 |
| - values (%d, ?, ?::interval, ?::jsonb, false) |
55 |
| - """.formatted(datasetId); |
56 |
| - final String updateDurationSql = |
57 |
| - //language=SQL |
58 |
| - """ |
59 |
| - update merlin.profile |
60 |
| - set duration = ?::interval |
61 |
| - where (dataset_id, id) = (%d, ?); |
62 |
| - """.formatted(datasetId); |
63 |
| - |
64 |
| - postProfileStatement = connection.prepareStatement(postProfilesSql, PreparedStatement.RETURN_GENERATED_KEYS); |
65 |
| - postSegmentsStatement = connection.prepareStatement(postSegmentsSql, PreparedStatement.NO_GENERATED_KEYS); |
66 |
| - updateDurationStatement = connection.prepareStatement(updateDurationSql, PreparedStatement.NO_GENERATED_KEYS); |
| 17 | + this.queryQueue = Executors.newSingleThreadExecutor(); |
| 18 | + this.queryHandler = new PostgresProfileQueryHandler(dataSource, datasetId); |
67 | 19 | }
|
68 | 20 |
|
69 | 21 | @Override
|
70 | 22 | public void accept(final ResourceProfiles resourceProfiles) {
|
71 |
| - try { |
72 |
| - // Add new profiles to DB |
73 |
| - for(final var realEntry : resourceProfiles.realProfiles().entrySet()){ |
74 |
| - if(!profileIds.containsKey(realEntry.getKey())){ |
75 |
| - addRealProfileToBatch(realEntry.getKey(), realEntry.getValue()); |
76 |
| - } |
77 |
| - } |
78 |
| - for(final var discreteEntry : resourceProfiles.discreteProfiles().entrySet()) { |
79 |
| - if(!profileIds.containsKey(discreteEntry.getKey())){ |
80 |
| - addDiscreteProfileToBatch(discreteEntry.getKey(), discreteEntry.getValue()); |
81 |
| - } |
82 |
| - } |
83 |
| - postProfiles(); |
84 |
| - |
85 |
| - // Post Segments |
86 |
| - for(final var realEntry : resourceProfiles.realProfiles().entrySet()){ |
87 |
| - addProfileSegmentsToBatch(realEntry.getKey(), realEntry.getValue(), realDynamicsP); |
88 |
| - } |
89 |
| - for(final var discreteEntry : resourceProfiles.discreteProfiles().entrySet()) { |
90 |
| - addProfileSegmentsToBatch(discreteEntry.getKey(), discreteEntry.getValue(), serializedValueP); |
91 |
| - } |
92 |
| - |
93 |
| - postProfileSegments(); |
94 |
| - updateProfileDurations(); |
95 |
| - } catch (SQLException ex) { |
96 |
| - throw new DatabaseException("Exception occurred while posting profiles.", ex); |
97 |
| - } |
98 |
| - } |
99 |
| - |
100 |
| - private void addRealProfileToBatch(final String name, ResourceProfile<RealDynamics> profile) throws SQLException { |
101 |
| - postProfileStatement.setString(1, name); |
102 |
| - postProfileStatement.setString(2, realProfileTypeP.unparse(Pair.of("real", profile.schema())).toString()); |
103 |
| - PreparedStatements.setDuration(this.postProfileStatement, 3, Duration.ZERO); |
104 |
| - |
105 |
| - postProfileStatement.addBatch(); |
106 |
| - |
107 |
| - profileDurations.put(name, Duration.ZERO); |
108 |
| - } |
109 |
| - |
110 |
| - private void addDiscreteProfileToBatch(final String name, ResourceProfile<SerializedValue> profile) throws SQLException { |
111 |
| - postProfileStatement.setString(1, name); |
112 |
| - postProfileStatement.setString(2, discreteProfileTypeP.unparse(Pair.of("discrete", profile.schema())).toString()); |
113 |
| - PreparedStatements.setDuration(this.postProfileStatement, 3, Duration.ZERO); |
114 |
| - |
115 |
| - postProfileStatement.addBatch(); |
116 |
| - |
117 |
| - profileDurations.put(name, Duration.ZERO); |
118 |
| - } |
119 |
| - |
120 |
| - /** |
121 |
| - * Insert the batched profiles and cache their ids for future use. |
122 |
| - * |
123 |
| - * This method takes advantage of the fact that we're using the Postgres JDBC, |
124 |
| - * which returns all columns when executing batches with `getGeneratedKeys`. |
125 |
| - */ |
126 |
| - private void postProfiles() throws SQLException { |
127 |
| - final var results = this.postProfileStatement.executeBatch(); |
128 |
| - for (final var result : results) { |
129 |
| - if (result == Statement.EXECUTE_FAILED) throw new FailedInsertException("merlin.profile_segment"); |
130 |
| - } |
131 |
| - |
132 |
| - final var resultSet = this.postProfileStatement.getGeneratedKeys(); |
133 |
| - while(resultSet.next()){ |
134 |
| - profileIds.put(resultSet.getString("name"), resultSet.getInt("id")); |
135 |
| - } |
136 |
| - } |
137 |
| - |
138 |
| - private void postProfileSegments() throws SQLException { |
139 |
| - final var results = this.postSegmentsStatement.executeBatch(); |
140 |
| - for (final var result : results) { |
141 |
| - if (result == Statement.EXECUTE_FAILED) throw new FailedInsertException("merlin.profile_segment"); |
142 |
| - } |
143 |
| - } |
144 |
| - |
145 |
| - private void updateProfileDurations() throws SQLException { |
146 |
| - final var results = this.updateDurationStatement.executeBatch(); |
147 |
| - for (final var result : results) { |
148 |
| - if (result == Statement.EXECUTE_FAILED) throw new FailedUpdateException("merlin.profile"); |
149 |
| - } |
150 |
| - } |
151 |
| - |
152 |
| - private <T> void addProfileSegmentsToBatch(final String name, ResourceProfile<T> profile, JsonParser<T> dynamicsP) throws SQLException { |
153 |
| - final var id = profileIds.get(name); |
154 |
| - this.postSegmentsStatement.setLong(1, id); |
155 |
| - |
156 |
| - var newDuration = profileDurations.get(name); |
157 |
| - for (final var segment : profile.segments()) { |
158 |
| - PreparedStatements.setDuration(this.postSegmentsStatement, 2, newDuration); |
159 |
| - final var dynamics = dynamicsP.unparse(segment.dynamics()).toString(); |
160 |
| - this.postSegmentsStatement.setString(3, dynamics); |
161 |
| - this.postSegmentsStatement.addBatch(); |
162 |
| - |
163 |
| - newDuration = newDuration.plus(segment.extent()); |
164 |
| - } |
165 |
| - |
166 |
| - this.updateDurationStatement.setLong(2, id); |
167 |
| - PreparedStatements.setDuration(this.updateDurationStatement, 1, newDuration); |
168 |
| - this.updateDurationStatement.addBatch(); |
169 |
| - |
170 |
| - profileDurations.put(name, newDuration); |
| 23 | + queryQueue.submit(() -> { |
| 24 | + queryHandler.uploadResourceProfiles(resourceProfiles); |
| 25 | + }); |
171 | 26 | }
|
172 | 27 |
|
173 | 28 | @Override
|
174 |
| - public void close() throws SQLException { |
175 |
| - this.postProfileStatement.close(); |
176 |
| - this.postSegmentsStatement.close(); |
177 |
| - this.updateDurationStatement.close(); |
178 |
| - this.connection.close(); |
| 29 | + public void close() { |
| 30 | + queryQueue.shutdown(); |
| 31 | + try { |
| 32 | + queryHandler.close(); |
| 33 | + } catch (SQLException e) { |
| 34 | + throw new DatabaseException("Error occurred while attempting to close PostgresProfileQueryHandler", e); |
| 35 | + } |
179 | 36 | }
|
| 37 | + |
180 | 38 | }
|
0 commit comments