Sitecore 9 CD Servers May Assume “Master” EventQueue by default

Here’s a quick one, and I wish it was a clever April Fools’ joke but it isn’t.

Sitecore support recently confirmed a bug for me in Sitecore 9.0 update-2 (may be present for other versions in the Sitecore 9 space — I’m unsure). A Sitecore CD environment might report exceptions assuming a “master” database endpoint like this:

Unknown connection string. Name: 'master'

For the stacktraces I’ve seen with this issue, it’s something like the following:

ERROR One or more exceptions occurred while processing the 
subscribers to the 'publish:end:remote' event.

In the days of Sitecore 8 (I guess those are the olden times now?), we’d adjust our SwitchMasterToWeb.config to address the EventQueue configuration that assumes the presence of a “master” database. For what it’s worth, I always thought Kam Figy’s was the most thorough at https://gist.github.com/kamsar/8096336f141c0e5e97b3.

In the case of this Sitecore 9 issue, we could brew up our own SwitchMasterToWeb.config patch file or work around the issue using role:require logic on the <eventQueue> node in Sitecore.config file. I thought the Sitecore 9 role:define features were designed to make the SwitchMasterToWeb.config obsolete, but if we don’t want to alter Sitecore’s default sitecore.config file, we may need a SwitchMasterToWebForSitecore9.config. History is cyclical!

Here’s the fragment of sitecore.config I’m referring to:

<eventQueueProvider defaultEventQueue=”core”>

<eventQueue role:require=”ContentManagement or Standalone” name=”master” type=”Sitecore.Data.Eventing.$(database)EventQueue, Sitecore.Kernel”>
<param ref=”dataApis/dataApi[@name=’$(database)’]” param1=”$(name)” />
<param hint=”” ref=”PropertyStoreProvider/store[@name=’$(name)’]” />
</eventQueue>

New Sitecore 8.2 & Sitecore 9 Security Patch

I guess I’m on a security hardening binge for 2019, since I’ll share a hot-off-the-presses security hardening measure from Sitecore today. I don’t want to say too much about the vulnerability, but this article explains in general terms and applying it to all Sitecore server roles for version 8.2 through any current 9 releases is emphasized as the best practice. I’d do it at the next opportunity.

I created a gist with the PowerShell necessary to apply this patch, just update line #8 with the path to your Sitecore website:


Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
Import-Module WebAdministration
$url = "https://kb.sitecore.net/~/media/7A638A36A71D4494981A8655E297AD23.ashx?la=en&quot;
$tempLocation = "C:\tempLocation"
$zippedPatch = "$tempLocation\SitecoreSupportPackage.302938.zip"
$unzippedPatch = "$tempLocation\SitecoreSupportPackage"
$sitecoreRoot = "C:\InetPub\Your\Sitecore\Website"
if (!(Test-Path $tempLocation))
{
New-Item -ItemType Directory -Path $tempLocation
}
Invoke-WebRequest -Uri $url -OutFile $zippedPatch
Expand-Archive -Path $zippedPatch -DestinationPath $unzippedPatch -Force
Copy-Item "$unzippedPatch/website/*" -Destination $sitecoreRoot -Recurse -Force
Write-Host "Patch applied to $sitecoreRoot"
Remove-Item $unzippedPatch -Recurse
Remove-Item $zippedPatch -Recurse

Our team has internal automation taking the above a bit further and using another layer of abstraction, and that’s secret Rackspace sauce I won’t share publicly,  but the snippet above should have your environment patched in just a few seconds.

One of the key elements to the patch involves an update to the /sitecore/admin/logs.aspx page which, if you dig into it, reveals a grip-load of additional C# validation logic and other stuff going on . . .

secpatch

There’s a lot to unpack in there if you’re curious, but suffice it to say that Sitecore’s keeping all your bases covered and isn’t trusting user input (using a broad interpretation of that principle).

How I Add Custom Sitecore Publishing Service Targets

At this point, I think I’ve installed, configured, or customized the new Sitecore Publishing Service at least a dozen times for various projects. Sometimes it’s on PaaS, sometimes on IaaS . . . I’ve used a variety of different versions depending on the compatibility matrix (see below as of Dec 16, 2018):

PubSvcVisual

I’m going to skip all the preamble about how the new Sitecore Publishing Service works, about .Net core being the new hotness, why this component can be a great addition to many distributed Sitecore implementations, etc — smart people have written a lot about this already. For example, check out Stephen Pope’s no-holds-barred look at the Publishing Service at http://www.stephenpope.co.uk/publishing or Jonathan Robbins has a nice overview piece at https://jonathanrobbins.co.uk/2016/09/02/setting-up-sitecore-publishing-service/.

I’ve learned a good bit from all the iterations of working with the component and I think consistently the most error-prone part of the setup is aligning any additional custom Sitecore publishing targets one is using in an implementation. This write-up from Geykel Moreno at AlphaSolutions has all the good information, but it’s not as easy to follow because it doesn’t post a comprehensive sc.publishing.xml file — it took a bit of trial and error for me, so to simplify for posterity I’m going to share a reference sample Gist at https://gist.github.com/grant-killian/d2fe8d3e89c5d7b15f47464dd1809d62 that includes 2 additional custom publishing targets. I’ve inserted XML comments for the 3 locations one must update in the config\sitecore\publishing\sc.publishing.xml file:

  1. You need to add your ConnectionString entry for each database to the Publishing/ConnectionStrings XML
  2. You need to add your Services/DefaultConnectionFactory/Options/Connections XML definition for each custom target
  3. You need to add entries for each target to the StoreFactory/Options/Stores/Targets XML that will include the GUID of the Sitecore item that defines each publishing target, along with the Name of the item and additional details

Here’s the gist with the full XML for reference:


