Skip to content

Commit

Permalink
Issue #56: Concurrency control for hosted scripts needs to be system …
Browse files Browse the repository at this point in the history
…wide
  • Loading branch information
oleg-shilo committed Mar 22, 2017
1 parent f6ab9d2 commit 16b5704
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 26 deletions.
114 changes: 91 additions & 23 deletions Source/CSScriptLibrary/CSScriptLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@

namespace CSScriptLibrary
{
public enum HostingConcurrencyControl
{
/// <summary>
/// Any call to CSScript.Compile within a given process is synchronized
/// </summary>
ProcessScope,
/// <summary>
/// Any call to CSScript.Compile a specific script within a given process is synchronized
/// </summary>
ScriptProcessScope,
/// <summary>
/// Any call to CSScript.Compile a specific script is synchronized system wide
/// </summary>
ScriptSystemScope,
}

#if !net1

/// <summary>
Expand Down Expand Up @@ -321,7 +337,7 @@ public void Execute<T>(Action<T> action, T context)
/// <returns>Reference to the <see cref="System.AppDomain"/>. It is the same object, which is passed as the <paramref name="domain"/>.</returns>
public static AppDomain Execute(this AppDomain domain, Action action, params string[] probingDirs)
{
var remote = (RemoteExecutor) domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString());
var remote = (RemoteExecutor)domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString());
remote.InitProbing(probingDirs);
remote.Execute(action);
remote.UninitProbing();
Expand Down Expand Up @@ -359,7 +375,7 @@ public static AppDomain Execute<T>(this AppDomain domain, Action<T> action, T co
{
//also possible to serialize lambda and execute it in remote AppDomain (yest it is dangerous)
//look at MetaLinq\ExpressionBuilder
var remote = (RemoteExecutor) domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString());
var remote = (RemoteExecutor)domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(RemoteExecutor).ToString());
remote.InitProbing(probingDirs);
remote.Execute(action, context);
remote.UninitProbing();
Expand Down Expand Up @@ -751,17 +767,38 @@ static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
/// </summary>
public static Settings GlobalSettings = Settings.Load(Environment.ExpandEnvironmentVariables(@"%CSSCRIPT_DIR%\css_config.xml"));

#if !net1

/// <summary>
/// Collection of all compiling results. Every time the script is compiled the compiling result is added to this collection regardless of
/// the success or failure of the actual compilation.
/// </summary>
public static Dictionary<FileInfo, CompilingInfo> CompilingHistory = new Dictionary<FileInfo, CompilingInfo>();

/// <summary>
/// The hosting concurrency control.
/// <list type="bullet">
/// <item ><description>
/// <para><c>ProcessScope</c></para>
/// Mutex name is a derived from the hosting process id.
/// Two calls to compile any script will be synchronized if the calls are made from the same host process.
/// </description></item>
/// <item ><description>
/// <para><c>ScriptProcessScope</c></para>
/// Mutex name is a derived from the combination of the script path and hosting process id.
/// Two calls to compile the same script file will be synchronized but only if the calls are made from the same host process.
/// </description></item>
/// <item ><description>
/// <para><c>ScriptSystemScope</c></para>
/// Mutex name is a derived from the script path.
/// Two calls to compile the same script file will be synchronized system wide.
/// </description></item>
/// </list>
/// </summary>
static public HostingConcurrencyControl HostingConcurrencyControl = HostingConcurrencyControl.ScriptSystemScope;

/// <summary>
/// The last script compilation result.
/// The last script compilation result. Note, invoking CSScript.Compile/CSScript.Load may not trigger the actual compilation if script caching is
/// engaged. Thus the <c>LastCompilingResult</c> value can be null.
/// </summary>
public static CompilingInfo LastCompilingResult = null;

Expand All @@ -779,8 +816,6 @@ public static bool KeepCompilingHistory
set { keepCompilingHistory = value; }
}

#endif

