| /* $Id$ */ |
| |
| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.manifoldcf.crawler.jobs; |
| |
| import java.util.*; |
| import java.io.*; |
| |
| import org.apache.manifoldcf.crawler.system.Logging; |
| |
| /** Debugging class to keep track of recent modifications to the jobqueue table, |
| * along with context as to where it occurred. If a jobqueue state error occurs, |
| * we can then print out all of the pertinent history and find the culprit. |
| */ |
| public class TrackerClass |
| { |
| // The goal of this class is to keep track of at least some of the history |
| // potentially affecting each record. |
| protected final static long HISTORY_LENGTH = 60000L * 15; // 15 minutes |
| |
| // Active transaction |
| protected final static Map<String,TransactionData> transactionData = new HashMap<String,TransactionData>(); |
| |
| // Modification history |
| protected final static List<HistoryRecord> history = new ArrayList<HistoryRecord>(); |
| |
| // Place where we keep track of individual modifications |
| private TrackerClass() |
| { |
| } |
| |
| /** Add a single record event, as yet uncommitted */ |
| public static void noteRecordChange(Long recordID, int newStatus, String description) |
| { |
| if (Logging.diagnostics.isDebugEnabled()) |
| addChange(new RecordChange(recordID, newStatus, description)); |
| } |
| |
| /** Add a global event, as yet uncommitted, which has the potential |
| * to affect any record's state in a given job. |
| */ |
| public static void noteJobChange(Long jobID, String description) |
| { |
| if (Logging.diagnostics.isDebugEnabled()) |
| addChange(new JobChange(jobID, description)); |
| } |
| |
| /** Add a global event, as yet uncommitted, which has the potential |
| * to affect the state of any record. |
| */ |
| public static void noteGlobalChange(String description) |
| { |
| if (Logging.diagnostics.isDebugEnabled()) |
| addChange(new GlobalChange(description)); |
| } |
| |
| protected static void addChange(DataChange dc) |
| { |
| String threadName = Thread.currentThread().getName(); |
| TransactionData td; |
| synchronized (transactionData) |
| { |
| td = transactionData.get(threadName); |
| if (td == null) |
| { |
| td = new TransactionData(); |
| transactionData.put(threadName,td); |
| } |
| } |
| td.addChange(dc); |
| } |
| |
| /** Note that we are about to commit. |
| */ |
| public static void notePrecommit() |
| { |
| if (!Logging.diagnostics.isDebugEnabled()) |
| return; |
| |
| long currentTime = System.currentTimeMillis(); |
| String threadName = Thread.currentThread().getName(); |
| |
| TransactionData td; |
| synchronized (transactionData) |
| { |
| td = transactionData.get(threadName); |
| } |
| |
| if (td == null) |
| return; |
| |
| HistoryRecord hr = new PrecommitEvent(new Exception("Precommit stack trace"),currentTime,threadName,td); |
| |
| synchronized (history) |
| { |
| history.add(hr); |
| } |
| } |
| |
| /** Note a read status operation. |
| */ |
| public static void noteRead(Long recordID) |
| { |
| if (!Logging.diagnostics.isDebugEnabled()) |
| return; |
| |
| long currentTime = System.currentTimeMillis(); |
| String threadName = Thread.currentThread().getName(); |
| |
| HistoryRecord hr = new ReadEvent(new Exception("Read stack trace"),currentTime,threadName,recordID); |
| |
| synchronized (history) |
| { |
| history.add(hr); |
| } |
| } |
| |
| /** Note about to read status operation. |
| */ |
| public static void notePreread(Long recordID) |
| { |
| if (!Logging.diagnostics.isDebugEnabled()) |
| return; |
| |
| long currentTime = System.currentTimeMillis(); |
| String threadName = Thread.currentThread().getName(); |
| |
| HistoryRecord hr = new PrereadEvent(new Exception("Pre-read stack trace"),currentTime,threadName,recordID); |
| |
| synchronized (history) |
| { |
| history.add(hr); |
| } |
| } |
| |
| /** Note a commit operation. |
| */ |
| public static void noteCommit() |
| { |
| if (!Logging.diagnostics.isDebugEnabled()) |
| return; |
| |
| long currentTime = System.currentTimeMillis(); |
| String threadName = Thread.currentThread().getName(); |
| |
| TransactionData td; |
| synchronized (transactionData) |
| { |
| td = transactionData.get(threadName); |
| transactionData.remove(threadName); |
| } |
| |
| if (td == null) |
| return; |
| |
| HistoryRecord hr = new CommitEvent(new Exception("Commit stack trace"),currentTime,threadName,td); |
| |
| // Only keep stuff around for an hour |
| long removalCutoff = currentTime - HISTORY_LENGTH; |
| synchronized (history) |
| { |
| history.add(hr); |
| // Clean out older records |
| // MHL - this logic is wrong |
| while (history.size() > 0) |
| { |
| HistoryRecord oldRecord = history.get(0); |
| if (oldRecord.isFlushable(removalCutoff)) |
| history.remove(0); |
| else |
| break; |
| } |
| } |
| |
| } |
| |
| /** Note a rollback operation. |
| */ |
| public static void noteRollback() |
| { |
| if (!Logging.diagnostics.isDebugEnabled()) |
| return; |
| |
| String threadName = Thread.currentThread().getName(); |
| synchronized (transactionData) |
| { |
| transactionData.remove(threadName); |
| } |
| } |
| |
| public static void printForensics(Long recordID, int existingStatus) |
| { |
| if (Logging.diagnostics.isDebugEnabled()) |
| { |
| synchronized (transactionData) |
| { |
| synchronized (history) |
| { |
| Logging.diagnostics.debug("==== Forensics for record "+recordID+", current status: "+existingStatus+" ===="); |
| Logging.diagnostics.debug("=== Current stack trace ===",new Exception("Forensics stack trace")); |
| Logging.diagnostics.debug("=== Active transactions ==="); |
| for (String threadName : transactionData.keySet()) |
| { |
| for (DataChange dc : transactionData.get(threadName).getChanges()) |
| { |
| if (dc.applies(recordID)) |
| { |
| Logging.diagnostics.debug("Thread '"+threadName+"' was doing things to this record: " + dc.getDescription()); |
| } |
| } |
| } |
| Logging.diagnostics.debug("=== Pertinent History ==="); |
| for (HistoryRecord hr : history) |
| { |
| if (hr.applies(recordID)) |
| { |
| hr.print(); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| protected static class TransactionData |
| { |
| protected final List<DataChange> changes = new ArrayList<DataChange>(); |
| |
| public TransactionData() |
| { |
| } |
| |
| public void addChange(DataChange change) |
| { |
| changes.add(change); |
| } |
| |
| public List<DataChange> getChanges() |
| { |
| return changes; |
| } |
| |
| public boolean applies(Long recordID) |
| { |
| for (DataChange dc : changes) |
| { |
| if (dc.applies(recordID)) |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| protected abstract static class DataChange |
| { |
| protected final String description; |
| |
| public DataChange(String description) |
| { |
| this.description = description; |
| } |
| |
| public String getDescription() |
| { |
| return description; |
| } |
| |
| public abstract boolean applies(Long recordID); |
| |
| } |
| |
| protected abstract static class HistoryRecord |
| { |
| protected final long timestamp; |
| protected final Exception trace; |
| protected final String threadName; |
| |
| public HistoryRecord(Exception trace, long timestamp, String threadName) |
| { |
| this.trace = trace; |
| this.timestamp = timestamp; |
| this.threadName = threadName; |
| } |
| |
| public void print(String description) |
| { |
| Logging.diagnostics.debug("== "+description+" by '"+threadName+"' at "+new Long(timestamp)+" ==",trace); |
| } |
| |
| public boolean isFlushable(long timestamp) |
| { |
| return this.timestamp < timestamp; |
| } |
| |
| public abstract boolean applies(Long recordID); |
| |
| public abstract void print(); |
| |
| } |
| |
| protected static class CommitEvent extends HistoryRecord |
| { |
| protected final TransactionData transactionData; |
| |
| public CommitEvent(Exception trace, long timestamp, String threadName, TransactionData transactionData) |
| { |
| super(trace,timestamp,threadName); |
| this.transactionData = transactionData; |
| } |
| |
| @Override |
| public void print() |
| { |
| super.print("Commit transaction"); |
| Logging.diagnostics.debug(" Transaction includes:"); |
| for (DataChange dc : transactionData.getChanges()) |
| { |
| Logging.diagnostics.debug(" "+dc.getDescription()); |
| } |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return transactionData.applies(recordID); |
| } |
| |
| } |
| |
| protected static class PrecommitEvent extends HistoryRecord |
| { |
| protected final TransactionData transactionData; |
| |
| public PrecommitEvent(Exception trace, long timestamp, String threadName, TransactionData transactionData) |
| { |
| super(trace,timestamp,threadName); |
| this.transactionData = transactionData; |
| } |
| |
| @Override |
| public void print() |
| { |
| super.print("About to commit transaction"); |
| Logging.diagnostics.debug(" Transaction includes:"); |
| for (DataChange dc : transactionData.getChanges()) |
| { |
| Logging.diagnostics.debug(" "+dc.getDescription()); |
| } |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return transactionData.applies(recordID); |
| } |
| } |
| |
| protected static class ReadEvent extends HistoryRecord |
| { |
| protected final Long recordID; |
| |
| public ReadEvent(Exception trace, long timestamp, String threadName, Long recordID) |
| { |
| super(trace,timestamp,threadName); |
| this.recordID = recordID; |
| } |
| |
| @Override |
| public void print() |
| { |
| super.print("Read status"); |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return recordID.equals(this.recordID); |
| } |
| } |
| |
| protected static class PrereadEvent extends HistoryRecord |
| { |
| protected final Long recordID; |
| |
| public PrereadEvent(Exception trace, long timestamp, String threadName, Long recordID) |
| { |
| super(trace,timestamp,threadName); |
| this.recordID = recordID; |
| } |
| |
| @Override |
| public void print() |
| { |
| super.print("About to read status"); |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return recordID.equals(this.recordID); |
| } |
| } |
| |
| protected static class RecordChange extends DataChange |
| { |
| protected final Long recordID; |
| protected final int newStatus; |
| |
| public RecordChange(Long recordID, int newStatus, String description) |
| { |
| super(description); |
| this.recordID = recordID; |
| this.newStatus = newStatus; |
| } |
| |
| @Override |
| public String getDescription() |
| { |
| return "Record "+recordID+" status modified to "+newStatus+": "+super.getDescription(); |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return recordID.equals(this.recordID); |
| } |
| } |
| |
| protected static class JobChange extends DataChange |
| { |
| protected final Long jobID; |
| |
| public JobChange(Long jobID, String description) |
| { |
| super(description); |
| this.jobID = jobID; |
| } |
| |
| @Override |
| public String getDescription() |
| { |
| return "All job related records modified for job "+jobID+": "+super.getDescription(); |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return true; |
| } |
| } |
| |
| protected static class GlobalChange extends DataChange |
| { |
| public GlobalChange(String description) |
| { |
| super(description); |
| } |
| |
| @Override |
| public String getDescription() |
| { |
| return "All records modified: "+super.getDescription(); |
| } |
| |
| @Override |
| public boolean applies(Long recordID) |
| { |
| return true; |
| } |
| } |
| |
| } |