/* * 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.addthis.hydra.data.tree.prop; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import com.addthis.bundle.core.Bundle; import com.addthis.bundle.core.BundleField; import com.addthis.bundle.util.ValueUtil; import com.addthis.bundle.value.ValueFactory; import com.addthis.bundle.value.ValueObject; import com.addthis.codec.annotations.FieldConfig; import com.addthis.codec.codables.Codable; import com.addthis.hydra.data.tree.DataTreeNode; import com.addthis.hydra.data.tree.DataTreeNodeUpdater; import com.addthis.hydra.data.tree.TreeDataParameters; import com.addthis.hydra.data.tree.TreeNodeData; import com.addthis.hydra.data.tree.TreeNodeDataDeferredOperation; public class DataLimitRecent extends TreeNodeData<DataLimitRecent.Config> { /** * This data attachment <span class="hydra-summary">limits child nodes to recent values</span>. * * @user-reference */ public static final class Config extends TreeDataParameters<DataLimitRecent> { /** * If non-zero then remove values that are older than the oldest bundle by this amount. * Either this field or {@link #size} must be nonzero. */ @FieldConfig(codable = true) private long age; /** * If non-zero then accept at most N bundles. * Either this field or {@link #age} must be nonzero. */ @FieldConfig(codable = true) private int size; /** * Bundle field name from which to draw time values. * If null then the system time is used while processing each bundle. */ @FieldConfig(codable = true) private String timeKey; /** * If true then use the time value to sort the observed values. * The sorting is inefficient and should be * reimplemented. Default is false. */ @FieldConfig(codable = true) private boolean sortQueue; @Override public DataLimitRecent newInstance() { DataLimitRecent dc = new DataLimitRecent(); dc.size = size; dc.age = age; dc.timeKey = timeKey; dc.sortQueue = sortQueue; dc.queue = new LinkedList<>(); return dc; } } /** */ public static final class KeyTime implements Codable { @FieldConfig(codable = true) private String key; @FieldConfig(codable = true) private long time; } @FieldConfig(codable = true) private int size; @FieldConfig(codable = true) private long age; @FieldConfig(codable = true) private long deleted; @FieldConfig(codable = true) private LinkedList<KeyTime> queue; @FieldConfig(codable = true) private String timeKey; @FieldConfig(codable = true) private boolean sortQueue; private BundleField keyAccess; @Override public boolean updateChildData(DataTreeNodeUpdater state, DataTreeNode tn, Config conf) { return false; } public static class DataLimitRecentDeferredOperation extends TreeNodeDataDeferredOperation { final DataTreeNode parentNode; final KeyTime e; DataLimitRecentDeferredOperation(DataTreeNode parentNode, KeyTime e) { this.parentNode = parentNode; this.e = e; } @Override public void run() { DataTreeNode check = parentNode.getOrCreateNode(e.key, null); if (check != null) { // TODO need 'count' field from current PathElement to be // strict instead of using 1 // TODO requires update to TreeNodeUpdater to keep ptr to // PathElement being processed /** * The following try/finally block locks 'parentNode' * when it should be locking 'check' instead. This is * to maintain the original behavior of the updateParentData() * method before the introduction of DataLimitRecentDeferredOperation. * * Prior to the introduction of deferred operations this * code was executed holding the parentNode lock. * Therefore we will continue to mimic this behavior * until the TreeNode class is properly refactored with * a policy for maintaining consistent state during * concurrent operations. */ parentNode.writeLock(); long counterVal; try { counterVal = check.incrementCounter(-1); } finally { parentNode.writeUnlock(); } if (counterVal <= 0) { if (!parentNode.deleteNode(e.key)) { // TODO booo hiss } } // check.release(); } } } @Override public boolean updateParentData(DataTreeNodeUpdater state, DataTreeNode parentNode, DataTreeNode childNode, List<TreeNodeDataDeferredOperation> deferredOps) { try { KeyTime e = new KeyTime(); e.key = childNode.getName(); if (timeKey != null) { Bundle p = state.getBundle(); if (keyAccess == null) { keyAccess = p.getFormat().getField(timeKey); } e.time = ValueUtil.asNumber(p.getValue(keyAccess)).asLong().getLong(); } else { e.time = System.currentTimeMillis(); } queue.addFirst(e); if ((size > 0 && queue.size() > size) || (age > 0 && e.time - queue.peekLast().time > age)) { if (sortQueue) { Collections.sort(queue, new Comparator<KeyTime>() { @Override public int compare(KeyTime o, KeyTime o1) { if (o.time > o1.time) { return -1; } else if (o.time < o1.time) { return 1; } else { return 0; } } }); } e = queue.removeLast(); deferredOps.add(new DataLimitRecentDeferredOperation(parentNode, e)); } } catch (Exception e) { throw new RuntimeException(e); } return false; } @Override public ValueObject getValue(String key) { return ValueFactory.create(deleted); } }