Skip to content

Commit

Permalink
Merge pull request #1844 from mikebell90/AddPostgres
Browse files Browse the repository at this point in the history
Add postgres support for JDBIHistory
  • Loading branch information
ssalinas authored Sep 26, 2018
2 parents 3eca071 + 633ba9e commit df0ddfd
Show file tree
Hide file tree
Showing 16 changed files with 585 additions and 241 deletions.
2 changes: 1 addition & 1 deletion Docs/about/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Slave placement can also be impacted by slave attributes. There are three scenar
#### Singularity Scheduler Dependencies
The Singularity scheduler uses ZooKeeper as a distributed replication log to maintain state and keep track of registered deployable items, the active deploys for these items and the running tasks that fulfill the deploys. As shown in the drawing, the same ZooKeeper quorum utilized by Mesos masters and slaves can be reused for Singularity.

Since ZooKeeper is not meant to handle large quantities of data, Singularity can optionally (and recommended for any real usage) utilize a MySQL database to periodically offload historical data from ZooKeeper and keep records of deployable item changes, deploy request history as well as the history of all launched tasks.
Since ZooKeeper is not meant to handle large quantities of data, Singularity can optionally (and recommended for any real usage) utilize a database (MySQL or PostgreSQL) to periodically offload historical data from ZooKeeper and keep records of deployable item changes, deploy request history as well as the history of all launched tasks.

In production environments Singularity should be run in high-availability mode by running multiple instances of the Singularity Scheduler component. As depicted in the drawing, only one instance is always active with all the other instances waiting in stand-by mode. While only one instance is registered for receiving resource offers, all instances can process API requests. Singularity uses ZooKeeper to perform leader election and maintain a single leader. Because of the ability for all instances to change state, Singularity internally uses queues which are consumed by the Singularity leader to make calls to Mesos.

Expand Down
6 changes: 3 additions & 3 deletions Docs/features/task-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ The above endpoint as well as `/api/history/request/{requestId}/tasks` now take
- `count`: Maximum number of items to return, defaults to 100 and has a maximum value of 1000
- `page`: Page of items to view (e.g. page 1 is the first `count` items, page 2 is the next `count` items), defaults to 1

For clusters using mysql that have a large number of tasks in the history, a relevant configuration option of `taskHistoryQueryUsesZkFirst` has been added in the base Singularity Configuration. This option can be used to either prefer efficiency or exact ordering when searching through task history, it defaults to `false`.
For clusters using a database that have a large number of tasks in the history, a relevant configuration option of `taskHistoryQueryUsesZkFirst` has been added in the base Singularity Configuration. This option can be used to either prefer efficiency or exact ordering when searching through task history, it defaults to `false`.

- When `false` the setting will prefer correct ordering. This may require multiple database calls, since Singularity needs to determine the overall order of items base on persisted (in mysql) and non-persisted (still in zookeeper) tasks. The overall search may be less efficient, but the ordering is guranteed to be correct.
- When `false` the setting will prefer correct ordering. This may require multiple database calls, since Singularity needs to determine the overall order of items base on persisted (in the database) and non-persisted (still in zookeeper) tasks. The overall search may be less efficient, but the ordering is guranteed to be correct.

