PageRenderTime 28ms CodeModel.GetById 1ms RepoModel.GetById 1ms app.codeStats 0ms

/core/src/main/java/io/keen/client/java/FileEventStore.java

https://gitlab.com/vectorci/KeenClient-Java
Java | 368 lines | 199 code | 39 blank | 130 comment | 32 complexity | 33d8607049fd6f23596150ee900f7ee8 MD5 | raw file
  1. package io.keen.client.java;
  2. import java.io.File;
  3. import java.io.FileFilter;
  4. import java.io.FileOutputStream;
  5. import java.io.IOException;
  6. import java.io.OutputStream;
  7. import java.io.OutputStreamWriter;
  8. import java.io.Writer;
  9. import java.util.ArrayList;
  10. import java.util.Arrays;
  11. import java.util.Calendar;
  12. import java.util.Collections;
  13. import java.util.Comparator;
  14. import java.util.HashMap;
  15. import java.util.List;
  16. import java.util.Locale;
  17. import java.util.Map;
  18. /**
  19. * Implementation of the {@link io.keen.client.java.KeenEventStore} interface using the file system
  20. * to cache events in between queueing and batch posting.
  21. *
  22. * @author Kevin Litwack (kevin@kevinlitwack.com)
  23. * @since 2.0.0
  24. */
  25. public class FileEventStore implements KeenEventStore {
  26. ///// PUBLIC CONSTRUCTORS /////
  27. /**
  28. * Constructs a new File-based event store.
  29. *
  30. * @param root The root directory in which to store queued event files.
  31. * @throws IOException If the provided {@code root} isn't an existing directory.
  32. */
  33. public FileEventStore(File root) throws IOException {
  34. if (!root.exists() || !root.isDirectory()) {
  35. throw new IOException("Event store root '" + root + "' must exist and be a directory");
  36. }
  37. this.root = root;
  38. }
  39. ///// PUBLIC METHODS /////
  40. /**
  41. * {@inheritDoc}
  42. */
  43. @Override
  44. public Object store(String projectId, String eventCollection,
  45. String event) throws IOException {
  46. // Prepare the collection cache directory.
  47. File collectionCacheDir = prepareCollectionDir(projectId, eventCollection);
  48. // Create the cache file.
  49. Calendar timestamp = Calendar.getInstance();
  50. File cacheFile = getFileForEvent(collectionCacheDir, timestamp);
  51. // Write the event to the cache file.
  52. Writer writer = null;
  53. try {
  54. OutputStream out = new FileOutputStream(cacheFile);
  55. writer = new OutputStreamWriter(out, ENCODING);
  56. writer.write(event);
  57. } finally {
  58. KeenUtils.closeQuietly(writer);
  59. }
  60. // Return the file as the handle to use for retrieving/removing the event.
  61. return cacheFile;
  62. }
  63. /**
  64. * {@inheritDoc}
  65. */
  66. @Override
  67. public String get(Object handle) throws IOException {
  68. if (!(handle instanceof File)) {
  69. throw new IllegalArgumentException("Expected File, but was " + handle.getClass());
  70. }
  71. File eventFile = (File) handle;
  72. if (eventFile.exists() && eventFile.isFile()) {
  73. return KeenUtils.convertFileToString(eventFile);
  74. } else {
  75. return null;
  76. }
  77. }
  78. /**
  79. * {@inheritDoc}
  80. */
  81. @Override
  82. public void remove(Object handle) throws IOException {
  83. if (!(handle instanceof File)) {
  84. throw new IllegalArgumentException("Expected File, but was " + handle.getClass());
  85. }
  86. File eventFile = (File) handle;
  87. if (eventFile.exists() && eventFile.isFile()) {
  88. if (eventFile.delete()) {
  89. KeenLogging.log(String.format(Locale.US, "Successfully deleted file: %s",
  90. eventFile.getAbsolutePath()));
  91. } else {
  92. KeenLogging.log(String.format(Locale.US,
  93. "CRITICAL ERROR: Could not remove event at %s",
  94. eventFile.getAbsolutePath()));
  95. }
  96. } else {
  97. KeenLogging.log(String.format(Locale.US, "WARNING: no event found at %s",
  98. eventFile.getAbsolutePath()));
  99. }
  100. }
  101. /**
  102. * {@inheritDoc}
  103. */
  104. @Override
  105. public Map<String, List<Object>> getHandles(String projectId) throws IOException {
  106. File projectDir = getProjectDir(projectId, false);
  107. if (projectDir.exists() && projectDir.isDirectory()) {
  108. return getHandlesFromProjectDirectory(projectDir);
  109. } else {
  110. return new HashMap<String, List<Object>>();
  111. }
  112. }
  113. ///// PRIVATE CONSTANTS /////
  114. /**
  115. * The encoding to use when writing events to files.
  116. */
  117. private static final String ENCODING = "UTF-8";
  118. /**
  119. * The number of events that can be stored for a single collection before aging them out.
  120. */
  121. private static final int MAX_EVENTS_PER_COLLECTION = 10000;
  122. /**
  123. * The number of events to drop when aging out.
  124. */
  125. private static final int NUMBER_EVENTS_TO_FORGET = 100;
  126. ///// PRIVATE FIELDS /////
  127. private final File root;
  128. ///// PRIVATE METHODS /////
  129. /**
  130. * Gets the handle map for all collections in the specified project cache directory.
  131. *
  132. * @param projectDir The cache directory for the project.
  133. * @return The handle map. See {@link #getHandles(String)} for details.
  134. * @throws IOException If there is an error reading the event files.
  135. */
  136. private Map<String, List<Object>> getHandlesFromProjectDirectory(File projectDir) throws
  137. IOException {
  138. File[] collectionDirs = getSubDirectories(projectDir);
  139. Map<String, List<Object>> handleMap = new HashMap<String, List<Object>>();
  140. if (collectionDirs != null) {
  141. // iterate through the directories
  142. for (File directory : collectionDirs) {
  143. String collectionName = directory.getName();
  144. File[] files = getFilesInDir(directory);
  145. if (files != null) {
  146. List<Object> handleList = new ArrayList<Object>();
  147. handleList.addAll(Arrays.asList(files));
  148. handleMap.put(collectionName, handleList);
  149. } else {
  150. KeenLogging.log("Directory was null while getting event handles: " + collectionName);
  151. }
  152. }
  153. }
  154. return handleMap;
  155. }
  156. /**
  157. * Gets the root directory of the Keen cache, based on the root directory passed to the
  158. * constructor of this file store. If necessary, this method will attempt to create the
  159. * directory.
  160. *
  161. * @return The root directory of the cache.
  162. */
  163. private File getKeenCacheDirectory() throws IOException {
  164. File file = new File(root, "keen");
  165. if (!file.exists()) {
  166. boolean dirMade = file.mkdir();
  167. if (!dirMade) {
  168. throw new IOException("Could not make keen cache directory at: " + file.getAbsolutePath());
  169. }
  170. }
  171. return file;
  172. }
  173. /**
  174. * Gets an array containing all of the sub-directories in the given parent directory.
  175. *
  176. * @param parent The directory from which to get sub-directories.
  177. * @return An array of sub-directories.
  178. * @throws IOException If there is an error listing the files in the directory.
  179. */
  180. private File[] getSubDirectories(File parent) throws IOException {
  181. return parent.listFiles(new FileFilter() { // Can return null if there are no events
  182. public boolean accept(File file) {
  183. return file.isDirectory();
  184. }
  185. });
  186. }
  187. /**
  188. * Gets an array containing all of the files in the given directory.
  189. *
  190. * @param dir A directory.
  191. * @return An array containing all of the files in the given directory.
  192. */
  193. private File[] getFilesInDir(File dir) {
  194. return dir.listFiles(new FileFilter() {
  195. public boolean accept(File file) {
  196. return file.isFile();
  197. }
  198. });
  199. }
  200. /**
  201. * Gets the cache directory for the given project. Optionally creates the directory if it
  202. * doesn't exist.
  203. *
  204. * @param projectId The project ID.
  205. * @return The cache directory for the project.
  206. * @throws IOException
  207. */
  208. private File getProjectDir(String projectId, boolean create) throws IOException {
  209. File projectDir = new File(getKeenCacheDirectory(), projectId);
  210. if (create && !projectDir.exists()) {
  211. KeenLogging.log("Cache directory for project '" + projectId + "' doesn't exist. " +
  212. "Creating it.");
  213. if (!projectDir.mkdirs()) {
  214. throw new IOException("Could not create project cache directory '" +
  215. projectDir.getAbsolutePath() + "'");
  216. }
  217. }
  218. return projectDir;
  219. }
  220. /**
  221. * Gets the directory for events in the given collection. Creates the directory (and any
  222. * necessary parents) if it does not exist already.
  223. *
  224. * @param projectId The project ID.
  225. * @param eventCollection The name of the event collection.
  226. * @return The directory for events in the collection.
  227. */
  228. private File getCollectionDir(String projectId, String eventCollection) throws IOException {
  229. File collectionDir = new File(getProjectDir(projectId, true), eventCollection);
  230. if (!collectionDir.exists()) {
  231. KeenLogging.log("Cache directory for event collection '" + eventCollection +
  232. "' doesn't exist. Creating it.");
  233. if (!collectionDir.mkdirs()) {
  234. throw new IOException("Could not create collection cache directory '" +
  235. collectionDir.getAbsolutePath() + "'");
  236. }
  237. }
  238. return collectionDir;
  239. }
  240. /**
  241. * Gets the file to use for a new event in the given collection with the given timestamp. If
  242. * there are multiple events with identical timestamps, this method will use a counter to
  243. * create a unique file name for each.
  244. *
  245. * @param collectionDir The cache directory for the event collection.
  246. * @param timestamp The timestamp of the event.
  247. * @return The file to use for the new event.
  248. */
  249. private File getFileForEvent(File collectionDir, Calendar timestamp) throws IOException {
  250. int counter = 0;
  251. File eventFile = getNextFileForEvent(collectionDir, timestamp, counter);
  252. while (eventFile.exists()) {
  253. eventFile = getNextFileForEvent(collectionDir, timestamp, counter);
  254. counter++;
  255. }
  256. return eventFile;
  257. }
  258. /**
  259. * Gets the file to use for a new event in the given collection with the given timestamp,
  260. * using the provided counter.
  261. *
  262. * @param dir The directory in which the file should be created.
  263. * @param timestamp The timestamp to use as the base file name.
  264. * @param counter The counter to append to the file name.
  265. * @return The file to use.
  266. */
  267. private File getNextFileForEvent(File dir, Calendar timestamp, int counter) {
  268. long timestampInMillis = timestamp.getTimeInMillis();
  269. String name = Long.toString(timestampInMillis);
  270. return new File(dir, name + "." + counter);
  271. }
  272. /**
  273. * Gets the maximum number of events per collection.
  274. *
  275. * @return The maximum number of events per collection.
  276. */
  277. private int getMaxEventsPerCollection() {
  278. return MAX_EVENTS_PER_COLLECTION;
  279. }
  280. /**
  281. * Gets the number of events to discard if the maximum number of events is exceeded.
  282. *
  283. * @return The number of events to discard.
  284. */
  285. private int getNumberEventsToForget() {
  286. return NUMBER_EVENTS_TO_FORGET;
  287. }
  288. /**
  289. * Prepares the file cache for the given event collection for another event to be added. This
  290. * method checks to make sure that the maximum number of events per collection hasn't been
  291. * exceeded, and if it has, this method discards events to make room.
  292. *
  293. * @param projectId The project ID.
  294. * @param eventCollection The name of the event collection.
  295. * @return The prepared cache directory for the given project/collection.
  296. * @throws IOException If there is an error creating the directory or validating/discarding
  297. * events.
  298. */
  299. private File prepareCollectionDir(String projectId, String eventCollection) throws IOException {
  300. File collectionDir = getCollectionDir(projectId, eventCollection);
  301. // Make sure the max number of events has not been exceeded in this collection. If it has,
  302. // delete events to make room.
  303. File[] eventFiles = getFilesInDir(collectionDir);
  304. if (eventFiles.length >= getMaxEventsPerCollection()) {
  305. // need to age out old data so the cache doesn't grow too large
  306. KeenLogging.log(String.format(Locale.US, "Too many events in cache for %s, " +
  307. "aging out old data", eventCollection));
  308. KeenLogging.log(String.format(Locale.US, "Count: %d and Max: %d",
  309. eventFiles.length, getMaxEventsPerCollection()));
  310. // delete the eldest (i.e. first we have to sort the list by name)
  311. List<File> fileList = Arrays.asList(eventFiles);
  312. Collections.sort(fileList, new Comparator<File>() {
  313. @Override
  314. public int compare(File file, File file1) {
  315. return file.getAbsolutePath().compareToIgnoreCase(file1.getAbsolutePath());
  316. }
  317. });
  318. for (int i = 0; i < getNumberEventsToForget(); i++) {
  319. File f = fileList.get(i);
  320. if (!f.delete()) {
  321. KeenLogging.log(String.format(Locale.US,
  322. "CRITICAL: can't delete file %s, cache is going to be too big",
  323. f.getAbsolutePath()));
  324. }
  325. }
  326. }
  327. return collectionDir;
  328. }
  329. }