<?xml version="1.0" encoding="UTF-8"?>
<Settings>
<Sitecore>
<Publishing>
<InstanceName>${SITECORE_InstanceName}</InstanceName>
<ConnectionStrings>
<Service>${Sitecore:Publishing:ConnectionStrings:Master}</Service>
<!– Add any additional publishing targets you may use (first location for changes to this file) –>
<previewweb>Data Source=Server-012345;Initial Catalog=PrevWeb;Integrated Security=True;MultipleActiveResultSets=True;ConnectRetryCount=15;ConnectRetryInterval=1</previewweb>
<liveweb>Data Source=Server-012345;Initial Catalog=LiveWeb;Integrated Security=True;MultipleActiveResultSets=True;ConnectRetryCount=15;ConnectRetryInterval=1</liveweb>
<!– end first location for changes –>
</ConnectionStrings>
<Services>
<DefaultConnectionFactory>
<Options>
<Connections>
<Links>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.SqlDatabaseConnection, Sitecore.Framework.Publishing.Data</Type>
<LifeTime>Transient</LifeTime>
<Options>
<ConnectionString>${Sitecore:Publishing:ConnectionStrings:Core}</ConnectionString>
<DefaultCommandTimeout>120</DefaultCommandTimeout>
<Behaviours>
<backend>sql-backend-default</backend>
<api>sql-api-default</api>
</Behaviours>
</Options>
</Links>
<Service>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.SqlDatabaseConnection, Sitecore.Framework.Publishing.Data</Type>
<LifeTime>Transient</LifeTime>
<Options>
<ConnectionString>${Sitecore:Publishing:ConnectionStrings:Service}</ConnectionString>
<DefaultCommandTimeout>120</DefaultCommandTimeout>
<Behaviours>
<backend>sql-backend-default</backend>
<api>sql-api-default</api>
</Behaviours>
</Options>
</Service>
<Master>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.SqlDatabaseConnection, Sitecore.Framework.Publishing.Data</Type>
<LifeTime>Transient</LifeTime>
<Options>
<ConnectionString>${Sitecore:Publishing:ConnectionStrings:Master}</ConnectionString>
<DefaultCommandTimeout>120</DefaultCommandTimeout>
<Behaviours>
<backend>sql-backend-default</backend>
<api>sql-api-default</api>
</Behaviours>
</Options>
</Master>
<Internet>
<!– Should match the name of the publishing target configured in SC. –>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.SqlDatabaseConnection, Sitecore.Framework.Publishing.Data</Type>
<LifeTime>Transient</LifeTime>
<Options>
<ConnectionString>${Sitecore:Publishing:ConnectionStrings:Web}</ConnectionString>
<DefaultCommandTimeout>120</DefaultCommandTimeout>
<Behaviours>
<backend>sql-backend-default</backend>
<api>sql-api-default</api>
</Behaviours>
</Options>
</Internet>
<!– start custom publishing target additions (2nd location) –>
<Preview>
<!– Should match the name of the publishing target configured in Sitecore –>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.SqlDatabaseConnection, Sitecore.Framework.Publishing.Data</Type>
<LifeTime>Transient</LifeTime>
<Options>
<ConnectionString>${Sitecore:Publishing:ConnectionStrings:previewweb}</ConnectionString>
<DefaultCommandTimeout>120</DefaultCommandTimeout>
<Behaviours>
<backend>sql-backend-default</backend>
<api>sql-api-default</api>
</Behaviours>
</Options>
</Preview>
<Live>
<!– Should match the name of the publishing target configured in Sitecore –>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.SqlDatabaseConnection, Sitecore.Framework.Publishing.Data</Type>
<LifeTime>Transient</LifeTime>
<Options>
<ConnectionString>${Sitecore:Publishing:ConnectionStrings:liveweb}</ConnectionString>
<DefaultCommandTimeout>120</DefaultCommandTimeout>
<Behaviours>
<backend>sql-backend-default</backend>
<api>sql-api-default</api>
</Behaviours>
</Options>
</Live>
<!– end custom publishing target additions (2nd location) –>
</Connections>
</Options>
</DefaultConnectionFactory>
<DbConnectionBehaviours>
<Options>
<Entries>
<sql-backend-default>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.NoRetryConnectionBehaviour, Sitecore.Framework.Publishing.Data</Type>
<Options>
<Name>Default Backend No Retry behaviour</Name>
<CommandTimeout>120</CommandTimeout>
</Options>
</sql-backend-default>
<sql-api-default>
<Type>Sitecore.Framework.Publishing.Data.AdoNet.NoRetryConnectionBehaviour, Sitecore.Framework.Publishing.Data</Type>
<Options>
<Name>Default Api No Retry behaviour</Name>
<CommandTimeout>10</CommandTimeout>
</Options>
</sql-api-default>
</Entries>
</Options>
</DbConnectionBehaviours>
<StoreFactory>
<Options>
<Stores>
<Service>
<Type>Sitecore.Framework.Publishing.Data.ServiceStore, Sitecore.Framework.Publishing.Data</Type>
<ConnectionName>Service</ConnectionName>
<FeaturesListName>ServiceStoreFeatures</FeaturesListName>
</Service>
<Sources>
<Master>
<Type>Sitecore.Framework.Publishing.Data.SourceStore, Sitecore.Framework.Publishing.Data</Type>
<ConnectionNames>
<master>Master</master>
</ConnectionNames>
<FeaturesListName>SourceStoreFeatures</FeaturesListName>
<!– The name of the Database entity in Sitecore. –>
<ScDatabase>master</ScDatabase>
</Master>
</Sources>
<Targets>
<!–Additional targets can be configured here–>
<Internet>
<Type>Sitecore.Framework.Publishing.Data.TargetStore, Sitecore.Framework.Publishing.Data</Type>
<ConnectionName>Internet</ConnectionName>
<FeaturesListName>TargetStoreFeatures</FeaturesListName>
<!– The id of the target item definition in Sitecore. –>
<Id>8E080626-DDC3-4EF4-A1D1-F0BE4A200254</Id>
<!– The name of the Database entity in Sitecore. –>
<ScDatabase>web</ScDatabase>
</Internet>
<!– start custom publishing target additions (third location) –>
<!– this XML node should be named the same as the item in Sitecore (not the "Display Name", but the Item name) –>
<Preview>
<Type>Sitecore.Framework.Publishing.Data.TargetStore, Sitecore.Framework.Publishing.Data</Type>
<ConnectionName>Preview</ConnectionName>
<FeaturesListName>TargetStoreFeatures</FeaturesListName>
<!– make sure the GUID below matches the GUID stored in Sitecore for the Publishing Target –>
<Id>8D1249E6-9413-4C2D-8C72-06561CE1D026</Id>
<ScDatabase>preveiwweb</ScDatabase>
</Preview>
<!– this XML node should be named the same as the item in Sitecore (not the "Display Name", but the Item name) –>
<Live>
<Type>Sitecore.Framework.Publishing.Data.TargetStore, Sitecore.Framework.Publishing.Data</Type>
<ConnectionName>Live</ConnectionName>
<FeaturesListName>TargetStoreFeatures</FeaturesListName>
<!– make sure the GUID below matches the GUID stored in Sitecore for the Publishing Target –>
<Id>0EA57D57-7837-4B51-A72C-E8B3F1322C07</Id>
<ScDatabase>liveweb</ScDatabase>
</Live>
<!– end custom publishing target additions (third location) –>
</Targets>
<ItemsRelationship>
<Type>Sitecore.Framework.Publishing.Data.ItemsRelationshipStore, Sitecore.Framework.Publishing.Data</Type>
<ConnectionName>Links</ConnectionName>
<FeaturesListName>ItemsRelationshipStoreFeatures</FeaturesListName>
</ItemsRelationship>
</Stores>
</Options>
</StoreFactory>
<StoreFeaturesLists>
<Options>
<FeatureLists>
<!–Source Store Features–>
<SourceStoreFeatures>
<ItemReadRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.CompositeItemReadRepository, Sitecore.Framework.Publishing.Data</Type>
</ItemReadRepositoryFeature>
<TestableContentRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.CompositeTestableContentRepository, Sitecore.Framework.Publishing.Data</Type>
</TestableContentRepositoryFeature>
<WorkflowStateRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.CompositeWorkflowStateRepository, Sitecore.Framework.Publishing.Data</Type>
</WorkflowStateRepositoryFeature>
<EventQueueRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.CompositeEventQueueRepository, Sitecore.Framework.Publishing.Data</Type>
<options>
<ConnectionName>master</ConnectionName>
</options>
</EventQueueRepositoryFeature>
<SourceIndexFeature>
<Type>Sitecore.Framework.Publishing.ItemIndex.SourceIndexWrapper, Sitecore.Framework.Publishing</Type>
</SourceIndexFeature>
</SourceStoreFeatures>
<!–Service Store Features–>
<ServiceStoreFeatures>
<ManifestRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Manifest.ManifestRepository, Sitecore.Framework.Publishing</Type>
</ManifestRepositoryFeature>
<PublisherOperationRepositoryFeature>
<Type>Sitecore.Framework.Publishing.PublisherOperations.PublisherOperationRepository, Sitecore.Framework.Publishing</Type>
</PublisherOperationRepositoryFeature>
<PublishJobQueueRepositoryFeature>
<Type>Sitecore.Framework.Publishing.PublishJobQueue.PublishJobQueueRepository, Sitecore.Framework.Publishing</Type>
</PublishJobQueueRepositoryFeature>
<TargetSyncStateRepositoryFeature>
<Type>Sitecore.Framework.Publishing.TargetSyncState.TargetSyncStateRepository, Sitecore.Framework.Publishing</Type>
</TargetSyncStateRepositoryFeature>
<ActivationLockRepositoryFeature>
<Type>Sitecore.Framework.Publishing.InstanceActivation.ActivationLockRepository, Sitecore.Framework.Publishing</Type>
</ActivationLockRepositoryFeature>
</ServiceStoreFeatures>
<!–Target Store Features–>
<TargetStoreFeatures>
<IndexableItemRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.Classic.ClassicIndexableItemRepository, Sitecore.Framework.Publishing.Data.Classic</Type>
</IndexableItemRepositoryFeature>
<ItemWriteRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.Classic.ClassicItemRepository, Sitecore.Framework.Publishing.Data.Classic</Type>
</ItemWriteRepositoryFeature>
<MediaRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.Classic.Repositories.ClassicMediaRepository, Sitecore.Framework.Publishing.Data.Classic</Type>
</MediaRepositoryFeature>
<TargetIndexFeature>
<Type>Sitecore.Framework.Publishing.ItemIndex.TargetIndexWrapper, Sitecore.Framework.Publishing</Type>
</TargetIndexFeature>
</TargetStoreFeatures>
<!–ItemsRelationship Store Features–>
<ItemsRelationshipStoreFeatures>
<DatabaseItemRelationshipRepositoryFeature>
<Type>Sitecore.Framework.Publishing.Data.Classic.ClassicItemRelationshipRepository, Sitecore.Framework.Publishing.Data.Classic</Type>
</DatabaseItemRelationshipRepositoryFeature>
</ItemsRelationshipStoreFeatures>
</FeatureLists>
</Options>
</StoreFeaturesLists>
</Services>
</Publishing>
</Sitecore>
</Settings>