- When `true` the setting will prefer efficiency. In this case, it will be assumed that all task histories in zookeeper (not yet persisted) come before those in mysql (persisted). This results in faster results and fewer queries, but ordering is not guaranteed to be correct between persisted and non-persisted items.
- When `true` the setting will prefer efficiency. In this case, it will be assumed that all task histories in zookeeper (not yet persisted) come before those in the database (persisted). This results in faster results and fewer queries, but ordering is not guaranteed to be correct between persisted and non-persisted items.
8 changes: 4 additions & 4 deletions Docs/getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ More info on how to manually set up a Zookeeper cluster lives [here](https://zoo

For testing or local development purposes, a single-node cluster running on your local machine is fine. If using the [docker testing/development setup](../development/developing-with-docker.md), this will already be present.

### 2. Set up MySQL (optional)
### 2. Set up MySQL or PostgreSQL (optional)

Singularity can be configured to move stale data from Zookeeper to MySQL after a configurable amount of time, which helps reduce strain on the cluster. If you're running Singularity in Production environment, MySQL is encouraged. See the [database reference](../reference/database.md) for help configuring the database.
Singularity can be configured to move stale data from Zookeeper to a database after a configurable amount of time, which helps reduce strain on the cluster. If you're running Singularity in Production environment, enabling database support is encouraged. See the [database reference](../reference/database.md) for help configuring the database.

### 3. Set up a Mesos cluster

Expand Down Expand Up @@ -93,9 +93,9 @@ ui:
Full configuration documentation lives here: [configuration.md](../reference/configuration.md)
### 6. Run MySQL migrations (if necessary)
### 6. Run database migrations (if necessary)
If you're operating Singularity with MySQL, you first need to run a liquibase migration to create all appropriate tables: (this snippet assumes your Singularity configuration YAML exists as `singularity_config.yaml`)
If you're operating Singularity with a database, you first need to run a liquibase migration to create all appropriate tables: (this snippet assumes your Singularity configuration YAML exists as `singularity_config.yaml`)

`java -jar SingularityService/target/SingularityService-*-shaded.jar db migrate singularity_config.yaml --migrations mysql/migrations.sql`

Expand Down
4 changes: 2 additions & 2 deletions Docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ These settings are less likely to be changed, but were included in the configura
| checkSchedulerEverySeconds | 5 | Runs scheduler checks (processes decommissions and pending queue) on this interval (these tasks also run when an offer is received) | long |
| checkWebhooksEveryMillis | 10000 (10 seconds) | Will check for and send new queued webhooks on this interval | long |
| cleanupEverySeconds | 5 | Will cleanup request, task, and other queues on this interval | long |
| persistHistoryEverySeconds | 3600 (1 hour) | Moves stale historical task data from ZooKeeper into MySQL, setting to 0 will disable history persistence | long |
| persistHistoryEverySeconds | 3600 (1 hour) | Moves stale historical task data from ZooKeeper into the database, setting to 0 will disable history persistence | long |
| saveStateEverySeconds | 60 | State about this Singularity instance is saved (available over API) on this interval | long |
| checkJobsEveryMillis | 600000 (10 mins) | Check for jobs running longer than the expected time on this interval | long |
| checkExpiringUserActionEveryMillis | 45000 | Check for expiring actions that should be expired on this interval | long |
Expand All @@ -130,7 +130,7 @@ These settings are less likely to be changed, but were included in the configura
| Parameter | Default | Description | Type |
|-----------|---------|-------------|------|
| closeWaitSeconds | 5 | Will wait at least this many seconds when shutting down thread pools | long |
| compressLargeDataObjects | true | Will compress larger objects inside of ZooKeeper and MySQL | boolean |
| compressLargeDataObjects | true | Will compress larger objects inside of ZooKeeper and the database | boolean |
| maxHealthcheckResponseBodyBytes | 8192 | Number of bytes to save from healthcheck responses (displayed in UI) | int |
| maxQueuedUpdatesPerWebhook | 50 | Max number of updates to queue for a given webhook url, after which some webhooks will not be delivered | int |
| zookeeperAsyncTimeout | 5000 | Milliseconds for ZooKeeper timeout. Calls to ZooKeeper which take over this timeout will cause the operations to fail and Singularity to abort | long |
Expand Down
9 changes: 7 additions & 2 deletions Docs/reference/database.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## Historical Data

Singularity can optionally persist all task and deployment historical information into a MySQL database. This is useful because Mesos does not necessarily keep state forever, nor does it provide a deploy-focused interface for viewing that state. The Singularity API and web application will return historical information from both ZooKeeper and MySQL. Singularity will periodically dump stale state into MySQL.
Singularity can optionally persist all task and deployment historical information into a MySQL or PostgreSQL database. This is useful because Mesos does not necessarily keep state forever, nor does it provide a deploy-focused interface for viewing that state. The Singularity API and web application will return historical information from both ZooKeeper and the database. Singularity will periodically dump stale state into the databaw.

### Configuration

The `database` section of the Singularity configuration file must be populated in order for Singularity to persist task and deploy history information. Here's an example:
The `database` section of the Singularity configuration file must be populated in order for Singularity to persist task and deploy history information. Here's an example using MySQL:

```
database:
Expand All @@ -14,6 +14,9 @@ database:
url: jdbc:mysql://HOSTNAME:3306/DB_NAME
```

A PostgreSQL configuration would be similar, but use a driverClass of `org.postgresql.Driver` and an appropriate
url - for example `jdbc:postgresql://HOSTNAME:5432/DB_NAME`

### Schema Changes

Singularity uses the [dropwizard-migrations](http://dropwizard.io/manual/migrations) bundle (which in turn uses [liquibase](http://www.liquibase.org/)) for managing and applying database schema changes.
Expand All @@ -27,6 +30,8 @@ INFO [2013-12-23 18:41:33,668] liquibase: Reading from singularity.DATABASECHAN
1 change sets have not been applied to root@localhost@jdbc:mysql://localhost:3306/singularity
```

**Note**: The appropriate migrations.sql differs depending on database flavor and is provided in the `postgres` or `mysql` directory of the Singularity distribution.

To apply pending migrations, run the `db migrate` task:

```
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Then simply run `docker-compose up` and it will start containers for...
- [ZooKeeper](https://zookeeper.apache.org/doc/r3.4.6/zookeeperStarted.html)
- Java 7+
- [MySQL](http://dev.mysql.com/usingmysql/get_started.html) (optional)
- PostgreSQL (optional)

##### Contact #####

Expand Down
16 changes: 16 additions & 0 deletions SingularityService/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
<basepom.shaded.main-class>com.hubspot.singularity.SingularityService</basepom.shaded.main-class>
</properties>

<dependencyManagement>
<!-- TODO: Push up to hubspot basepom? -->
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.4</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>

<dependency>
Expand Down Expand Up @@ -408,6 +419,11 @@
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.antlr</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.hubspot.singularity.data.history;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.skife.jdbi.v2.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Optional;
import com.hubspot.singularity.ExtendedTaskState;
import com.hubspot.singularity.OrderDirection;
import com.hubspot.singularity.SingularityTaskIdHistory;

// Common code for DB queries
public abstract class AbstractHistoryJDBI implements HistoryJDBI {
protected static final Logger LOG = LoggerFactory.getLogger(HistoryJDBI.class);

private static final String GET_TASK_ID_HISTORY_QUERY = "SELECT taskId, requestId, updatedAt, lastTaskStatus, runId FROM taskHistory";
private static final String GET_TASK_ID_HISTORY_COUNT_QUERY = "SELECT COUNT(*) FROM taskHistory";

protected void addWhereOrAnd(StringBuilder sqlBuilder, boolean shouldUseWhere) {
if (shouldUseWhere) {
sqlBuilder.append(" WHERE ");
} else {
sqlBuilder.append(" AND ");
}
}

protected void applyTaskIdHistoryBaseQuery(StringBuilder sqlBuilder, Map<String, Object> binds, Optional<String> requestId, Optional<String> deployId, Optional<String> runId, Optional<String> host,
Optional<ExtendedTaskState> lastTaskStatus, Optional<Long> startedBefore, Optional<Long> startedAfter, Optional<Long> updatedBefore,
Optional<Long> updatedAfter) {
if (requestId.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("requestId = :requestId");
binds.put("requestId", requestId.get());
}

if (deployId.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("deployId = :deployId");
binds.put("deployId", deployId.get());
}

if (runId.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("runId = :runId");
binds.put("runId", runId.get());
}

if (host.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("host = :host");
binds.put("host", host.get());
}

if (lastTaskStatus.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("lastTaskStatus = :lastTaskStatus");
binds.put("lastTaskStatus", lastTaskStatus.get().name());
}

if (startedBefore.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("startedAt < :startedBefore");
binds.put("startedBefore", new Date(startedBefore.get()));
}

if (startedAfter.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("startedAt > :startedAfter");
binds.put("startedAfter", new Date(startedAfter.get()));
}

if (updatedBefore.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("updatedAt < :updatedBefore");
binds.put("updatedBefore", new Date(updatedBefore.get()));
}

if (updatedAfter.isPresent()) {
addWhereOrAnd(sqlBuilder, binds.isEmpty());
sqlBuilder.append("updatedAt > :updatedAfter");
binds.put("updatedAfter", new Date(updatedAfter.get()));
}
}

@Override
public List<SingularityTaskIdHistory> getTaskIdHistory(Optional<String> requestId, Optional<String> deployId, Optional<String> runId, Optional<String> host,
Optional<ExtendedTaskState> lastTaskStatus, Optional<Long> startedBefore, Optional<Long> startedAfter, Optional<Long> updatedBefore,
Optional<Long> updatedAfter, Optional<OrderDirection> orderDirection, Optional<Integer> limitStart, Integer limitCount) {

final Map<String, Object> binds = new HashMap<>();
final StringBuilder sqlBuilder = new StringBuilder(GET_TASK_ID_HISTORY_QUERY);

applyTaskIdHistoryBaseQuery(sqlBuilder, binds, requestId, deployId, runId, host, lastTaskStatus, startedBefore, startedAfter, updatedBefore, updatedAfter);

sqlBuilder.append(" ORDER BY startedAt ");
sqlBuilder.append(orderDirection.or(OrderDirection.DESC).name());

if (!requestId.isPresent()) {
sqlBuilder.append(", requestId ");
sqlBuilder.append(orderDirection.or(OrderDirection.DESC).name());
}

// NOTE: PG, MySQL are both compatible with OFFSET LIMIT syntax, while only MySQL understands LIMIT offset, limit.
if (limitCount != null ){
sqlBuilder.append(" LIMIT :limitCount");
binds.put("limitCount", limitCount);
}

if (limitStart.isPresent()) {
sqlBuilder.append(" OFFSET :limitStart ");
binds.put("limitStart", limitStart.get());
}

final String sql = sqlBuilder.toString();

LOG.trace("Generated sql for task search: {}, binds: {}", sql, binds);

final Query<SingularityTaskIdHistory> query = getHandle().createQuery(sql).mapTo(SingularityTaskIdHistory.class);
for (Map.Entry<String, Object> entry : binds.entrySet()) {
query.bind(entry.getKey(), entry.getValue());
}

return query.list();
}

@Override
public int getTaskIdHistoryCount(Optional<String> requestId, Optional<String> deployId, Optional<String> runId, Optional<String> host,
Optional<ExtendedTaskState> lastTaskStatus, Optional<Long> startedBefore, Optional<Long> startedAfter, Optional<Long> updatedBefore,
Optional<Long> updatedAfter) {

final Map<String, Object> binds = new HashMap<>();
final StringBuilder sqlBuilder = new StringBuilder(GET_TASK_ID_HISTORY_COUNT_QUERY);

applyTaskIdHistoryBaseQuery(sqlBuilder, binds, requestId, deployId, runId, host, lastTaskStatus, startedBefore, startedAfter, updatedBefore, updatedAfter);

final String sql = sqlBuilder.toString();

LOG.trace("Generated sql for task search count: {}, binds: {}", sql, binds);

final Query<Integer> query = getHandle().createQuery(sql).mapTo(Integer.class);
for (Map.Entry<String, Object> entry : binds.entrySet()) {
query.bind(entry.getKey(), entry.getValue());
}

return query.first();
}

}
Loading

0 comments on commit df0ddfd

Please sign in to comment.