/*
* 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 gobblin.data.management.trash;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import com.google.common.collect.Lists;
import lombok.Data;
import lombok.Getter;
/**
* Implementation of {@link ProxiedTrash} to use for testing. All operations in this implementation are noop, but user
* can get all delete operations executed using {@link #getDeleteOperations}. This implementation does not use the
* file system at all, so user can use a minimally mocked file system.
*
* <p>
* This class optionally support simulating file system delay with an internal clock. The clock does not advance
* by itself, allowing programmers fine testing over a file system with delay.
* </p>
*/
public class TestTrash extends MockTrash {
private static final String DELAY_TICKS_KEY = "gobblin.trash.test.delays.ticks";
/**
* Creates {@link java.util.Properties} that will generate a {@link gobblin.data.management.trash.TestTrash} when
* using {@link gobblin.data.management.trash.TrashFactory}.
*/
public static Properties propertiesForTestTrash() {
Properties properties = new Properties();
properties.setProperty(TrashFactory.TRASH_TEST, Boolean.toString(true));
return properties;
}
/**
* Mutates properties so that creating a TestTrash with this properties object will simulate delay in the
* filesystem.
*
* <p>
* When simulating delay, any operation related to the filesystem will initially block indefinitely. The test
* trash uses an internal clock that must be advanced by the user (it does not advance by itself).
* Operations are blocked for a specified number of ticks in the clock. To tick, the user must call the
* {@link #tick} method.
* </p>
*
* <p>
* For example, if delay is 2:
* * User calls testTrash.moveToTrash(new Path("/")) -> call blocks indefinitely, nothing added to delete operations
* list.
* * User calls testTrash.tick() -> call still blocked.
* * User calls testTrash.tick() -> moveToTrash call returns, operation added to delete operations list.
* </p>
*
* @param properties {@link Properties} used for building a test trash.
* @param delay All calls to {@link TestTrash} involving file system will simulate a delay of this many ticks.
*/
public static void simulateDelay(Properties properties, long delay) {
properties.setProperty(DELAY_TICKS_KEY, Long.toString(delay));
}
/**
* Abstraction for a delete operation. Stores deleted {@link org.apache.hadoop.fs.Path} and user proxied for the
* deletion. When calling {@link #moveToTrash}, {@link #user} is set to null.
*/
@Data
public static class DeleteOperation {
private final Path path;
private final String user;
}
@Getter
private final List<DeleteOperation> deleteOperations;
private final String user;
private final long delay;
private long clockState;
private final Lock lock;
private final Condition clockStateUpdated;
private final Condition signalReceived;
private final AtomicLong callsReceivedSignal;
private final AtomicLong operationsWaiting;
private final AtomicLong operationsReceived;
@Getter
private final boolean simulate;
@Getter
private final boolean skipTrash;
public TestTrash(FileSystem fs, Properties props, String user) throws IOException {
super(fs, propertiesForConstruction(props), user);
this.user = user;
this.deleteOperations = Lists.newArrayList();
this.simulate =
props.containsKey(TrashFactory.SIMULATE) && Boolean.parseBoolean(props.getProperty(TrashFactory.SIMULATE));
this.skipTrash =
props.containsKey(TrashFactory.SKIP_TRASH) && Boolean.parseBoolean(props.getProperty(TrashFactory.SKIP_TRASH));
this.operationsReceived = new AtomicLong();
this.lock = new ReentrantLock();
this.clockStateUpdated = this.lock.newCondition();
this.signalReceived = this.lock.newCondition();
this.clockState = 0;
this.operationsWaiting = new AtomicLong();
this.callsReceivedSignal = new AtomicLong();
if (props.containsKey(DELAY_TICKS_KEY)) {
this.delay = Long.parseLong(props.getProperty(DELAY_TICKS_KEY));
} else {
this.delay = 0;
}
}
@Override
public boolean moveToTrash(Path path) throws IOException {
this.operationsReceived.incrementAndGet();
addDeleteOperation(new DeleteOperation(path, null));
return true;
}
@Override
public boolean moveToTrashAsUser(Path path, String user) throws IOException {
this.operationsReceived.incrementAndGet();
addDeleteOperation(new DeleteOperation(path, user));
return true;
}
@Override
public boolean moveToTrashAsOwner(Path path) throws IOException {
return moveToTrashAsUser(path, this.user);
}
public long getOperationsReceived() {
return this.operationsReceived.get();
}
public long getOperationsWaiting() {
return this.operationsWaiting.get();
}
/**
* Advance the internal clock by one tick. The call will block until all appropriate threads finish adding their
* {@link DeleteOperation}s to the list.
*/
public void tick() {
this.lock.lock();
try {
// Advance clock
this.clockState++;
// Acquire lock, register how many threads are waiting for signal
long callsAwaitingSignalOld = this.operationsWaiting.get();
this.callsReceivedSignal.set(0);
this.operationsWaiting.set(0);
// Send signal
this.clockStateUpdated.signalAll();
while (this.callsReceivedSignal.get() < callsAwaitingSignalOld) {
// this will release the lock, and it will periodically compare the number of threads that were awaiting
// signal against the number of threads that have already received the signal. Therefore, this statement
// will block until all threads have acked signal.
this.signalReceived.await();
}
} catch (InterruptedException ie) {
// Interrupted
} finally {
this.lock.unlock();
}
}
private void addDeleteOperation(DeleteOperation dop) {
// Acquire lock
this.lock.lock();
// Figure out when the operation can return
long executeAt = this.clockState + this.delay;
boolean firstLoop = true;
try {
// If delay is 0, this continues immediately.
while (this.clockState < executeAt) {
// If this is not the first loop, it means we have received a signal from tick, but still not at
// appropriate clock state. Ack the receive (this is done here because if it is ready to "delete", it should
// only ack after actually adding the DeleteOperation to the list).
if (!firstLoop) {
this.callsReceivedSignal.incrementAndGet();
this.signalReceived.signalAll();
}
firstLoop = false;
// Add itself to the list of calls awaiting signal
this.operationsWaiting.incrementAndGet();
// Await for signal that the clock has been updated
this.clockStateUpdated.await();
}
// Perform "delete" operation, i.e. add DeleteOperation to list
this.deleteOperations.add(dop);
// Ack receipt of signal
this.callsReceivedSignal.incrementAndGet();
this.signalReceived.signal();
} catch (InterruptedException ie) {
// Interrupted
} finally {
this.lock.unlock();
}
}
private static Properties propertiesForConstruction(Properties properties) {
Properties newProperties = new Properties();
newProperties.putAll(properties);
newProperties.setProperty(Trash.SNAPSHOT_CLEANUP_POLICY_CLASS_KEY,
NoopSnapshotCleanupPolicy.class.getCanonicalName());
newProperties.setProperty(Trash.TRASH_LOCATION_KEY, "/test/path");
return newProperties;
}
}