blob: 06991603184ab3f15744076a485fce9d0226a196 [file] [log] [blame]
// Copyright (C) 2014 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.ericsson.gerrit.plugins.eventslog.sql;
import static com.ericsson.gerrit.plugins.eventslog.sql.SQLTable.TABLE_NAME;
import com.google.common.io.Files;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.events.ProjectEvent;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.ericsson.gerrit.plugins.eventslog.EventPool;
import com.ericsson.gerrit.plugins.eventslog.EventStore;
import com.ericsson.gerrit.plugins.eventslog.EventsLogConfig;
import com.ericsson.gerrit.plugins.eventslog.EventsLogException;
import com.ericsson.gerrit.plugins.eventslog.ServiceUnavailableException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Singleton
class SQLStore implements EventStore, LifecycleListener {
private static final Logger log = LoggerFactory.getLogger(SQLStore.class);
private static final String H2_DB_SUFFIX = ".h2.db";
private final ProjectControl.GenericFactory projectControlFactory;
private final Provider<CurrentUser> userProvider;
private SQLClient eventsDb;
private SQLClient localEventsDb;
private final int maxAge;
private final int maxTries;
private final int waitTime;
private final int connectTime;
private boolean online = true;
private boolean copyLocal;
private final ScheduledThreadPoolExecutor pool;
private ScheduledFuture<?> checkConnTask;
private Path localPath;
@Inject
SQLStore(ProjectControl.GenericFactory projectControlFactory,
Provider<CurrentUser> userProvider,
EventsLogConfig cfg,
@EventsDb SQLClient eventsDb,
@LocalEventsDb SQLClient localEventsDb,
@EventPool ScheduledThreadPoolExecutor pool) {
this.maxAge = cfg.getMaxAge();
this.maxTries = cfg.getMaxTries();
this.waitTime = cfg.getWaitTime();
this.connectTime = cfg.getConnectTime();
this.copyLocal = cfg.getCopyLocal();
this.projectControlFactory = projectControlFactory;
this.userProvider = userProvider;
this.eventsDb = eventsDb;
this.localEventsDb = localEventsDb;
this.pool = pool;
this.localPath = cfg.getLocalStorePath();
}
@Override
public void start() {
setUp();
}
@Override
public void stop() {
cancelCheckConnectionTaskIfScheduled(true);
eventsDb.close();
localEventsDb.close();
}
/**
* {@inheritDoc}
* The events returned are restricted to the projects which are visible to
* the user.
* @throws ServiceUnavailableException if working in offline mode
*/
@Override
public List<String> queryChangeEvents(String query) throws EventsLogException {
if (!online) {
throw new ServiceUnavailableException();
}
List<SQLEntry> entries = new ArrayList<>();
Project.NameKey project = null;
for (Entry<String, Collection<SQLEntry>> entry
: eventsDb.getEvents(query).asMap().entrySet()) {
try {
project = new Project.NameKey(entry.getKey());
if (projectControlFactory.controlFor(project,
userProvider.get()).isVisible()) {
entries.addAll(entry.getValue());
}
} catch (NoSuchProjectException e) {
log.warn("Database contains a non-existing project, " + project.get()
+ ", removing project from database");
eventsDb.removeProjectEvents(project.get());
} catch (IOException e) {
log.warn("Cannot get project visibility info for " + project.get()
+ " from cache", e);
}
}
return sortedEventsFromEntries(entries);
}
private List<String> sortedEventsFromEntries(List<SQLEntry> entries) {
Collections.sort(entries);
List<String> events = new ArrayList<>();
for (SQLEntry entry : entries) {
events.add(entry.getEvent());
}
return events;
}
/**
* {@inheritDoc}
* If storing the event fails due to a connection problem, storage will be
* re-attempted as specified in gerrit.config. After failing the maximum
* amount of times, the event will be stored in a local h2 database.
*/
@Override
public void storeEvent(ProjectEvent event) {
Project.NameKey projectName = event.getProjectNameKey();
if (projectName == null) {
return;
}
int failedConnections = 0;
boolean done = false;
while (!done) {
done = true;
try {
getEventsDb().storeEvent(event);
} catch (SQLException e) {
log.warn("Cannot store ChangeEvent for: " + projectName.get(), e);
if (e.getCause() instanceof ConnectException
|| e.getMessage().contains("terminating connection")) {
done = false;
retryIfAllowed(failedConnections);
failedConnections++;
}
}
}
}
private void retryIfAllowed(int failedConnections) {
if (failedConnections < maxTries - 1) {
log.info("Retrying store event");
try {
Thread.sleep(waitTime);
} catch (InterruptedException e1) {
return;
}
} else {
log.error("Failed to store event " + maxTries + " times");
setOnline(false);
}
}
private void setUp() {
try {
getEventsDb().createDBIfNotCreated();
} catch (SQLException e) {
log.warn("Cannot start the database. Events will be stored locally"
+ " until database connection can be established", e);
setOnline(false);
}
if (online) {
restoreEventsFromLocal();
}
getEventsDb().removeOldEvents(maxAge);
}
private SQLClient getEventsDb() {
return online ? eventsDb : localEventsDb;
}
private void setOnline(boolean online) {
this.online = online;
setUp();
if (!online) {
checkConnTask = pool.scheduleWithFixedDelay(
new CheckConnectionTask(), 0, connectTime, TimeUnit.MILLISECONDS);
} else {
cancelCheckConnectionTaskIfScheduled(false);
}
}
private void cancelCheckConnectionTaskIfScheduled(boolean mayInterrupt) {
if (checkConnTask != null) {
checkConnTask.cancel(mayInterrupt);
}
}
private void restoreEventsFromLocal() {
if (!localDbExists()) {
return;
}
try {
List<SQLEntry> entries = localEventsDb.getAll();
if (entries.isEmpty()) {
log.debug("No events to restore from local");
return;
}
for (SQLEntry entry : entries) {
restoreEvent(entry);
}
} catch (SQLException e) {
log.warn("Could not query all events from local", e);
}
copyFile(copyLocal);
localEventsDb.removeOldEvents(0);
}
private void restoreEvent(SQLEntry entry) {
try {
eventsDb.storeEvent(entry.getName(), entry.getTimestamp(),
entry.getEvent());
} catch (SQLException e) {
log.warn("Could not restore events from local", e);
}
}
class CheckConnectionTask implements Runnable {
CheckConnectionTask() {
}
@Override
public void run() {
if (checkConnection()) {
setOnline(true);
log.info("Connected to database");
}
}
@Override
public String toString() {
return "(Events-log) Connect to database";
}
private boolean checkConnection() {
log.info("Checking database connection...");
try {
eventsDb.queryOne();
return true;
} catch (SQLException e) {
log.debug("Problem checking database connection", e);
return false;
}
}
}
private boolean localDbExists() {
boolean exists = false;
try {
exists = localEventsDb.dbExists();
} catch (SQLException e) {
log.warn(
"Could not check existence of local database, assume that it doesn't exist",
e);
}
return exists;
}
private void copyFile(boolean copyLocal) {
if (!copyLocal) {
return;
}
File file = localPath.resolve(TABLE_NAME + H2_DB_SUFFIX).toFile();
File copyFile = localPath.resolve(TABLE_NAME
+ (TimeUnit.MILLISECONDS.toSeconds(TimeUtil.nowMs()))
+ H2_DB_SUFFIX).toFile();
try {
Files.copy(file, copyFile);
} catch (IOException e) {
log.warn("Could not copy local database file with timestamp", e);
}
}
}