https://gist.github.com/grant-killian/d2fe8d3e89c5d7b15f47464dd1809d62.js

Sitecore artifact table patch config

I’ve patched EventQueues several times through the years, so I saved this Gist to make it easier for future opportunities.  There’s not much new to share in terms of introducing why one does this, refer to this blog about Sitecore artifact tables (or the old reliable Sitecore CMS Tuning Guide). You also have to take care around the order of configuration file processing, so zzzzzArtifactTableRetention.config this sucker if you really must 🙂

Here’s the Gist – https://gist.github.com/grant-killian/ffa1e84770b10a90e2454e241986b911  and here’s the expanded XML:


<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/&quot; xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform"&gt;
<sitecore>
<scheduling>
<agent type="Sitecore.Tasks.CleanupEventQueue, Sitecore.Kernel">
<patch:delete />
</agent>
<agent type="Sitecore.Tasks.CleanupEventQueue, Sitecore.Kernel" method="Run" interval="01:00:00">
<IntervalToKeep>06:00:00</IntervalToKeep>
</agent>
<agent type="Sitecore.Tasks.CleanupPublishQueue, Sitecore.Kernel">
<patch:delete />
</agent>
<agent type="Sitecore.Tasks.CleanupPublishQueue, Sitecore.Kernel" method="Run" interval="04:00:00">
<DaysToKeep>7</DaysToKeep>
</agent>
</scheduling>
<databases>
<database id="master">
<Engines.HistoryEngine.Storage>
<patch:delete />
</Engines.HistoryEngine.Storage>
<Engines.HistoryEngine.Storage>
<obj type="Sitecore.Data.SqlServer.SqlServerHistoryStorage, Sitecore.Kernel">
<param connectionStringName="$(id)" />
<EntryLifeTime>7.00:00:00</EntryLifeTime>
</obj>
</Engines.HistoryEngine.Storage>
</database>
<database id="web">
<Engines.HistoryEngine.Storage>
<patch:delete />
</Engines.HistoryEngine.Storage>
<Engines.HistoryEngine.Storage>
<obj type="Sitecore.Data.SqlServer.SqlServerHistoryStorage, Sitecore.Kernel">
<param connectionStringName="$(id)" />
<EntryLifeTime>7.00:00:00</EntryLifeTime>
</obj>
</Engines.HistoryEngine.Storage>
</database>
</databases>
</sitecore>
</configuration>

