//******************************************************************************************************
// HadoopReplicationProvider.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), 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.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 11/12/2009 - Pinal C. Patel
// Generated original version of source code.
// 11/18/2009 - Pinal C. Patel
// Localized the use of replication log file to ReplicateArchive() method.
// 12/04/2009 - Pinal C. Patel
// Added more information to replication log file that can be useful for troubleshooting.
// 12/22/2009 - Pinal C. Patel
// Added try...catch block to the code that performs FTP file delete when an exception is
// encountered in ReplicateArchive() method.
// 12/23/2009 - Pinal C. Patel
// Added HashRequestAttempts and HashRequestWaitTime properties for better control over HDFS file
// hash request process.
// 12/24/2009 - Pinal C. Patel
// Added error handling around the hash request process due to the flaky nature of FTP server when
// dealing with large files.
// 03/03/2010 - Pinal C. Patel
// Modified to include files in sub directories of the root directory to be included in the
// replication process.
// 05/20/2010 - Pinal C. Patel
// Added the option to allow for deletion of the original files after being replicated.
// 06/10/2010 - Pinal C. Patel
// Added the option to allow multiple archive locations to be specified delimited by ';' in
// ArchiveLocation for convenience.
// Added extensive debug messages to enhance the debugging experience of the adapter.
// 09/13/2010 - Pinal C. Patel
// Modified the replication process to attempt deletion of the original file again if deletion was
// unsuccessful during previous attempt.
// 10/11/2010 - Mihir Brahmbhatt
// Updated header and license agreement.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using GSF;
using GSF.Collections;
using GSF.Configuration;
using GSF.Historian.Replication;
using GSF.IO;
using GSF.IO.Checksums;
using GSF.Net.Ftp;
using GSF.Units;
namespace Hadoop.Replication
{
///
/// Represents a provider of replication for the to Hadoop using FTP channel.
///
public class HadoopReplicationProvider : ReplicationProviderBase
{
#region [ Members ]
// Constants
///
/// Specifies the default value for the property.
///
public const int DefaultBytesPerCrc32 = 512;
///
/// Specifies the default value for the property.
///
public const int DefaultHdfsBlockSize = 64;
///
/// Specifies the default value for the property.
///
public const bool DefaultApplyBufferPadding = true;
///
/// Specifies the default value for the property.
///
public const int DefaultHashRequestAttempts = 3;
///
/// Specifies the default value for the property.
///
public const int DefaultHashRequestWaitTime = 3000;
///
/// Specifies the default value for the property.
///
public const bool DefaultDeleteOriginalFiles = false;
///
/// Length to be used for the method.
///
private const int FilePathTrimLength = 30;
///
/// Name of the file where replication history information is to be serialized.
///
private const string ReplicationLogFile = "HadoopReplicationLog.xml";
// Fields
private int m_bytesPerCrc32;
private int m_hdfsBlockSize;
private bool m_applyBufferPadding;
private int m_hashRequestAttempts;
private int m_hashRequestWaitTime;
private bool m_deleteOriginalFiles;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the class.
///
public HadoopReplicationProvider()
{
m_bytesPerCrc32 = DefaultBytesPerCrc32;
m_hdfsBlockSize = DefaultHdfsBlockSize;
m_applyBufferPadding = DefaultApplyBufferPadding;
m_hashRequestAttempts = DefaultHashRequestAttempts;
m_hashRequestWaitTime = DefaultHashRequestWaitTime;
m_deleteOriginalFiles = DefaultDeleteOriginalFiles;
}
#endregion
#region [ Properties ]
///
/// Gets or sets the number of bytes at which HDFS is configured to compute a CRC32.
///
/// Value being assigned is zero or negative.
public int BytesPerCrc32
{
get
{
return m_bytesPerCrc32;
}
set
{
if (value < 1)
throw new ArgumentException("Value must be at least 1.");
m_bytesPerCrc32 = value;
}
}
///
/// Gets or sets the size, in MB, of the data blocks for HDFS where the file resides.
///
/// Value being assigned is zero or negative.
public int HdfsBlockSize
{
get
{
return m_hdfsBlockSize;
}
set
{
if (value < 1)
throw new ArgumentException("Value must be at least 1.");
m_hdfsBlockSize = value;
}
}
///
/// Gets or sets a boolean value that indicates whether the buffer used for computing file hash is to be padded with null bytes for replicating HDFS hashing bug.
///
public bool ApplyBufferPadding
{
get
{
return m_applyBufferPadding;
}
set
{
m_applyBufferPadding = value;
}
}
///
/// Gets or sets the maximum number of requests to be made to the FTP server for HDFS file hash.
///
/// The value being assigned is negative or zero.
public int HashRequestAttempts
{
get
{
return m_hashRequestAttempts;
}
set
{
if (value < 0)
throw new ArgumentException("Value must be positive");
m_hashRequestAttempts = value;
}
}
///
/// Gets or set the time, in milliseconds, to wait between requests to the FTP server for HDFS file hash.
///
/// The value being assigned is negative or zero.
public int HashRequestWaitTime
{
get
{
return m_hashRequestWaitTime;
}
set
{
if (value < 0)
throw new ArgumentException("Value must be positive");
m_hashRequestWaitTime = value;
}
}
///
/// Gets or sets a boolean value that indicates whether the original files are to be deleted after being replicated successfully.
///
public bool DeleteOriginalFiles
{
get
{
return m_deleteOriginalFiles;
}
set
{
m_deleteOriginalFiles = value;
}
}
#endregion
#region [ Methods ]
///
/// Saves settings to the config file if the property is set to true.
///
public override void SaveSettings()
{
base.SaveSettings();
if (PersistSettings)
{
// Save settings under the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings["BytesPerCrc32", true].Update(m_bytesPerCrc32);
settings["HdfsBlockSize", true].Update(m_hdfsBlockSize);
settings["ApplyBufferPadding", true].Update(m_applyBufferPadding);
settings["HashRequestAttempts", true].Update(m_hashRequestAttempts);
settings["HashRequestWaitTime", true].Update(m_hashRequestWaitTime);
settings["DeleteOriginalFiles", true].Update(m_deleteOriginalFiles);
config.Save();
}
}
///
/// Loads saved settings from the config file if the property is set to true.
///
public override void LoadSettings()
{
base.LoadSettings();
if (PersistSettings)
{
// Load settings from the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings.Add("BytesPerCrc32", m_bytesPerCrc32, "Number of bytes at which HDFS is configured to compute a CRC32.");
settings.Add("HdfsBlockSize", m_hdfsBlockSize, "Size (in MB) of the data blocks for HDFS where the file resides.");
settings.Add("ApplyBufferPadding", m_applyBufferPadding, "True if the buffer used for computing file hash is to be padded with null bytes for replicating HDFS hashing bug, otherwise False.");
settings.Add("HashRequestAttempts", m_hashRequestAttempts, "Maximum number of requests to be made to the FTP server for HDFS file hash.");
settings.Add("HashRequestWaitTime", m_hashRequestWaitTime, "Time (in milliseconds) to wait between requests to the FTP server for HDFS file hash.");
settings.Add("DeleteOriginalFiles", m_deleteOriginalFiles, "True if the original files are to be deleted after being replicated successfully; otherwise False.");
BytesPerCrc32 = settings["BytesPerCrc32"].ValueAs(m_bytesPerCrc32);
HdfsBlockSize = settings["HdfsBlockSize"].ValueAs(m_hdfsBlockSize);
ApplyBufferPadding = settings["ApplyBufferPadding"].ValueAs(m_applyBufferPadding);
HashRequestAttempts = settings["HashRequestAttempts"].ValueAs(m_hashRequestAttempts);
HashRequestWaitTime = settings["HashRequestWaitTime"].ValueAs(m_hashRequestWaitTime);
DeleteOriginalFiles = settings["DeleteOriginalFiles"].ValueAs(m_deleteOriginalFiles);
}
}
///
/// Replicates the .
///
protected override void ReplicateArchive()
{
WriteTrace("Archive replication started");
// Parse FTP client information.
Uri replicaUri = new Uri(ReplicaLocation);
string[] credentials = replicaUri.UserInfo.Split(':');
// Ensure credentials are supplied.
if (credentials.Length != 2)
throw new ArgumentException("FTP credentials are missing in ReplicaLocation.");
// Create FTP client for uploading.
FtpClient ftpClient = new FtpClient();
ftpClient.Server = replicaUri.Host;
ftpClient.Port = replicaUri.Port;
ftpClient.FileTransferProgress += FtpClient_FileTransferProgress;
// Initialize the replication log.
WriteTrace("Initializing archive replication log");
DataTable replicationLog = new DataTable("ReplicationRecord");
replicationLog.Columns.Add("DateTime");
replicationLog.Columns.Add("FileName");
replicationLog.Columns.Add("FileHash");
replicationLog.Columns.Add("FileSync");
replicationLog.Columns.Add("HashingTime");
replicationLog.Columns.Add("TransferTime");
replicationLog.Columns.Add("TransferRate");
replicationLog.Columns.Add("ServerRequests");
replicationLog.Columns.Add("ServerResponse");
if (File.Exists(FilePath.GetAbsolutePath(ReplicationLogFile)))
replicationLog.ReadXml(FilePath.GetAbsolutePath(ReplicationLogFile));
WriteTrace("Archive replication log initialized");
try
{
// Connect FTP client to server.
WriteTrace("Connecting to ftp://{0}:{1}", ftpClient.Server, ftpClient.Port);
ftpClient.Connect(credentials[0], credentials[1]);
WriteTrace("Connection successful");
WriteTrace("Changing current directory to '{0}'", replicaUri.AbsolutePath);
ftpClient.SetCurrentDirectory(replicaUri.LocalPath);
WriteTrace("Current directory changed to '{0}'", ftpClient.CurrentDirectory.FullPath);
// Process all archive location(s).
foreach (string folder in ArchiveLocation.Split(';'))
{
if (!string.IsNullOrEmpty(folder))
ReplicateToHadoop(ftpClient, folder.Trim(), replicationLog);
}
}
finally
{
ftpClient.Dispose();
replicationLog.WriteXml(FilePath.GetAbsolutePath(ReplicationLogFile));
}
WriteTrace("Archive replication complete");
}
private void ReplicateToHadoop(FtpClient ftpClient, string replicationFolder, DataTable replicationLog)
{
WriteTrace("Replicating folder '{0}'", FilePath.TrimFileName(replicationFolder, FilePathTrimLength));
// Create list of files to be replicated.
List files = new List(Directory.GetFiles(replicationFolder, "*_to_*.d", SearchOption.AllDirectories));
files.Sort();
// Process all the files in the list.
WriteTrace("Found {0} files in folder", files.Count);
foreach (string file in files)
{
// Initialize local variables.
int requests = int.MinValue;
bool uploading = false;
double hashingStartTime = double.MinValue;
double hashingTotalTime = double.MinValue;
double transferStartTime = double.MinValue;
double transferTotalTime = double.MinValue;
string justFileName = FilePath.GetFileName(file);
DataRow record = null;
DataRow[] filter = replicationLog.Select(string.Format("FileName ='{0}'", justFileName));
FileInfo fileInfo = new FileInfo(file);
WriteTrace("Replicating file '{0}' of size {1:0,0} KB", FilePath.TrimFileName(file, FilePathTrimLength), Convert.ToInt32(fileInfo.Length / 1024D));
try
{
// Continue to "ping" FTP server so that it knows we are alive and well
ftpClient.ControlChannel.Command("NOOP");
// Compute HDFS file hash.
WriteTrace("Hashing file");
hashingStartTime = Common.SystemTimer;
byte[] localHash = ComputeHdfsFileHash(file, m_bytesPerCrc32, m_hdfsBlockSize, m_applyBufferPadding);
hashingTotalTime = Common.SystemTimer - hashingStartTime;
WriteTrace("File hashed in {0} seconds", Convert.ToInt32(hashingTotalTime));
// Check if file is to be uploaded.
if (filter.Length == 0 ||
filter[0]["FileSync"].ToString() == "Fail" ||
localHash.CompareTo(ByteEncoding.Hexadecimal.GetBytes(filter[0]["FileHash"].ToString())) != 0)
{
// Upload file to HDFS since:
// 1) File has not been replicated previously.
// OR
// 2) File has been replicated in the past, but its content has changed since then.
uploading = true;
WriteTrace("Uploading file");
transferStartTime = Common.SystemTimer;
ftpClient.CurrentDirectory.PutFile(file);
transferTotalTime = Common.SystemTimer - transferStartTime;
WriteTrace("File uploaded in {0} seconds", Convert.ToInt32(transferTotalTime));
// Request file hash from HDFS.
for (requests = 1; requests <= m_hashRequestAttempts; requests++)
{
try
{
// Wait before request.
WriteTrace("Waiting {0} seconds before HDFS hash request", m_hashRequestWaitTime / 1000);
Thread.Sleep(m_hashRequestWaitTime);
// Request file hash.
WriteTrace("Requesting HDFS hash (Attempt {0})", requests);
ftpClient.ControlChannel.Command(string.Format("HDFSCHKSM {0}{1}", ftpClient.CurrentDirectory.FullPath, justFileName));
WriteTrace("Hash request response - {0}", ftpClient.ControlChannel.LastResponse.Message.RemoveCrLfs());
// Exit when successful.
if (ftpClient.ControlChannel.LastResponse.Code == 200)
break;
}
catch (Exception ex)
{
// Try again - Apache MINA FTP server acts funny with updoad & hash check of large files.
WriteTrace("Hash request error - {0}", ex.Message);
}
}
// Initialize replication log entry.
if (filter.Length > 0)
{
record = filter[0];
}
else
{
record = replicationLog.NewRow();
replicationLog.Rows.Add(record);
}
// Update replication log entry.
record["DateTime"] = DateTime.UtcNow;
record["FileName"] = justFileName;
record["FileHash"] = ByteEncoding.Hexadecimal.GetString(localHash);
record["HashingTime"] = hashingTotalTime.ToString("0.000");
record["TransferTime"] = transferTotalTime.ToString("0.000");
record["TransferRate"] = ((new FileInfo(file).Length / SI2.Kilo) / transferTotalTime).ToString("0.00");
record["ServerRequests"] = requests < m_hashRequestAttempts ? requests : m_hashRequestAttempts;
record["ServerResponse"] = ftpClient.ControlChannel.LastResponse.Message.RemoveCrLfs();
// Compare local and HDFS hash.
if (ftpClient.ControlChannel.LastResponse.Code == 200 &&
localHash.CompareTo(ByteEncoding.Hexadecimal.GetBytes(ftpClient.ControlChannel.LastResponse.Message.RemoveCrLfs().Split(':')[1])) == 0)
{
// File uploaded and hashes match.
record["FileSync"] = "Pass";
WriteTrace("Replication successful");
// Deleted original file after replication.
if (m_deleteOriginalFiles)
DeleteOriginalFile(file, fileInfo);
// Notify about the successful replication.
OnReplicationProgress(new ProcessProgress("ReplicateArchive", justFileName, 1, 1));
}
else
{
// Hashes are different - possible causes:
// 1) Local file got modified after hash was computed locally.
// OR
// 2) Local and remote hashing algorithms are not the same.
record["FileSync"] = "Fail";
WriteTrace("Replication unsuccessful");
throw new InvalidDataException("File hash mismatch");
}
// Write replication entry to the log file.
WriteTrace("Updating replication log file");
replicationLog.WriteXml(FilePath.GetAbsolutePath(ReplicationLogFile));
WriteTrace("Replication log file updated");
}
else
{
WriteTrace("Replication skipped - file content unchanged");
// Deleted original file if skipped previously.
if (m_deleteOriginalFiles)
DeleteOriginalFile(file, fileInfo);
}
}
catch (Exception ex)
{
WriteTrace("Replication error - {0}", ex.Message);
// Delete file from FTP site if an exception is encountered when processing the file.
try
{
if (uploading && ftpClient.IsConnected)
{
WriteTrace("Deleting partial upload");
ftpClient.CurrentDirectory.RemoveFile(justFileName);
WriteTrace("Partial upload deleted");
}
}
catch (Exception exDelete)
{
WriteTrace("Delete error - {0}", exDelete.Message);
}
if (ex is ThreadAbortException)
// Re-throw the encountered exception.
throw;
else
// Notify about the encountered exception.
OnReplicationException(ex);
}
}
WriteTrace("Folder '{0}' replicated", FilePath.TrimFileName(replicationFolder, FilePathTrimLength));
}
private static void DeleteOriginalFile(string file, FileInfo fileInfo)
{
try
{
WriteTrace("Deleting original file");
if (fileInfo.IsReadOnly)
fileInfo.IsReadOnly = false;
File.Delete(file);
WriteTrace("Original file deleted");
}
catch (Exception ex)
{
WriteTrace("File delete error - {0}", ex.Message);
}
}
private void FtpClient_FileTransferProgress(object sender, EventArgs, TransferDirection> e)
{
// Show transfer progress at 10% increaments.
long percentComplete = (e.Argument1.Complete * 100) / e.Argument1.Total;
if ((percentComplete % 10) == 0)
WriteTrace("{0}ed {1}%", e.Argument2, percentComplete);
}
#endregion
#region [ Static ]
// Static Methods
///
/// Computes a MD5 hash of the file content using the algorithm used by HDFS.
///
/// Name of the file for which the hash is to be computed.
/// Number of bytes at which HDFS is configured to compute a CRC32.
/// Size (in MB) of the data blocks for HDFS where the file resides.
/// true if the buffer used for computing file hash is to be padded with null bytes for replicating HDFS hashing bug, otherwise false.
/// An of s containing the file hash.
public static byte[] ComputeHdfsFileHash(string fileName, int bytesPerCrc32, int hdfsBlockSize, bool applyBufferPadding)
{
int bytesRead = 0;
int blockCount = 0;
byte[] blockCRC = null;
byte[] blockMD5 = null;
byte[] fileHash = null;
FileStream fileStream = null;
List blockCRCs = new List();
List blockMD5s = new List();
byte[] readBuffer = new byte[bytesPerCrc32];
MD5CryptoServiceProvider hasher = new MD5CryptoServiceProvider();
try
{
WriteTrace("Computing HDFS file hash for '{0}'", FilePath.TrimFileName(fileName, FilePathTrimLength));
// Open file whose hash is to be computed.
fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// Calculate the number of HDFS blocks used by the file on HDFS.
blockCount = (int)Math.Ceiling((double)fileStream.Length / (double)(hdfsBlockSize * SI2.Mega));
// For each HDFS block used by the file on HDFS:
// 1) Compute CRC32s at every "bytesPerCrc32" bytes (default is 512 bytes) and store it in a buffer.
// 2) Compute block MD5 hash by computing a MD5 hash of the buffer that contains the block CRC32s.
for (int i = 1; i <= blockCount; i++)
{
// Clear existing data from CRC32 buffer.
blockCRCs.Clear();
while ((bytesRead = fileStream.Read(readBuffer, 0, readBuffer.Length)) != 0)
{
// Read "bytesPerCrc32" bytes from the file and compute CRC32.
blockCRC = BigEndian.GetBytes(readBuffer.Crc32Checksum(0, bytesRead));
// Add big-endian byte array of the computed CRC32 to CRC32 buffer.
blockCRCs.AddRange(blockCRC);
// Stop reading and compute block MD5 hash when a HDFS block data has been processed.
if (fileStream.Position >= i * hdfsBlockSize * SI2.Mega)
break;
}
// Compute block MD5 hash from the buffer containg block CRC32s.
blockMD5 = hasher.ComputeHash(blockCRCs.ToArray());
// Store the computed block MD5 hash in a buffer that will be used for computing the file hash.
if (applyBufferPadding)
{
// Apply padding - this replicates a bug in HDFS file hashing algorithm.
if (blockMD5s.Count == 0)
// Initialize the buffer to 32 bytes.
blockMD5s.AddRange(new byte[32]);
else if (blockMD5s.Count < blockMD5.Length * i)
// Extend the buffer twice its current size.
blockMD5s.AddRange(new byte[(blockMD5s.Count * 2) - blockMD5s.Count]);
blockMD5s.UpdateRange((i - 1) * blockMD5.Length, blockMD5);
}
else
{
// Don't apply padding - this will compute the correct HDFS file hash as per its design.
blockMD5s.AddRange(blockMD5);
}
WriteTrace("Block {0} MD5 - {1}", i, ByteEncoding.Hexadecimal.GetString(blockMD5));
}
// Compute the final file hash from the buffer that contains block MD5 hashes.
fileHash = hasher.ComputeHash(blockMD5s.ToArray());
WriteTrace("HDFS file hash - {0}", ByteEncoding.Hexadecimal.GetString(fileHash));
}
finally
{
if (fileStream != null)
fileStream.Dispose();
}
return fileHash;
}
private static void WriteTrace(string message, params object[] args)
{
Trace.WriteLine(DateTime.UtcNow + ": " + string.Format(message, args), typeof(HadoopReplicationProvider).Name);
}
#endregion
}
}