/// <summary>
/// Invokes global (static) CSExecutor (C# script engine)
/// </summary>
Expand Down Expand Up @@ -1032,12 +1067,7 @@ static string ResolveConfigFilePath(string cssConfigFile)
return cssConfigFile != null ? cssConfigFile : Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "css_config.xml");
}

static string GetCompilerLockName(string script, Settings scriptSettings)
{
return GetCompilerLockName(script, scriptSettings.OptimisticConcurrencyModel);
}

static string GetCompilerLockName(string script, bool optimisticConcurrencyModel)
static string GetCompilerLockName_old(string script, bool optimisticConcurrencyModel)
{
if (optimisticConcurrencyModel)
{
Expand All @@ -1049,6 +1079,33 @@ static string GetCompilerLockName(string script, bool optimisticConcurrencyModel
}
}

static string GetCompilerLockName(string script)
{
var scriptPath = Path.GetFullPath(script);

if (!Utils.IsLinux())
scriptPath = scriptPath.ToLower();

switch (HostingConcurrencyControl)
{
// Mutex name is a derived from the hosting process id.
// Two calls to compile any script will be synchronized if the calls are made from the same host process.
case HostingConcurrencyControl.ProcessScope:
return "cs-script." + Process.GetCurrentProcess().Id;

// Mutex name is a derived from the combination of the script path and hosting process id.
// Two calls to compile the same script file will be synchronized but only if the calls are made from the same host process.
case HostingConcurrencyControl.ScriptProcessScope:
return string.Format("cs-script.{0}.{1}", Process.GetCurrentProcess().Id, CSSUtils.GetHashCodeEx(scriptPath));

// Mutex name is a derived from the script path.
// Two calls to compile the same script file will be synchronized system wide.
case HostingConcurrencyControl.ScriptSystemScope:
default:
return "cs-script." + CSSUtils.GetHashCodeEx(scriptPath);
}
}

/// <summary>
/// Creates the compiler lock object (<see cref="System.Threading.Mutex"/>). The Mutex object is now initially owned.
/// <para>This object is to be used for the access synchronization to the compiled script file and it can be useful for the
Expand All @@ -1058,17 +1115,28 @@ static string GetCompilerLockName(string script, bool optimisticConcurrencyModel
/// </summary>
/// <param name="compiledScriptFile">The script file.</param>
/// <param name="optimisticConcurrencyModel">if set to <c>true</c> the operation is thread-safe within the current process.
/// Otherwise the operation is thread-safe system wide..</param>
/// Otherwise the operation is thread-safe system wide.</param>
/// <returns></returns>
#if !net4
[Obsolete("This method is no longer supported use CreateCompilerLock(string compiledScriptFile) instead, which offers more comprehensive concurrency control.")]
static public Mutex CreateCompilerLock(string compiledScriptFile, bool optimisticConcurrencyModel)
#else
static public Mutex CreateCompilerLock(string compiledScriptFile, bool optimisticConcurrencyModel = false)
#endif
{
return new Mutex(false, GetCompilerLockName(compiledScriptFile, optimisticConcurrencyModel));
return new Mutex(false, GetCompilerLockName(compiledScriptFile));
}

/// <summary>
/// Creates the compiler lock object (<see cref="System.Threading.Mutex"/>). The Mutex object is now initially owned.
/// <para>This object is to be used for the access synchronization to the compiled script file and it can be useful for the
/// tasks like cache purging or explicit script recompilation.</para>
/// <para>The concurrency/lock scope is controlled by <see cref="CSScriptLibrary.CSScript.Ho"/>.
/// And it is to be used to control the concurrency scope.</para>
/// </summary>
/// <param name="compiledScriptFile">The script file.</param>
/// Otherwise the operation is thread-safe system wide.</param>
/// <returns></returns>
static public Mutex CreateCompilerLock(string compiledScriptFile)
{
return new Mutex(false, GetCompilerLockName(compiledScriptFile));
}
/// <summary>
/// Compiles script file into assembly with CSExecutor. Uses script engine settings object and compiler specific options.
/// </summary>
Expand All @@ -1083,7 +1151,7 @@ static public string CompileWithConfig(string scriptFile, string assemblyFile, b
{
lock (typeof(CSScript))
{
using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile, scriptSettings)))
using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile??scriptFile)))
{
ExecuteOptions oldOptions = CSExecutor.options;
try
Expand Down Expand Up @@ -1202,11 +1270,11 @@ static ExecuteOptions InitExecuteOptions(ExecuteOptions options, Settings script

if (scriptFile != "")
{
scriptFile = FileParser.ResolveFile(scriptFile, (string[]) dirs.ToArray(typeof(string))); //to handle the case when the script file is specified by file name only
scriptFile = FileParser.ResolveFile(scriptFile, (string[])dirs.ToArray(typeof(string))); //to handle the case when the script file is specified by file name only
dirs.Add(Path.GetDirectoryName(scriptFile));
}

options.searchDirs = RemovePathDuplicates((string[]) dirs.ToArray(typeof(string)));
options.searchDirs = RemovePathDuplicates((string[])dirs.ToArray(typeof(string)));

options.scriptFileName = scriptFile;

Expand Down Expand Up @@ -1569,7 +1637,7 @@ static public object Eval(params object[] args)
if (lastArg == null || !(lastArg is string))
throw new Exception("You did not specify the code to 'Eval'");

string methodCode = ((string) lastArg).Trim();
string methodCode = ((string)lastArg).Trim();

string methodName = methodCode.Split(new char[] { '(', ' ' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();

Expand Down Expand Up @@ -2001,7 +2069,7 @@ static public Assembly LoadWithConfig(string scriptFile, string assemblyFile, bo
{
lock (typeof(CSScript))
{
using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile, CSScript.GlobalSettings)))
using (Mutex fileLock = new Mutex(false, GetCompilerLockName(assemblyFile??scriptFile)))
{
ExecuteOptions oldOptions = CSExecutor.options;

Expand Down
3 changes: 2 additions & 1 deletion Source/CSScriptLibrary/Evaluator.Extensions.Remote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ public interface IRemoteAgent
public static class AttachedProperies
{
#pragma warning disable 1591 // Missing XML comment for publicly visible type or member
public static ConditionalWeakTable<object, Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object, Dictionary<string, object>>();
#pragma warning restore 1591 // Missing XML comment for publicly visible type or member

public static ConditionalWeakTable<object, Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object, Dictionary<string, object>>();

/// <summary>
/// Sets the named value to the object.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions Source/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,14 @@ public bool OptimisticConcurrencyModel
}
bool optimisticConcurrencyModel = true;


[Browsable(false)]
public bool ConcurrencyModel
{
get { return optimisticConcurrencyModel; }
set { optimisticConcurrencyModel = value; }
}

/// <summary>
/// Gets or sets a value indicating whether auto-class decoration should allow C# 6 specific syntax.
/// If it does the statement "using static dbg;" will be injected at the start of the auto-class definition thus the
Expand Down
2 changes: 1 addition & 1 deletion Source/Tests/Eval.Remote.Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void CreateDelegateRemotelyRoslyn()
// MSTest (or xUnit) loads the test assembly from its build directory but places and loads the
// dependencies into individual temporary folders (one asm per folder). What a "brilliant" idea!!!
//
// MSText does explicit asm resolving, what helps for MonoEval. However Roslyn resolving
// MSTest does explicit asm resolving, what helps for MonoEval. However Roslyn resolving
// has to be based on name-only algorithm (a version checking) as it has wired dependencies on
// portable asms. Thus we need to set up special asm probing (extra argument) for Roslyn case.

Expand Down
4 changes: 3 additions & 1 deletion Source/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,9 @@ public static bool IsRuntimeCompatibleAsm(string file)

public static bool IsLinux()
{
return (Environment.OSVersion.Platform == PlatformID.Unix);
// Note it is not about OS being exactly Linux but rather about OS having Linux type of file system.
// For example path being case sensitive
return (Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX);
}

public static bool ContainsPath(string path, string subPath)
Expand Down

0 comments on commit 16b5704

Please sign in to comment.