All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.styx.LocalFileScheduleSource Maven / Gradle / Ivy

There is a newer version: 1.0.70
Show newest version
/*-
 * -\-\-
 * Spotify Styx Local Files Schedule Source
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * 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.spotify.styx;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
import static java.util.Collections.emptySet;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Closer;
import com.spotify.styx.model.Workflow;
import com.spotify.styx.model.deprecated.Schedule;
import com.spotify.styx.schedule.ScheduleSource;
import com.sun.nio.file.SensitivityWatchEventModifier;
import com.typesafe.config.Config;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import okio.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A {@link ScheduleSource} that monitors a local directory for yaml files.
 *
 * 

The contents of the files are parsed into a {@link YamlScheduleDefinition} and * {@link Workflow} objects. * *

todo * - handle if watch dir is deleted (warn, try to re-watch until dir appears again) */ class LocalFileScheduleSource implements ScheduleSource { private static final WatchEvent.Kind[] EVENTS = { ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE }; private static final String LOCAL_DIR_CONFIG_KEY = "styx.source.local.dir"; private static final long POLL_TIMEOUT_MILLIS = 100; private static final Logger LOG = LoggerFactory.getLogger(LocalFileScheduleSource.class); private final Config config; private final Closer closer; private final ScheduledExecutorService executor; private final Consumer changeListener; private final Consumer removeListener; private final Map> workflows = Maps.newHashMap(); private volatile boolean running; LocalFileScheduleSource( Config config, Closer closer, ScheduledExecutorService executor, Consumer changeListener, Consumer removeListener) { this.config = Objects.requireNonNull(config); this.closer = Objects.requireNonNull(closer); this.executor = Objects.requireNonNull(executor); this.changeListener = Objects.requireNonNull(changeListener); this.removeListener = Objects.requireNonNull(removeListener); } @Override public void start() { if (!config.hasPath(LOCAL_DIR_CONFIG_KEY)) { LOG.error("Configuration key '{}' not set", LOCAL_DIR_CONFIG_KEY); throw new RuntimeException("Can't load local file schedule source: not configured"); } final String sourceDir = config.getString(LOCAL_DIR_CONFIG_KEY); final Path path; try { path = Paths.get(sourceDir); } catch (InvalidPathException e) { LOG.error("Invalid path: {}", sourceDir, e); throw new RuntimeException("Can't load local file schedule source: invalid path", e); } final Stream list; try { list = Files.list(path); } catch (IOException e) { LOG.error("Failed to List: {}", sourceDir, e); throw new RuntimeException("Can't load local file schedule source: initial listing failed", e); } list.filter(this::isYamlFile).forEach(this::readFile); WatchService watcher; try { watcher = FileSystems.getDefault().newWatchService(); path.register(watcher, EVENTS, SensitivityWatchEventModifier.HIGH); } catch (IOException e) { LOG.error("Could not watch: {}", path, e); throw new RuntimeException("Can't load local file schedule source", e); } running = true; closer.register(() -> running = false); executor.submit(() -> poll(path, watcher)); } private void poll(Path watchPath, WatchService watchService) { LOG.info("Watching {} for schedule definitions", watchPath); try { while (running) { final WatchKey key = watchService.poll(POLL_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); if (key == null) { continue; } for (WatchEvent event : key.pollEvents()) { final WatchEvent.Kind kind = event.kind(); if (kind == OVERFLOW) { continue; } final WatchEvent pathEvent = cast(event); final Path file = watchPath.resolve(pathEvent.context()); if (!isYamlFile(file)) { continue; } final String componentId = componentId(file); LOG.debug("{} event for component {}, from file {}", kind, componentId, file); if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) { readFile(file); } if (kind == ENTRY_DELETE) { final Set deleted = workflows.getOrDefault(componentId, emptySet()); deleted.forEach(removeListener); deleted.clear(); } } // Reset the key -- this step is critical if you want to // receive further watch events. If the key is no longer valid, // the directory is inaccessible so exit the loop. boolean valid = key.reset(); if (!valid) { break; } } } catch (InterruptedException e) { LOG.warn("interrupted", e); } LOG.info("Stopped watching {}", watchPath); } private boolean isYamlFile(Path file) { final String fileName = file.getFileName().toString(); return fileName.endsWith(".yaml") || fileName.endsWith(".yml"); } private void readFile(Path file) { try { for (Workflow workflow : readWorkflows(file)) { workflows.computeIfAbsent(workflow.componentId(), (k) -> Sets.newHashSet()) .add(workflow); changeListener.accept(workflow); } } catch (IOException e) { LOG.warn("Failed to read schedule definition {}", file, e); } } private List readWorkflows(Path path) throws IOException { final byte[] bytes = Files.readAllBytes(path); LOG.debug("Read yaml file \n{}", ByteString.of(bytes).utf8()); final YamlScheduleDefinition definitions = Yaml.parseScheduleDefinition(bytes); LOG.debug("Parsed schedule definitions: {}", definitions); final String componentId = componentId(path); final URI componentUri = path.toUri(); return definitions.schedules().stream() .map(schedule -> Workflow.create(componentId, componentUri, schedule)) .collect(Collectors.toList()); } private String componentId(Path path) { return path.getFileName().toString(); } @SuppressWarnings("unchecked") private static WatchEvent cast(WatchEvent event) { return (WatchEvent) event; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy