Reduce Technical Debt Part 2 – Empty Try Catch
source link: https://blog.coates.dk/2020/04/06/reduce-technical-debt-part-2-empty-try-catch/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Reduce Technical Debt Part 2 – Empty Try Catch
Here is a the second in the series on how to reduce Technical Debt, please read part one as it gives an insight into the scale and challenges we faced, and outlines what this blog post is trying to address.
As you are aware the first part introduced a few code examples to help remove redundant code, this blog will continue to focus on how to remove redundant code by introducing the EmptyTryCatchService class and the IgnoreEmptyTryCatch Custom attribute .
But before that I just briefly want to mention integrations, in my experience this is where a lot of redundant and or unnecessary code can hide.
Integrations
Therefore, an important concept to reduce technical debt, is to identify, separate and isolate dependencies on external systems, especially complex and or legacy systems.
I have already written a blog series about this, so if you missed please read it.
Integrations Platform
I believe in an ideal world, most integrations and especially complex and or legacy system specific code should be move out of the website solution to an integration platform!
Most issues, difficulties, problems and cost relating to code maintenance and technical debt for website is due to being responsible for stuff they should not be.
For example, the website is responsible for aggregation data from several systems to provide a unified view of their data, NO this is the job of an Integrations/aggregation platform
Empty Try Catch
So, let me start by stating – ignoring exceptions is a bad idea, because you are silently swallowing an error condition and then continuing execution.
Occasionally this may be the right thing to do, but often it’s a sign that a developer saw an exception, didn’t know what to do about it, and so used an empty catch to silence the problem.
It’s the programming equivalent of putting black tape over an engine warning light.
It’s best to handle exceptions as close as possible to the source, because the closer you are, the more context you have to achieve doing something useful with the exception.
Ignore Empty Try Catch – Custom attribute
In some rare cases the empty try catch can be valid, in which case you can use the custom attribute to mark the function and explain why it is OK, and check one last time is there not a TryParse version of the function and or code you are calling.
Performance
Slightly off topic, but still a type of technical debt, do not use exceptions for program flow!
Throwing exceptions is very expensive (must dump the registries, call stack, etc. and whilst doing this it blocks all threads) so it has a big impact on performance.
I have seen sites brought to their knees because of the number exceptions being thrown.
Redundant Code
In the solution we took over there were over 300 empty try-catch statements
But how can it hide redundant code?
When an exception is thrown it can jump over lots of code, which is therefore never called.
Therefore, all the code after the exception is redundant.
Below is the classic Hello World program it works as expected, it prints out “Hello World”.
But there is a lot of technical debt, now this might look like a funny example, but I have seen a lot of similar examples in real world, usually with a lot more code in the try catch, and usually found most often around big complex integrations!
Solution – EmptyTryCatchService
For empty try catches I would not recommend you use Sitecore’s standard logging, as it can create enormous log files which is enough to kill your sitecore solution, if the empty try catch is called a lot.
For tracking down empty try catches, it is good to have a dedicated log file and a way to limit the amount of data written to the log file.
EmptyTryCatchService class provides the following features:
- Report interval – the interval between exceptions with the same owner, name and exception message are written to the log file.
- Max Log limit – when the number exceptions with the same owner, name and exception message is exceed no more data is written to the log file.
- Dedicated log file for each day
- Disable all logging via configuration.
EmptyTryCatchService class is a simple class that, relies on the MaxUsageLog for most of its functionality (see the code below).
In addition to finding redundant code the EmptyTryCatchService will track down hidden errors and problems in your solution, which will result in a reduction of the technical debt.
You must be careful when reviewing the exceptions logged and deciding how best to deal with the exceptions. See part 3 in the series, to reduce technical debt.
public
class
EnsureIsObsoleteService
{
private
readonly MaxUsageLog _maxUsageLog =
new
MaxUsageLog(10000,
"EnsureIsObsoleteService"
,1000);
public
void
EnsureIsObsolete(object owner, string message)
{
_maxUsageLog.Log(owner, message);
}
}
public
class
MaxUsageLog
{
public
MaxUsageLog(
int
maxLogLimit,
string fileNamePrefix,
int
reportCountInterval=1000000)
{
_maxLogLimit = maxLogLimit;
_fileNamePrefix = !string.IsNullOrEmpty(fileNamePrefix) ? fileNamePrefix :
"MaxUsageLog"
;
_reportCountInterval = reportCountInterval;
}
public
void
Log(object owner, string message, Exception ex = null)
{
if
(!IsEnabled())
return
;
string type = string.Empty;
if
(owner != null)
{
if
(owner is Type typeObj)
{
type = typeObj.FullName;
}
else
{
type = owner.GetType().FullName;
}
}
string key = GenerateKey(type, message, ex);
if
(!ShouldLog(owner, key))
return
;
var count = Count(key);
WriteToFile(owner, type, message, ex, count);
}
private
int
Count(string key)
{
return
Usage.ContainsKey(key) ? Usage[key] : 0;
}
private
void
WriteToFile(object owner, string type, string message, Exception exceptionToLog,
int
count)
{
try
{
StreamWriter
log
= File.Exists(FileName) ? File.AppendText(FileName) : File.CreateText(FileName);
try
{
log
.AutoFlush =
true
;
log
.WriteLine($
"{DateTime.Now.ToUniversalTime()}: Type:'{type}' Message:'{message}' Count:{count}"
);
if
(exceptionToLog != null)
{
log
.WriteLine($
"Exception:{exceptionToLog}"
);
}
log
.Close();
}
finally
{
log
.Close();
}
}
catch
(Exception ex)
{
if
(!Sitecore.Configuration.ConfigReader.ConfigutationIsSet)
return
;
Sitecore.Diagnostics.Log.Error(
$
"Failed writing log file {FileName}. The following text may be missing from the file: Type:{type} Message:{message}"
,
ex, owner);
}
}
private
bool
ShouldLog(object owner, string key)
{
if
(!Usage.ContainsKey(key))
{
Usage.Add(key, 1);
return
true
;
}
var count = Usage[key] = Usage[key] + 1;
if
(count % _reportCountInterval == 0)
{
WriteToFile(owner,
"******** Report Count Interval ******"
, $
"Key:'{key}'"
, null,count);
}
if
(count > _maxLogLimit)
return
false
;
if
(count == _maxLogLimit)
{
WriteToFile(owner,
"******** Usage Max Exceeded ******"
, $
"Key:'{key}' Max Limit:{_maxLogLimit}"
,null,count);
return
false
;
}
return
true
;
}
private
string GenerateKey(string type, string message, Exception ex)
{
return
ex != null ?
$
"{_fileNamePrefix}_{type}_{message}_{ex.HResult}"
:
$
"{_fileNamePrefix}_{type}_{message}"
;
}
private
string FileName
{
get
{
DateTime date = DateTime.Now;
string fileName = $@
"\{_fileNamePrefix}.{date.Year}.{date.Month}.{date.Day}.log"
;
if
(!Sitecore.Configuration.ConfigReader.ConfigutationIsSet)
return
ConfigurationManager.AppSettings[Constants.Configuration.Key.LogFolderForApplications] + fileName;
return
Sitecore.MainUtil.MapPath(Sitecore.Configuration.Settings.LogFolder) + fileName;
}
}
private
bool
IsEnabled()
{
if
(!Sitecore.Configuration.ConfigReader.ConfigutationIsSet)
return
StringToBool(ConfigurationManager.AppSettings[Constants.Configuration.Key.MaxUsageLogEnabled],
false
);
return
Sitecore.Configuration.Settings.GetBoolSetting(Constants.Configuration.Key.MaxUsageLogEnabled,
true
);
}
private
bool
StringToBool(string value,
bool
defaultValue)
{
if
(value == null)
return
defaultValue;
bool
result;
if
(!
bool
.TryParse(value, out result))
return
defaultValue;
return
result;
}
private
readonly
int
_maxLogLimit;
private
readonly string _fileNamePrefix;
private
readonly
int
_reportCountInterval;
// this is to ensure we can count how many times a message has been logged across all threads
private
static
readonly Dictionary<string,
int
> Usage =
new
Dictionary<string,
int
>();
}
Hope this was of help, Alan
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK