views:

265

answers:

2

I have a web service that checks a dictionary to see if a file exists and then if it does exist it reads the file, otherwise it saves to the file. This is from a web app. I wonder what is the best way to do this because I occasionally get a FileNotFoundException exception if the same file is accessed at the same time. Here's the relevant parts of the code:

String signature;
signature = "FILE," + value1 + "," + value2 + "," + value3 + "," + value4;     // this is going to be the filename

string result;            
MultipleRecordset mrSummary = new MultipleRecordset();  // MultipleRecordset is an object that retrieves data from a sql server database

if (mrSummary.existsFile(signature))
{                
    result = mrSummary.retrieveFile(signature);                
}
else
{                
    result = mrSummary.getMultipleRecordsets(System.Configuration.ConfigurationManager.ConnectionStrings["MyConnectionString"].ConnectionString.ToString(), value1, value2, value3, value4);
    mrSummary.saveFile(signature, result);
}

Here's the code to see if the file already exists:

private static Dictionary<String, bool> dict = new Dictionary<String, bool>();

public bool existsFile(string signature)
{                       
    if (dict.ContainsKey(signature))
    {
        return true;
    }
    else
    {
        return false;
    }                                
}

Here's what I use to retrieve if it already exists:

try
{                               

    byte[] buffer;
    FileStream fileStream = new FileStream(@System.Configuration.ConfigurationManager.AppSettings["CACHEPATH"] + filename, FileMode.Open, FileAccess.Read, FileShare.Read);
      try
      {
        int length = 0x8000;  // get file length
        buffer = new byte[length];            // create buffer
        int count;                            // actual number of bytes read
        JSONstring = "";
        while ((count = fileStream.Read(buffer, 0, length)) > 0)
        {
            JSONstring += System.Text.ASCIIEncoding.ASCII.GetString(buffer, 0, count);
        }
      }
      finally
      {
        fileStream.Close();
      }
}
catch (Exception e)
{

    JSONstring = "{\"error\":\"" + e.ToString() + "\"}";                
}

If the file doesn't previously exist it saves the JSON to the file:

try 
{                
    if (dict.ContainsKey(filename) == false)
    {
        dict.Add(filename, true);
    }
    else
    {
        this.retrieveFile(filename, ipaddress);
    }

}
catch
{


}


try
{
    TextWriter tw = new StreamWriter(@System.Configuration.ConfigurationManager.AppSettings["CACHEPATH"] + filename);
    tw.WriteLine(JSONstring);
    tw.Close();
}
catch { 

}

Here are the details to the exception I sometimes get from running the above code:

System.IO.FileNotFoundException: Could not find file 'E:\inetpub\wwwroot\cache\FILE,36,36.25,14.5,14.75'.
File name: 'E:\inetpub\wwwroot\cache\FILE,36,36.25,14.5,14.75'
   at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at com.myname.business.MultipleRecordset.retrieveFile(String filename, String ipaddress)
+2  A: 

You get a FileNotFoundException because you add the filename to the dictionary before actually writing the file. Thus, concurrent operations on the same file will cause this problem.

Adding it to the dictionary after writing will only create a new problem however: it will start trying to write to the file simultaneously (and failing miserably). If performance is critical, I would change my dictionary to a Dictionary<string, object> and use the value as a synchronization object at the file level. You will also need to add a separate synchronization object for checking and adding to the dictionary itself.

This is probably overkill however, and it would require manually using Monitor.Enter and Monitor.Exit. Here's a slightly simpler implementation:

static HashSet<string> files = new HashSet<string>();
static object syncRoot = new object();

void Whatever(string filename, string ipaddress)
{
    bool fileFound;

    lock (syncRoot)
    {
        fileFound = files.Contains(filename);
        if (!fileFound)
        {
            files.Add(filename);
            // Code to write file here
        }
    }

    if (fileFound)
    {
        retrieveFile(filename, ipaddress);
    }
}

When a write is done, the performance would be a little sub-optimal, because it blocks all read operations until writing is finished. If the majority of the operations are reads, this isn't a problem however.

I've changed your dictionary to a HashSet in this example, since you seem to be only using the key, and not the value.

Thorarin
Thank you -- your implementation works well.
A: 

It's a threading issue.

The FileNotFoundException is happening on this line

  FileStream fileStream = new FileStream(System.Configuration.ConfigurationManager.AppSettings["CACHEPATH"] + filename, FileMode.Open, FileAccess.Read, FileShare.Read);  

because the file doesn't exist yet.

You're calling

dict.Add(filename, true);

before you create the file

TextWriter tw = new StreamWriter(@System.Configuration.ConfigurationManager.AppSettings["CACHEPATH"] + filename); 
tw.WriteLine(JSONstring); 
tw.Close(); 

which causes your test for file existence to be inaccurate

public bool existsFile(string signature)       
{                              
    if (dict.ContainsKey(signature))       
    {       
        return true;       
    }       
    else       
    {       
        return false;       
    }                                       
}  
ChrisNel52
That isn't the entire problem however. Changing the order of those two operations will cause a new potential problem (see my answer).
Thorarin