package org.fluxtream.core.services.impl; import com.newrelic.api.agent.NewRelic; import com.newrelic.api.agent.Trace; import org.codehaus.plexus.util.ExceptionUtils; import org.fluxtream.core.Configuration; import org.fluxtream.core.aspects.FlxLogger; import org.fluxtream.core.connectors.Connector; import org.fluxtream.core.connectors.updaters.*; import org.fluxtream.core.domain.*; import org.fluxtream.core.domain.UpdateWorkerTask.Status; import org.fluxtream.core.services.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import java.util.Date; import java.util.List; @Component @Scope("prototype") class UpdateWorker implements Runnable { FlxLogger logger = FlxLogger.getLogger(UpdateWorker.class); @Autowired ConnectorUpdateService connectorUpdateService; @Autowired ApiDataService apiDataService; @Autowired GuestService guestService; @Autowired SystemService systemService; @Autowired NotificationsService notificationsService; @Autowired SettingsService settingsService; @Autowired BuddiesService buddiesService; @Autowired BodyTrackStorageService bodyTrackStorageService; @Autowired Configuration env; UpdateWorkerTask task; public UpdateWorker() { } @Trace(dispatcher=true) @Override public void run() { ApiKey apiKey = null; try { final UpdateWorkerTask claimed = connectorUpdateService.claimForExecution(task.getId(), Thread.currentThread().getName()); if (claimed == null) { return; } else { this.task = claimed; } logNR(); StringBuilder sb = new StringBuilder("module=updateQueue component=worker action=start").append(" guestId=").append(task.getGuestId()).append(" connector=").append(task.connectorName).append(" objectType=").append(task.objectTypes).append(" apiKeyId=").append(task.apiKeyId); logger.info(sb.toString()); apiKey = guestService.getApiKey(task.apiKeyId); Connector conn = apiKey.getConnector(); // Check if this connector type is enabled and supportsSync before calling update. // If it is disabled and/or does not support sync, don't try to update it. boolean doUpdate = true; if (conn != null) { try { final ConnectorInfo connectorInfo = systemService.getConnectorInfo(apiKey.getConnector().getName()); // Make sure that this connector type supports sync and is enabled in this Fluxtream instance if (!connectorInfo.supportsSync || !connectorInfo.enabled) { doUpdate = false; } } catch (Throwable e) { // Skip this connector doUpdate = false; } } else { doUpdate = false; } if (doUpdate) { AbstractUpdater updater = connectorUpdateService.getUpdater(conn); guestService.setApiKeyToSynching(apiKey.getId(), true); switch (task.updateType) { case INITIAL_HISTORY_UPDATE: updateDataHistory(apiKey, updater); break; case PUSH_TRIGGERED_UPDATE: pushTriggeredUpdate(apiKey, updater); break; case INCREMENTAL_UPDATE: updateData(apiKey, updater); break; default: logger.warn("module=updateQueue component=worker message=\"UpdateType was not handled (" + task.updateType + ")\""); this.task = connectorUpdateService.setUpdateWorkerTaskStatus(task.getId(), Status.FAILED); } } else { // This connector does not support update so mark the update task as done this.task = connectorUpdateService.setUpdateWorkerTaskStatus(task.getId(), Status.DONE); StringBuilder sb2 = new StringBuilder("module=updateQueue component=worker").append(" guestId=").append(task.getGuestId()).append(" connector=").append(task.connectorName).append(" apiKeyId=").append(task.apiKeyId).append(" message=\"Connector does not support sync, skipping update\""); logger.info(sb2.toString()); } } catch (Throwable t) { logger.warn("Warning (UpdateWorker): run aborted - taskId=" + task.getId() + " connector=" + task.connectorName + " objectType=" + task.objectTypes + " guestId=" + task.getGuestId() + "\n" + ExceptionUtils.getStackTrace(t)); try { task = connectorUpdateService.getTask(task.getId()); if (task.status==Status.IN_PROGRESS) { logger.warn("Warning (UpdateWorker): Task was still marked as IN_PROGRESS - taskId=" + task.getId() + " connector=" + task.connectorName + " objectType=" + task.objectTypes + " guestId=" + task.getGuestId()); connectorUpdateService.setUpdateWorkerTaskStatus(task.getId(), Status.FAILED); shortReschedule(guestService.getApiKey(task.apiKeyId), new UpdateWorkerTask.AuditTrailEntry(new Date(), "Run aborted for no specific reason", "Rescheduling...")); logger.warn("Warning (UpdateWorker): Task was rescheduled - taskId=" + task.getId() + " connector=" + task.connectorName + " objectType=" + task.objectTypes + " guestId=" + task.getGuestId()); } else { logger.warn("Warning (UpdateWorker): worker didn't complete after updating connector " + "status=" + task.status + " taskId=" + task.getId() + " connector=" + task.connectorName + " objectType=" + task.objectTypes + " guestId=" + task.getGuestId()); } } catch (Throwable t2) { logger.warn("Warning (UpdateWorker): this could be the sign of a zombie thread: could not reschedule aborted worker - " + "taskId=" + task.getId() + " connector=" + task.connectorName + " objectType=" + task.objectTypes + " guestId=" + task.getGuestId() + "\n" + ExceptionUtils.getStackTrace(t2)); } } } private void logNR() { try { StringBuilder taskName = new StringBuilder("Background_Update_"); taskName.append(task.connectorName); taskName.append("_").append(task.objectTypes); NewRelic.setTransactionName(null, taskName.toString()); NewRelic.addCustomParameter("connector", task.connectorName); NewRelic.addCustomParameter("objectType", task.objectTypes); NewRelic.addCustomParameter("guestId", task.getGuestId()); } catch (Throwable t) { logger.warn("Could not set NR info..." + task.connectorName); } } private void pushTriggeredUpdate(ApiKey apiKey, AbstractUpdater updater) { // TODO: check if this connector type is enabled and supportsSync before calling update. // If it is disabled and/or does not support sync, don't try to update it. logger.info("module=updateQueue component=worker action=pushTriggeredUpdate " + "connector=" + apiKey.getConnector().getName() + " guestId=" + apiKey.getGuestId()); UpdateInfo updateInfo = UpdateInfo.pushTriggeredUpdateInfo(apiKey, task.objectTypes, task.jsonParams); UpdateResult updateResult = updater.updateData(updateInfo); handleUpdateResult(updateInfo, updateResult); } private void updateDataHistory(ApiKey apiKey, AbstractUpdater updater) { // TODO: this message should not be displayed when this is called over and over as a result of rate limitations... String message = "<img class=\"loading-animation\" src=\"/static/img/loading.gif\"/>You have successfully added a new connector: " + apiKey.getConnector().prettyName() + ". Your data is now being retrieved. " + "It may take a little while until it becomes visible."; notificationsService.addNamedNotification(apiKey.getGuestId(), Notification.Type.INFO, apiKey.getConnector().statusNotificationName(), message); // TODO: check if this connector type is enabled and supportsSync before calling update. // If it is disabled and/or does not support sync, don't try to update it. logger.info("module=updateQueue component=worker action=updateDataHistory " + "connector=" + apiKey.getConnector().getName() + " guestId=" + apiKey.getGuestId()); UpdateInfo updateInfo = UpdateInfo.initialHistoryUpdateInfo(apiKey, task.objectTypes); applyConnectorUpgrades(updateInfo); UpdateResult updateResult = updater.updateDataHistory(updateInfo); syncSettings(updater, updateInfo, updateResult); handleUpdateResult(updateInfo, updateResult); try { updater.afterHistoryUpdate(updateInfo); } catch (Exception e) { logger.warn("afterHistoryUpdate failed: apiKeyId=" + apiKey.getId() + " connector=" + apiKey.getConnector().getName()); } } private void updateData(final ApiKey apiKey, final AbstractUpdater updater) { // TODO: check if this connector type is enabled and supportsSync before calling update. // If it is disabled and/or does not support sync, don't try to update it. logger.info("module=updateQueue component=worker action=\"updateData (incremental update)\"" + " connector=" + apiKey.getConnector().getName() + " guestId=" + apiKey.getGuestId()); UpdateInfo updateInfo = UpdateInfo.IncrementalUpdateInfo(apiKey, task.objectTypes); applyConnectorUpgrades(updateInfo); UpdateResult result = updater.updateData(updateInfo); syncSettings(updater, updateInfo, result); handleUpdateResult(updateInfo, result); try { updater.afterConnectorUpdate(updateInfo); } catch (Exception e) { logger.warn("afterConnectorUpdate failed: apiKeyId=" + apiKey.getId() + " connector=" + apiKey.getConnector().getName()); } } private void applyConnectorUpgrades(UpdateInfo updateInfo) { try { final String channelsAreMapped = guestService.getApiKeyAttribute(updateInfo.apiKey, "channelMappings"); if (channelsAreMapped == null || channelsAreMapped.equals("dirty")) { boolean success = bodyTrackStorageService.mapChannels(updateInfo.apiKey); if (success) { guestService.setApiKeyAttribute(updateInfo.apiKey, "channelMappings", "clean"); // if we just mapped channels, it means people who had shared this connector will no longer share // its associated channels, which means we need to automatically do it for them here String sharedConnectorsChannelsAreShared = guestService.getApiKeyAttribute(updateInfo.apiKey, "sharedConnectorsChannelsAreShared"); if (sharedConnectorsChannelsAreShared == null || !sharedConnectorsChannelsAreShared.equals("true")) { List<SharedConnector> sharedConnectors = buddiesService.getSharedConnectors(updateInfo.apiKey); if (sharedConnectors != null) { for (SharedConnector sharedConnector : sharedConnectors) { List<ChannelMapping> channelMappings = bodyTrackStorageService.getChannelMappings(updateInfo.apiKey.getId()); for (ChannelMapping channelMapping : channelMappings) { buddiesService.addSharedChannel(sharedConnector.buddy.buddyId, sharedConnector.buddy.guestId, channelMapping.getId()); } } } guestService.setApiKeyAttribute(updateInfo.apiKey, "sharedConnectorsChannelsAreShared", "true"); } } } } catch (Throwable e) { logger.warn("Could not apply connector upgrades apiKeyId=" + updateInfo.apiKey.getId()); } } private void syncSettings(final AbstractUpdater updater, final UpdateInfo updateInfo, final UpdateResult updateResult) { if (updateResult.getType()== UpdateResult.ResultType.UPDATE_SUCCEEDED) { if (updater instanceof SettingsAwareUpdater) { final SettingsAwareUpdater settingsAwareUpdater = (SettingsAwareUpdater)updater; syncConnectorSettings(updateInfo, settingsAwareUpdater); } if (updater instanceof SharedConnectorSettingsAwareUpdater) { final SharedConnectorSettingsAwareUpdater sharedConnectorSettingsAwareUpdater = (SharedConnectorSettingsAwareUpdater)updater; final ApiKey apiKey = guestService.getApiKey(updateInfo.apiKey.getId()); final List<SharedConnector> sharedConnectors = buddiesService.getSharedConnectors(apiKey); for (SharedConnector sharedConnector : sharedConnectors) { sharedConnectorSettingsAwareUpdater.syncSharedConnectorSettings(updateInfo.apiKey.getId(), sharedConnector); } } } } private void syncConnectorSettings(final UpdateInfo updateInfo, final SettingsAwareUpdater settingsAwareUpdater) { final Object synchedSettings = settingsAwareUpdater.syncConnectorSettings(updateInfo, settingsService.getConnectorSettings(updateInfo.apiKey.getId())); final Object defaultSettings = settingsAwareUpdater.syncConnectorSettings(updateInfo, null); settingsService.persistConnectorSettings(updateInfo.apiKey.getId(), synchedSettings, defaultSettings); } private void handleUpdateResult(final UpdateInfo updateInfo, UpdateResult updateResult) { guestService.setApiKeyToSynching(updateInfo.apiKey.getId(), false); final Connector connector = updateInfo.apiKey.getConnector(); String statusName = updateInfo.apiKey.getConnector().statusNotificationName(); long guestId=updateInfo.apiKey.getGuestId(); Notification notification = null; switch (updateResult.getType()) { case DUPLICATE_UPDATE: duplicateUpdate(); break; case AUTH_REVOKED: String message = "Your " + connector.prettyName() + " Authorization Token has been revoked."; if (updateResult.getAuthRevokedException().isDataCleanupRequested()) { guestService.removeApiKey(task.apiKeyId); message += "<br>With the revocation info we received, there was an indication that you wanted your " + "data to be permanently deleted from our servers. Consequently we just permanently removed your " + connector.getPrettyName() + " connector and all its associated data."; } notificationsService.addNamedNotification(updateInfo.getGuestId(), Notification.Type.WARNING, connector.statusNotificationName(), message); UpdateWorkerTask.AuditTrailEntry failed = new UpdateWorkerTask.AuditTrailEntry(new Date(), updateResult.getType().toString(), "abort"); abort(updateInfo.apiKey, failed, updateResult.reason); break; case NEEDS_REAUTH: notificationsService.addNamedNotification(updateInfo.getGuestId(), Notification.Type.WARNING, connector.statusNotificationName(), "Heads Up. Your " + connector.prettyName() + " Authorization Token has expired.<br>" + "Please head to <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a>,<br>" + "scroll to the " + connector.prettyName() + " section, and renew your tokens (look for the <i class=\"icon-resize-small icon-large\"></i> icon)"); failed = new UpdateWorkerTask.AuditTrailEntry(new Date(), updateResult.getType().toString(), "abort"); failed.stackTrace = updateResult.stackTrace; abort(updateInfo.apiKey, failed, updateResult.reason); break; case HAS_REACHED_RATE_LIMIT: final UpdateWorkerTask.AuditTrailEntry rateLimit = new UpdateWorkerTask.AuditTrailEntry(new Date(), updateResult.getType().toString(), "long reschedule"); rateLimit.stackTrace = updateResult.stackTrace; // do this only if a notification is visible for that connector at this time notification = notificationsService.getNamedNotification(guestId,statusName); if (notification!=null && notification.deleted==false) { notificationsService.addNamedNotification(guestId, Notification.Type.INFO, statusName, "<i class=\"icon-time\" style=\"margin-right:7px\"/>Import of your " + updateInfo.apiKey.getConnector().getPrettyName() + " data is delayed due to API rate limitations. Please, be patient."); } rescheduleAccordingToQuotaSpecifications(updateInfo, rateLimit); break; case UPDATE_SUCCEEDED: // Check for existing status notification notification = notificationsService.getNamedNotification(guestId,statusName); if (notification!=null && notification.deleted==false) { // This is either an initial history update or there's an existing visible status notification. // Update the notification to show the update succeeded. notificationsService.addNamedNotification(guestId, Notification.Type.INFO, statusName, "<i class=\"icon-ok\" style=\"margin-right:7px\"/>Your " + updateInfo.apiKey.getConnector().getPrettyName() + " data was successfully imported. " + "See <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a> dialog for details."); } success(updateInfo.apiKey); break; case UPDATE_FAILED: case UPDATE_FAILED_PERMANENTLY: failed = new UpdateWorkerTask.AuditTrailEntry(new Date(), updateResult.getType().toString(), "abort"); failed.stackTrace = updateResult.stackTrace; connectorUpdateService.addAuditTrail(task.getId(), failed); final UpdateWorkerTask.AuditTrailEntry retry = new UpdateWorkerTask.AuditTrailEntry(new Date(), updateResult.getType().toString(), "retry"); retry.stackTrace = updateResult.stackTrace; // Consider this a transient failure and retry if the failure type was not permanent // and the current status of the connector instance is not already STATUS_PERMANENT_FAILURE. // if(updateResult.getType()==UpdateResult.ResultType.UPDATE_FAILED && updateInfo.apiKey.getStatus()!=ApiKey.Status.STATUS_PERMANENT_FAILURE) { retry(updateInfo, retry); } else { // This was a permanent failure, so we should set status to permanent failure and // we should not retry abort(updateInfo.apiKey, failed, updateResult.reason); } if (updateInfo.getUpdateType()== UpdateInfo.UpdateType.INITIAL_HISTORY_UPDATE) notificationsService.addNamedNotification(updateInfo.apiKey.getGuestId(), Notification.Type.ERROR, connector.statusNotificationName(), "<i class=\"icon-remove-sign\" style=\"color:red;margin-right:7px\"/>There was a problem while importing your " + connector.getPrettyName() + " data. We will try again later. " + "See <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a> dialog for details." ); break; case NO_RESULT: abort(updateInfo.apiKey, null, updateResult.reason); break; } } private void rescheduleAccordingToQuotaSpecifications(final UpdateInfo updateInfo, final UpdateWorkerTask.AuditTrailEntry auditTrailEntry) { longReschedule(updateInfo, auditTrailEntry); guestService.setApiKeyStatus(updateInfo.apiKey.getId(), ApiKey.Status.STATUS_OVER_RATE_LIMIT, auditTrailEntry.stackTrace, null); } private void duplicateUpdate() { logger.warn("module=updateQueue component=worker action=duplicateUpdate guestId=" + task.getGuestId() + " connector=" + task.connectorName + " objectType=" + task.objectTypes); } private void success(ApiKey apiKey) { StringBuilder stringBuilder = new StringBuilder("module=updateQueue component=worker action=success") .append(" guestId=").append(task.getGuestId()) .append(" connector=").append(task.objectTypes); logger.info(stringBuilder.toString()); guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_UP, null, null); this.task = connectorUpdateService.setUpdateWorkerTaskStatus(task.getId(), Status.DONE); } private void abort(ApiKey apiKey, UpdateWorkerTask.AuditTrailEntry auditTrailEntry, final String reason) { StringBuilder stringBuilder = new StringBuilder("module=updateQueue component=worker action=abort") .append(" guestId=").append(task.getGuestId()) .append(" connector=").append(task.connectorName) .append(" objectType=").append(task.objectTypes); logger.info(stringBuilder.toString()); guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_PERMANENT_FAILURE, auditTrailEntry.stackTrace, reason); this.task = connectorUpdateService.setUpdateWorkerTaskStatus(task.getId(), Status.FAILED); } private void retry(UpdateInfo updateInfo, UpdateWorkerTask.AuditTrailEntry auditTrailEntry) { StringBuilder stringBuilder = new StringBuilder("module=updateQueue component=worker action=retry") .append(" guestId=").append(task.getGuestId()) .append(" connector=").append(task.connectorName) .append(" objectType=").append(task.objectTypes); if (auditTrailEntry.stackTrace!=null) { stringBuilder.append(" stackTrace=<![CDATA[") .append(auditTrailEntry.stackTrace) .append("]]>"); } logger.info(stringBuilder.toString()); int maxRetries = 0; try { maxRetries = getMaxRetries(updateInfo.apiKey.getConnector()); } catch (Throwable t) { t.printStackTrace(); } if (task.retries < maxRetries) { auditTrailEntry.nextAction = "short reschedule"; shortReschedule(updateInfo.apiKey, auditTrailEntry); } else { auditTrailEntry.nextAction = "long reschedule"; longReschedule(updateInfo, auditTrailEntry); } } private void longReschedule(UpdateInfo updateInfo, UpdateWorkerTask.AuditTrailEntry auditTrailEntry) { StringBuilder stringBuilder = new StringBuilder("module=updateQueue component=worker action=longReschedule") .append(" guestId=").append(task.getGuestId()) .append(" connector=").append(task.connectorName) .append(" objectType=").append(task.objectTypes); logger.info(stringBuilder.toString()); guestService.setApiKeyStatus(updateInfo.apiKey.getId(), ApiKey.Status.STATUS_TRANSIENT_FAILURE, auditTrailEntry.stackTrace, null); // re-schedule when we are below rate limit again Long resetTime = updateInfo.getSafeResetTime(); if (resetTime==null) { final int longRetryDelay = getLongRetryDelay(updateInfo.apiKey.getConnector()); resetTime = System.currentTimeMillis() + longRetryDelay; } connectorUpdateService.reScheduleUpdateTask(task.getId(), resetTime, false, auditTrailEntry); } private void shortReschedule(ApiKey apiKey, UpdateWorkerTask.AuditTrailEntry auditTrailEntry) { StringBuilder sb = new StringBuilder("module=updateQueue component=worker action=shortReschedule") .append(" guestId=").append(task.getGuestId()) .append(" connector=").append(task.connectorName) .append(" objectType=").append(task.objectTypes) .append(" retries=").append(String.valueOf(task.retries)); logger.info(sb.toString()); // schedule 1 minute later, typically guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_TRANSIENT_FAILURE, auditTrailEntry.stackTrace, null); connectorUpdateService.reScheduleUpdateTask(task.getId(), System.currentTimeMillis() + getShortRetryDelay(apiKey.getConnector()), true, auditTrailEntry); } private int getMaxRetries(Connector connector) { String key = connector.getName() + ".maxRetries"; if (env.connectors.containsKey(key)) { return Integer.valueOf((String)env.connectors.getProperty(key)); } else { return Integer.valueOf((String)env.connectors.getProperty("maxRetries")); } } private int getShortRetryDelay(Connector connector) { String key = connector.getName() + ".shortRetryDelay"; if (env.connectors.containsKey(key)) { return Integer.valueOf((String)env.connectors.getProperty(key)); } else { return Integer.valueOf((String)env.connectors.getProperty("shortRetryDelay")); } } private int getLongRetryDelay(Connector connector) { String key = connector.getName() + ".longRetryDelay"; if (env.connectors.containsKey(key)) { return Integer.valueOf((String)env.connectors.getProperty(key)); } else { return Integer.valueOf((String)env.connectors.getProperty("longRetryDelay")); } } }