The Game Is Afoot . . . Solr Shenanigans for Sitecore (Part 1)

I took the challenge of presenting at the Manchester, New Hampshire Sitecore User Group a few days ahead of the 2018 Sitecore Symposium. I say challenge because

  1. Delivering content that isn’t superseded by the Sitecore Symposium agenda can be difficult (all the good Commerce or Azure or DevOps material would be saved for Symposium week)
  2. I would be following Michael West and his showcase of the new Sitecore PowerShell Extensions version 5.0 module (curious that https://doc.sitecorepowershell.com/releases doesn’t list the 5.0 yet, but I’m sure it’s coming there too).

As I finalized my topic, I surveyed the work I’ve been up to recently and figured I could take my talk in one of two directions that would be generally absent from the Sitecore Symposium agenda: Sitecore and Solr, or The Heresy of Sitecore on AWS. While we are doing some interesting things around AWS with RDS, ElastiCache, that’s officially unsupported territory with Sitecore and not fully baked enough for me to present it as anything approaching a best practice — but check back with me in 6 months. So, I elected to give a talk on Solr and explore some of the lessons learned from years in the trenches making Sitecore successful with Solr; the topic was finalized as Solr, Sitecore 9, 7 Shenanigans:

shenanigans
The title slide from the talk

I covered some of the history and underpinnings of Solr with regards to Sitecore, the dependence on Solr.Net (which is <important>NOT</important> a port of Solr to .Net the way Lucene.Net IS a port of Java’s Lucene — and why we should care), and common architecture patterns for Sitecore integrations based on Solr master/slave and Solr Cloud. I guess I’ve blogged a lot about Solr over the years; for instance, here and here are a couple sample areas I delved into.

I think the most fun part of the talk was the Shenanigans, however, as I went with a Sherlock Holmes theme to frame the conversation. I reviewed 7 cases and we had fun digging into some of the diagnostic bits.

shenanigans2

Here’s a quick run down of the first 3 Shenanigans:

  1. The case of the disappearing Java
    • Where we started with Solr that wouldn’t start for a set of Production servers  . . .

shen1.JPG

    • . . . and eventually solved the case by determining the development team installed a Java SDK with auto-update enabled and the system had removed the Java identified in the ClassPath. This is a brutal one for a Production implementation!
  1. The case of the underachieving mega-server
    • These OutOfMemoryErrors are not fun and can be due to a variety of issues:
      • shen2.JPG
    • In this case, we determined this 32 GB server was running Solr with the default 512 MB of memory set aside for Solr . . . a pretty fundamental issue:3.512 MB - default of 32 GB
    • We tuned the Solr start settings to use more of the server capacity for Java and Solr, in this case I think we used 10 GB as a starting point, and solved this specific case. This isn’t the last we’ll hear about OutOfMemory errors in our Shenanigans, however, as there can be many causes (see this great summary published just yesterday, for example).
  2. The case of the disappearing content
    • This scenario had a public-facing website’s content disappear periodically after content publishes . . . sound familiar to anyone? It was due to the threshold for full rebuilding of the search indexes after a Sitecore publish set low enough to trigger regularly
      • For the record, the setting is ContentSearch.FullRebuildItemCountThreshold and the default is 100,000 (0x186a0 – 100,000):Shen3

    • This customer didn’t have SwitchOnRebuild implemented for these key public-facing search indexes, so the first step of the index rebuild logic was to remove all documents from the Solr collection, then add the documents back in as the indexing process ran it’s course.  To the site visitor it created missing or inconsistent search results while the rebuilding took place, and for a large set of items it can take 60 minutes or longer for rebuilding.
    • The solution is to use the SwitchOnRebuild implementation for their Sitecore search indexes – https://doc.sitecore.net/sitecore_experience_platform/setting_up_and_maintaining/search_and_indexing/indexing/switch_solr_indexes and related documentation from Sitecore covers this process.

I will cover the remaining four Solr + Sitecore Shenanigans in my next post; here’s a teaser for the topics:

  • Case #4 – The case of the default query crippler
  • Case #5 – The case of the bandwidth blowout
  • Case #6 – The case of the misguided, well-intentioned, administrator
  • Case #7 – The case of the AppPool recycle-fest

As Sherlock Holmes would say: “The game is afoot!”

Sitecore and SearchMaxResults for Solr

I’ve consulted with a number of Sitecore implementations in the last month or two that had a variety of challenges with Sitecore integration with Solr. Solr is a powerful distributed system with a variety of angles of interest to Sitecore. I wanted to take this chance to comment on a Sitecore setting that can have a significant impact on how Sitecore search functions, but is easily overlooked. The setting is defined in Sitecore.ContentSearch.config and it’s called ContentSearch.SearchMaxResults. The XML comment for this setting is straight-forward, here’s how it’s presented in that file:

snip

There’s a lot to digest in that xml comment above. One could read it and assume “this can be set but it is best kept as the default” means this shouldn’t be altered, but in my experience that can be problematic.

The .Net int.MaxValue constant is 2,147,483,647. If you leave this setting at the default (so “”), one is telling Solr to return up to 2,147,483,647 results in a single response to a query, which we’ve observed in some cases to cause significant performance problems (Solr will fetch the large volume of records from disk and write them to the Solr response causing IO pressure etc.) It’s not always the case since it really comes down to the number of documents one is querying from Solr, but this sets up the potential for a virtually unbounded Solr query.

It’s interesting to trace this setting through Sitecore and into Solr, and it sheds light on how these two complex systems work together. Fun, right!? I cooked up the diagram below that shows an overview of how Sitecore and Solr work together in completing search queries:

snipp

Each application has it’s own logging which will help trace activity between the systems.

The Sitecore ContentSearch Provider for Solr relies on Solr.Net for connectivity to Solr. It’s common for .Net libraries to copy their open source equivalents from the Java world (like Log4J has a .Net port for logging named Log4net, Lucene has a .Net port for search called Lucene.Net, etc). Solr.Net, however, is not a port of the Solr Java application to .Net. Instead, Solr.Net is a wrapper for the main Solr API elements that can be easily called by .Net applications. When it comes to Sitecore’s ContentSearch Provider for Solr, Solr.Net is Sitecore’s bridge for getting data to and from the Solr application.

Just an aside: some projects do creative things with Solr search and Sitecore, and for certain scenarios it’s necessary to bypass Solr.Net and use the REST API directly from Sitecore. This write-up focuses on a conventional Sitecore -> Solr.Net -> Solr pattern, but I wanted to acknowledge that it’s not the only pattern.

Tracking ContentSearch.SearchMaxResults in Sitecore

On the Sitecore side, one can see the ContentSearch.SearchMaxResults setting in the Sitecore logs when you turn up the diagnostics to get more granular data; this isn’t a configuration that’s recommended for using beyond a discrete troubleshooting session as the amount of data it can generate can be significant . . . but here’s how you dial up the diagnostic data Sitecore reports about queries:

snip3

If you run a few queries that exercise your Sitecore implementation code that queries Solr, you can review the contents of the Search log in the Sitecore /data directory and find entries such as:

INFO Solr Query – ?q=associated_docs:(“\*A5C71A21-47B5-156E-FBD1-B0E5EFED4D33\*”)&rows=2147483647&fq=_indexname:(domain_index_web)

or

INFO  Solr Query – ?q=((_fullpath:(\/sitecore/content/Branches/ABC/*) AND _templates:(d0351826b1cd4f57ac05816471ba3ebc)))&rows=2147483647&fq=_indexname:(domain_index_web)

The .Net int.MaxValue 2147483647 is what Sitecore, through Solr.Net, is specifying as the number of rows to return from this query. For Solr cores with only a few hundred results matching this query, it’s not that big a deal because the query has a fairly small universe to process and retrieve. If you have 100,000 documents, however, that’s a very heavy query for Solr to respond to and it will probably impact the performance of your Sitecore implementation.

Tracking ContentSearch.SearchMaxResults in Solr

Solr has it’s own logging systems and this 2147483647 value can be seen in these logs once Solr has completed the API call. In a default Solr installation, the logs will be located at server/logs (check your log4j.properties file if you don’t know for sure where your logs are being stored) and you can a open up the log and scan for our ContentSearch.SearchMaxResults setting value. You’ll see entries such as:

INFO  – 2018-03-26 21:20:19.624; org.apache.solr.core.SolrCore; [domain_index_web] webapp=/solr path=/select params={q=(_template:(a6f3979d03df4441904309e4d281c11b)+AND+_path:(1f6ce22fa51943f5b6c20be96502e6a7))&fl=*,score&fq=_indexname:(domain_index_web)&rows=2147483647&version=2.2} hits=2681 status=0 QTime=88

  • The above Solr query returned 2,681 results (hits) and the QTime (time elapsed between the arrival of the query request to Solr and the completion of the request handler) was 88 milliseconds. This is probably no big deal as it relates to the ContentSearch.SearchMaxResults, but you don’t know if this data will increase over time…

INFO  – 2018-03-26 21:20:19.703; org.apache.solr.core.SolrCore; [domain_index_web] webapp=/solr path=/select params={q=((((include_nav_xml_b:(True)+AND+_path:(00ada316e3e4498797916f411bc283cf)))+AND+url_s:[*+TO+*])+AND+(_language:(no-NO)+OR+_language:(en)))&fl=*,score&fq=_indexname:( domain_index_web)&rows=2147483647&version=2.2} hits=9 status=0 QTime=16

  • The above Solr query returned 9 results (hits) and the QTime was 16 milliseconds. This is unlikely a problem when it comes to ContentSearch.SearchMaxResults.

 INFO  – 2018-03-26 21:20:19.812; org.apache.solr.core.SolrCore; [domain_index_web] webapp=/solr path=/select params={q=(_template:(8ed95d51A5f64ae1927622f3209a661f)+AND+regionids_sm:(33ada316e3e4498799916f411bc283cf))&fl=*,score&fq=_indexname:(domain_index_web)&rows=2147483647&version=2.2} hits=89372 status=0 QTime=1600

  • The above Solr query returned 89,372 results (hits) and the QTime was 1600 milliseconds. Look out. This is the type of query that could easily cause problems based on the Sitecore ContentSearch.SearchMaxResults setting as the volume of data Solr is working with is getting large. That query time is already climbing high and that’s a lot of results for Sitecore to require in a single request.

The impact of retrieving so many documents from Solr can cause a cascade of difficulties besides just the handling of the query. Solr caches the query results in memory and if you request 1,000,000 documents you could also be caching 1,000,000 million documents. Too much of this activity and it can stress Solr to the breaking point.

Best Practices

There is no magic value to set for ContentSearch.SearchMaxResults other than not “”. General best practice when retrieving lots of data from most any system is to use paging. It’s recommended to do that for Sitecore ContentSearch queries, too. A general recommendation would be to set a specific value for the ContentSearch.SearchMaxResults setting, such as “500” or “1000”. This should be thoroughly tested, however, as limiting the search results for an implementation that isn’t properly using search result paging can lead to inconsistent behavior across the site. Areas such as site maps, general site search, and other areas with implementation logic that could assume all the search results are available in a single request deserve special attention when tuning this setting.

What About Noisy Solr Neighbors?

I’ve worked on some implementations where Solr was a resource shared between a variety of Sitecore implementations. One project, in this example, might set ContentSearch.SearchMaxResults to “2000” for their Sitecore sites while another project sets a value of “500” – but what if there’s a third organization making use of the same Solr resources and that project doesn’t explicitly set a value for ContentSearch.SearchMaxResults? That one project leaves the setting at the “” default, so it uses the .Net int.MaxValue. This is a classic noisy neighbor problem where shared services become a point of vulnerability to all the consuming applications. The one project with “” for ContentSearch.SearchMaxResults could be responsible for dragging Solr performance down across all the sites.

Solr is an extensible platform much like Sitecore, and in some ways even more so. In Sitecore one extends pipelines or overrides events to achieve the customizations you desire; the same general idea can be applied to Solr – you just use Java with Solr instead of C#.

In this case, our concern being unbounded Solr queries, we can use an extension to a search component (org.apache.solr.handler.component.SearchComponent) to introduce our custom logic into the Solr query processing. In our case, we want to enforce limits to the number of rows a query can request. This would be a safety net in case an un-tuned Sitecore implementation left a ContentSearch.SearchMaxResults setting at the default.

Some care must be taken in how this is introduced into the Solr execution context and where exactly the custom logic is handled. This topic is all covered very well, along with sample code etc, at https://jorgelbg.wordpress.com/2015/03/12/adding-some-safeguard-measures-to-solr-with-a-searchcomponent/. For an enterprise Solr implementation with a variety of Sitecore consumers, a safety measure such as this could be vital to the general stability and perf of the platform – especially if one doesn’t have control over the specific Sitecore projects and their use (or abuse!) of the ContentSearch.SearchMaxResults setting. Maybe file this under best practices for governing Sitecore implementations with shared Solr infrastructure.

Sitecore Commerce 8.2.1 and ListManager with EXM

I’ve been engaged on a few more Sitecore Commerce builds (Commerce 8.2.1 still as these have carried over from 2017) and found an interesting wrinkle the other day. At first, it looked like a MongoDB issue as contacts weren’t being properly added to Sitecore ListManager “Lists” for use in EXM, but after scratching beneath the surface it was a lot more interesting. I used a utility sent my way by Sitecore support — it’s a .zip that has a Sitecore 8.2 update-5 specific tool for seeing Sitecore Lists and their status in terms of what’s in MongoDB and what’s in content search indexes (Solr in my case).

The tool made it pretty clear that the data was being stored properly in MongoDB but NOT in the search index (the screenshot below shows “Contacts in index: 3” which is after we corrected the problem — initially the Contacts in index would only ever show 0 and that’s what helped to isolate the problem to Sitecore Content Search):

lists

Another piece of evidence, in the Sitecore UI when we’d try to add a new contact to ListManager we’d see this message:

Please note that contacts in the list are currently being indexed, so not all contacts are available to view at this time. 0 out of 3 contacts are currently indexed.

Once we enabled verbose logging for search and examined the Search.log output, we see messages like this in the logs:

INFO  Solr Query - ?q=(type_t:(contact) AND contact.tags_sm:(ContactLists\:\{B76B0E74-E94D-4EBB-F219-6A347C75520D\}))&start=0&rows=20&fl=contact.contactid_s,contact.identifier_t,contact.firstname_t,contact.surname_t,contact.preferredemail_t,contactscount_tl,_uniqueid,_datasource&fq=_indexname:(sitecore_analytics_index)

I bolded the contact.tags_sm criteria in the query as that turned out to be key. This is the query that Sitecore issues to Solr when trying to obtain contacts for ListManager.

Through considerable trial and error, Solr schema inspection, and just determination (and I think Dana [https://twitter.com/thesoftwarejedi] was the one who finally yelled “bingo” and discovered this), when we run this query directly against Solr, we would find our missing ListManagement contact:s

http://solr-server:8983/solr/sitecore_analytics_index/select?q=(type_t:(contact) AND contact.tags_tm:(ContactLists\:\{B76B0E74-E94D-4EBB-F219-6A347C75520D\}))&start=0&rows=20&fl=contact.contactid_s,contact.identifier_t,contact.firstname_t,contact.surname_t,contact.preferredemail_t,contactscount_tl,_uniqueid,_datasource&fq=_indexname:(sitecore_analytics_index

The contact.tags_tm is bold above, and that was the crux of our challenge.

Sitecore was indexing contacts using tags_tm while Sitecore queries were looking for tags_sm.

In Sitecore config file CommerceServer\CommerceServer.ContentSearch.Solr.DefaultIndexConfiguration.config is a fragment of XML like the following:

<typeMatches hint="raw:AddTypeMatch">
 <typeMatch typeName="idCollection" type="System.Collections.Generic.List`1[[Sitecore.Data.ID, Sitecore.Kernel]]" fieldNameFormat="{0}_sm" multiValued="true" settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider" />
 <typeMatch typeName="textCollection" type="System.Collections.Generic.List`1[System.String]" fieldNameFormat="{0}_tm" multiValued="true" settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider" />

The typeMatch for typeName=”textCollection” was the issue, along with how it duplicates the mapping for the System.Collections.Generic.List`1[System.String] type — and the many places that were using the textCollection returnType that depended on this typeMatch. I removed the typeMatch from the config file and updated any dependency on textCollection to use stringCollection instead and . . . magic . . . the contacts properly indexed into Solr and the contact.tags_sm criteria would match the new data.

According to Sitecore Support, this is a defect in the way Commerce search indexing is setup and it’s overlap with Sitecore ListManager (EXM in our case). Commerce should probably use a custom configuration section instead of modifying the default index configuration, but we’ll have to wait and see how this is implemented in a future patch or release.

For the time being, I’ve created the following Sitecore patch configuration file to remove the textCollection elements. This is preferable to editing the standard Sitecore configuration files that come with the product and will make for easier Sitecore upgrades or adjustments when (or if?) a true correction for this defect is released by Sitecore:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
 <sitecore>
 <contentSearch>
 <indexConfigurations>
 <defaultSolrIndexConfiguration type="Sitecore.ContentSearch.SolrProvider.SolrIndexConfiguration, Sitecore.ContentSearch.SolrProvider">
 <fieldMap type="Sitecore.ContentSearch.SolrProvider.SolrFieldMap, Sitecore.ContentSearch.SolrProvider">
 <typeMatches hint="raw:AddTypeMatch">
 <typeMatch typeName="textCollection">
 <patch:delete />
 </typeMatch>
 </typeMatches>
 <fieldNames>
 <field fieldName="instocklocations">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 <field fieldName="outofstocklocations">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 <field fieldName="orderablelocations">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 <field fieldName="commerceancestornames">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field> 
 </fieldNames>
 <fieldTypes hint="raw:AddFieldByFieldTypeName">
 <fieldType fieldTypeName="catalog selection control">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </fieldType>
 <fieldType fieldTypeName="child categories list control">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </fieldType>
 <fieldType fieldTypeName="child products list control">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </fieldType>
 <fieldType fieldTypeName="parent categories list control">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </fieldType>
 <fieldType fieldTypeName="relationship list control">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </fieldType>
 <fieldType fieldTypeName="variant list control">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </fieldType>
 </fieldTypes>
 </fieldMap>
 <documentOptions>
 <fields hint="raw:AddComputedIndexField">
 <field fieldName="instocklocations">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 <field fieldName="outofstocklocations">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 <field fieldName="orderablelocations">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 <field fieldName="commerceancestornames">
 <patch:attribute name="returnType">stringCollection</patch:attribute>
 </field>
 </fields>
 </documentOptions>
 </defaultSolrIndexConfiguration>
 </indexConfigurations>
 </contentSearch>
 </sitecore>
</configuration>

How a 13 year old archived list serv helped me out with Sitecore Commerce

Sitecore Commerce is an interesting landscape — it’s never a dull moment.  After recently swapping the backing store from Azure SQL to SQL Server (due to an interesting Inventory gotcha with the Reference Storefront that I’ll maybe share at some other time), I’m finding nooks and crannies of configuration I never knew existed with the Commerce product until now.

After I migrated to Azure IaaS SQL Server VMs from Azure SQL, I thought I had everything tidied up.

  • Commerce Server Manager references?  ✔
  • Sitecore application connection strings?  ✔
  • Bootstrap configuration (I posted this gist on manipulating those files to make this easier)? 

I updated the Azure SQL database credentials to prove that I had no lingering connections to Azure SQL.  I encountered an exception at Sitecore start-up related to initialization of the profile service, however, and had to start digging.  CommerceProfileSystemException was the exception type and the stacktrace started as follows:

Exception type: CommerceProfileSystemException 
 Exception message: Failed to initialize profile service handle.
 at CommerceServer.Core.Runtime.Profiles.ProfileContext..ctor(String profileServiceConnectionString, String providerConnectionString, String bdaoConnectionString, DebugContext debugContext)
 at CommerceServer.Core.Runtime.CommerceContextFactory.CreateProfileContext()
 at CommerceServer.Core.Runtime.CommerceContextFactory.get_ProfileContextSingleton()
 at CommerceServer.Core.Runtime.Profiles.CommerceProfileModule.get_ModuleProfileContext()
 at CommerceServer.Core.Runtime.Profiles.CommerceProfileModule.get_ProfileContext()

The Commerce Server Manager encapsulates the connection strings, and I thought I had them all updated to the SQL Server VM equivalents, even going so far as to inspect MSCS_Admin in SQL Server with a query like this:

SELECT [i_ResourceID]
 ,[s_PropertyName]
 ,[s_Value]
 FROM [MSCS_Admin].[dbo].[ResourceProps]
 where f_IsConnStr=1
 ORDER BY 1

While interesting to find where this information is stored (may or may not be permanent, though — tough to tell with Commerce!), this output didn’t shed light on what might be going on, though:

Eventually I stumbled across some 13 year old documentation on Commerce Server discussing updating the ProfileService data source in some detail (http://microsoft.public.commerceserver.general.narkive.com/NPLMLusv/commerce-2002sp3-on-windows-2003-can-t-change-profiles-data-source).  It turns out, this 13 year old solution was completely applicable to my 2017 Sitecore Commerce predicament.

Succinctly, within Commerce Server manager you should do the following:

  1. Expand the Commerce Server “Global Resources” node, then “Profiles” node, then “Profile Catalog” node, then “Data Sources” node, and finally expand the “ProfileService_SQLSource” node
  2. Click on the Partitions node:
  3. In the right pane, there’s a SQLSource element you right-click and choose “Properties”
  4. Select the Partitions Tab, then “Edit” the connection string
  5. Make your connection string modifications here.  This is where my elusive reference to Azure SQL was hiding and causing Sitecore to fail to initialize.

The more work I do with Sitecore Commerce, the more I’m appreciating the value of the older documentation targeting previous editions of the product.  The catch is, it’s not 100% relevant to the modern experience with Sitecore Commerce . . . and knowing what is and isn’t applicable to the Sitecore Commerce 8.2.1 world is a challenge.  I think we’re getting there, a little bit at a time!

A note on the Sitecore Commerce DCOM Config Permissions

I’ve been having lots of fun with multiple Sitecore Commerce projects and Azure SQL lately . . . here’s one quick note I can share that might save some hassle for others working in this space . . .

This Sitecore link mentions a DCOM permission to verify in the course of troubleshooting some Sitecore Commerce “Staging” issues.  My problem was that this guid, 7E95698D-CD3C-4C98-93C7-6510C31F7DDF, wasn’t visible in the Component Services treeview under “DCOM Config.”

Sitecore support informed me that they needed to update their documentation to mention that in the absence of that Guid, one should locate the “CSS Replication Server” object in the treeview and proceed from there.

My screen shot shows where the illusive Guid is stored as a property of the CSS Replication Server entry: DCOM

I expect Sitecore will update their documentation shortly, so this blog post may have a brief shelf-life in terms of relevancy . . . but if you’re like me and the Commerce platform, any notes are appreciated, so I’ll see if this helps anyone in the community.

Encrypting Sitecore connection strings for Sitecore Commerce, Azure SQL, and beyond

There’s been a lot of Sitecore Commerce on my plate this Summer, and sadly one limitation of using that product for some customers is the requirement for SQL Server authentication instead of Active Directory and Windows Auth; I won’t get into why they need SQL auth at this point, but trust that in many use-cases this is a necessity.

In an effort to always deliver a secured platform for customers, at Rackspace we encrypt the App_Config/connectionStrings.config file to avoid having plaintext usernames and passwords on disk.    This is a link to our Rackspace GitHub “gist” performing such encryption with the ASP.Net tool aspnet_regiis.exe.  The logic is also there to un-encrypt, in case that’s necessary.

Encryption success
You can update the $configLocation variable at the top of the script to point to where your Sitecore installation is located; you then run this script using PowerShell, and you’ll get an output like this.

Once you’ve run the script, your connectionStrings.config file will resemble this:

Before you get too excited, for Sitecore Commerce in the current incarnation, there are several other plaintext passwords on disk in the \CommerceAuthoring\wwwroot\data\Environments and related .json files for both SQL and Sitecore.  The PowerShell I’ve shared doesn’t address those areas.  The Sitecore Commerce documentation does a good job of cataloging where to find these references, at least, but this still leaves a lot to be desired in terms of security.

I’m not going to go too far down this path, since I mostly wanted to post the PowerShell we use to automate SQL Server connection string encryption.  This technique can be useful for a variety of projects, not just for Sitecore Commerce — although this is the use case we’re repeatedly seeing right now.  If I have time, I’ll share some other Sitecore Commerce tips around Azure SQL friendly deployments (Sitecore’s documentation is a decent start, but lacking in some respects).

Here’s the script to encrypt/decrypt those Sitecore connectionStrings.config file:


<#
Note:
– The encyption is specific to each server, so this needs to be run separately on every IIS server
– ASPNet_RegIIS requires a web.config file to operate, so we have to massage our Sitecore .config into a web.config format it will understand
Steps:
1) Copy current Connectionstrings.config to a file named "web.config"
2) insert <configuration> node surrounding the <connectionStrings> XML
3) run this new web.config file through aspNet_RegIIS…
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -pef "connectionStrings" "S:\Sitecore\TEST-CMS\website\App_Config"
4) take the contents of the — now encrypted — web.config file and pull the information within the
<connectionStrings>…</connectionStrings> nodes and overwrite what's currently in connectionStrings.config
#>
$configLocation = "S:\Sitecore\website\App_Config"
#this is here only in case you want a back-up, but don't blindly leave a back-up around or it defeats the purpose of encrypting
#Copy-Item -Path ($configLocation + "\connectionStrings.config") -Destination ($configLocation + "\connectionStrings.PlainText.backup")
Copy-Item -Path ($configLocation + "\connectionStrings.config") -Destination ($configLocation + "\web.config")
$plainConnectionStrings = Get-Content ($configLocation + "\web.config")
$plainConnectionStrings.replace('</connectionStrings>', '</connectionStrings></configuration>') | Set-Content ($configLocation + "\web.config")
$plainConnectionStrings = Get-Content ($configLocation + "\web.config")
$plainConnectionStrings.replace('<connectionStrings>', '<configuration><connectionStrings>') | Set-Content ($configLocation + "\web.config")
#Encrypt
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -pef "connectionStrings" $configLocation
$encryptedString = Get-Content ($configLocation + "\web.config")
$encryptedString.replace('</connectionStrings></configuration>', '</connectionStrings>') | Set-Content ($configLocation + "\web.config")
$encryptedString = Get-Content ($configLocation + "\web.config")
$encryptedString.replace('<configuration><connectionStrings', '<connectionStrings') | Set-Content ($configLocation + "\web.config")
#this is now our XML to inject into the Sitecore connectionStrings.config
$encryptedString = Get-Content ($configLocation + "\web.config")
Clear-Content -Path ($configLocation + "\connectionStrings.config")
Set-Content -Path ($configLocation + "\connectionStrings.config") -Value $encryptedString
Remove-Item ($configLocation + "\web.config")
Write-Host "$configLocation\webconnectionStrings.config is now encrypted" -ForegroundColor Magenta
########################################################################
# to un-encrypt, run the following from the machine that performed the encryption. ConnectionStrings will be revealed in plain text in a new web.config file
<#
$configLocation = "S:\Sitecore\website\App_Config"
Copy-Item -Path ($configLocation + "\connectionStrings.config") -Destination ($configLocation + "\web.config")
$plainConnectionStrings = Get-Content ($configLocation + "\web.config")
$plainConnectionStrings.replace('</connectionStrings>', '</connectionStrings></configuration>') | Set-Content ($configLocation + "\web.config")
$plainConnectionStrings = Get-Content ($configLocation + "\web.config")
$plainConnectionStrings.replace('<connectionStrings', '<configuration><connectionStrings') | Set-Content ($configLocation + "\web.config")
C:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe -pdf "connectionStrings" $configLocation
Write-Host "Check $configLocation\web.config for the plain text configuration" -ForegroundColor Magenta
#>