Skip to content

Commit

Permalink
* move the update ignorance for BasePost.AuthorUid to a new method …
Browse files Browse the repository at this point in the history
…`GlobalFieldUpdateIgnorance()`

* move method `IsLatestReplierUser()` to the only implement in `UserSaver` of a new method `ShouldIgnoreEntityRevision()`
@ SaverWithRevision.cs

+ static prop `Instance` @ `User.EqualityComparer`
* move placeholder implement of virtual method `RevisionWithSplitting.IsAllFieldsIsNullExceptSplit()` to its parent declare in `BaseRevisionWithSplitting`
- static field `_dataSourceSingleton` @ CrawlerDbContext.cs
@ crawler

+ static prop `Instance` @ `ByteArrayEqualityComparer`
+ static prop `Instance` @ `NpgsqlCamelCaseNameTranslator`
- merge method `GetNpgsqlDataSourceFactory()` with `GetNpgsqlDataSource` and convert it from `protected virtual` to `private` @ TbmDbContext.cs
@ shared
@ c#
  • Loading branch information
n0099 committed May 15, 2024
1 parent 8663191 commit a0f48f3
Show file tree
Hide file tree
Showing 11 changed files with 54 additions and 49 deletions.
8 changes: 1 addition & 7 deletions c#/crawler/src/Db/CrawlerDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ namespace tbm.Crawler.Db;
public class CrawlerDbContext(ILogger<CrawlerDbContext> logger, Fid fid = 0)
: TbmDbContext<CrawlerDbContext.ModelCacheKeyFactory>(logger)
{
private static Lazy<NpgsqlDataSource>? _dataSourceSingleton;

public delegate CrawlerDbContext NewDefault();
public delegate CrawlerDbContext New(Fid fid);

Expand Down Expand Up @@ -108,11 +106,7 @@ protected override void OnModelCreating(ModelBuilder b)
}

protected override void OnBuildingNpgsqlDataSource(NpgsqlDataSourceBuilder builder) =>
builder.MapEnum<PostType>("tbmcr_triggeredBy", new NpgsqlCamelCaseNameTranslator());

[SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields")]
protected override Lazy<NpgsqlDataSource> GetNpgsqlDataSource(string? connectionString) =>
_dataSourceSingleton ??= GetNpgsqlDataSourceFactory(connectionString);
builder.MapEnum<PostType>("tbmcr_triggeredBy", NpgsqlCamelCaseNameTranslator.Instance);

public class ModelCacheKeyFactory : IModelCacheKeyFactory
{ // https://stackoverflow.com/questions/51864015/entity-framework-map-model-class-to-table-at-run-time/51899590#51899590
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ public abstract class BaseRevisionWithSplitting : RowVersionedEntity
{
public uint TakenAt { get; set; }
public ushort? NullFieldsBitMask { get; set; }
public abstract bool IsAllFieldsIsNullExceptSplit();
public virtual bool IsAllFieldsIsNullExceptSplit() => throw new NotSupportedException();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ public abstract class RevisionWithSplitting<TBaseRevision> : BaseRevisionWithSpl
{
private readonly Dictionary<Type, TBaseRevision> _splitEntities = [];
public IReadOnlyDictionary<Type, TBaseRevision> SplitEntities => _splitEntities;
public override bool IsAllFieldsIsNullExceptSplit() => throw new NotSupportedException();

protected TValue? GetSplitEntityValue<TSplitEntity, TValue>
(Func<TSplitEntity, TValue?> valueSelector)
Expand Down
4 changes: 3 additions & 1 deletion c#/crawler/src/Db/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ public class User : TimestampedEntity

public class EqualityComparer : EqualityComparer<User>
{
public static EqualityComparer Instance { get; } = new();

public override bool Equals(User? x, User? y) => x == y || (
x != null && y != null &&
(x.Uid, x.Name, x.DisplayName, x.Portrait, x.PortraitUpdatedAt, x.Gender, x.FansNickname, x.IpGeolocation)
== (y.Uid, y.Name, y.DisplayName, y.Portrait, y.PortraitUpdatedAt, y.Gender, y.FansNickname, y.IpGeolocation)
&& (x.Icon == y.Icon
|| (x.Icon != null && y.Icon != null && new ByteArrayEqualityComparer().Equals(x.Icon, y.Icon))));
|| (x.Icon != null && y.Icon != null && ByteArrayEqualityComparer.Instance.Equals(x.Icon, y.Icon))));

public override int GetHashCode(User obj)
{
Expand Down
2 changes: 1 addition & 1 deletion c#/crawler/src/Tieba/Crawl/Saver/ReplySignatureSaver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ select s
public sealed record UniqueSignature(uint Id, byte[] XxHash3)
{
public bool Equals(UniqueSignature? other) =>
other != null && Id == other.Id && new ByteArrayEqualityComparer().Equals(XxHash3, other.XxHash3);
other != null && Id == other.Id && ByteArrayEqualityComparer.Instance.Equals(XxHash3, other.XxHash3);

public override int GetHashCode()
{
Expand Down
38 changes: 10 additions & 28 deletions c#/crawler/src/Tieba/Crawl/Saver/SaverWithRevision.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ public abstract class SaverWithRevision<TBaseRevision>
protected abstract IReadOnlyDictionary<Type, AddRevisionDelegate> AddRevisionDelegatesKeyBySplitEntityType { get; }
protected abstract NullFieldsBitMask GetRevisionNullFieldBitMask(string fieldName);

private static bool GlobalFieldUpdateIgnorance(string propName, object? oldValue, object? newValue) => propName switch
{ // possible rarely respond with the protoBuf default value 0
nameof(BasePost.AuthorUid) when newValue is 0L && oldValue is not null => true,
_ => false
};
protected virtual bool FieldUpdateIgnorance(string propName, object? oldValue, object? newValue) => false;
protected virtual bool FieldRevisionIgnorance(string propName, object? oldValue, object? newValue) => false;
protected virtual bool UserFieldUpdateIgnorance(string propName, object? oldValue, object? newValue) => false;
protected virtual bool UserFieldRevisionIgnorance(string propName, object? oldValue, object? newValue) => false;
protected virtual bool ShouldIgnoreEntityRevision(string propName, PropertyEntry propEntry, EntityEntry entityEntry) => false;

protected void SaveEntitiesWithRevision<TEntity, TRevision>(
CrawlerDbContext db,
Expand Down Expand Up @@ -51,15 +57,14 @@ bool IsTimestampingFieldName(string name) => name is nameof(BasePost.LastSeenAt)
{
var pName = p.Metadata.Name;
if (!p.IsModified || IsTimestampingFieldName(pName)) continue;
if (ShouldIgnoreEntityRevision(pName, p, entityEntry)) return null;
if (FieldUpdateIgnorance(
pName, p.OriginalValue, p.CurrentValue)
|| SaverWithRevision<TBaseRevision>.GlobalFieldUpdateIgnorance(
pName, p.OriginalValue, p.CurrentValue)
|| (entryIsUser && UserFieldUpdateIgnorance(
pName, p.OriginalValue, p.CurrentValue))
// possible rarely respond with the protoBuf default value 0
|| (pName == nameof(BasePost.AuthorUid)
&& p is { CurrentValue: 0L, OriginalValue: not null}))
pName, p.OriginalValue, p.CurrentValue)))
{
p.IsModified = false;
continue; // skip following revision check
Expand All @@ -70,8 +75,6 @@ bool IsTimestampingFieldName(string name) => name is nameof(BasePost.LastSeenAt)
pName, p.OriginalValue, p.CurrentValue)))
continue;
if (IsLatestReplierUser(pName, p, entityEntry)) return null;
if (!IRevisionProperties.Cache[typeof(TRevision)].TryGetValue(pName, out var revisionProp))
{
object? ToHexWhenByteArray(object? value) =>
Expand Down Expand Up @@ -118,25 +121,4 @@ bool IsTimestampingFieldName(string name) => name is nameof(BasePost.LastSeenAt)
.GroupBy(pair => pair.Key, pair => pair.Value)
.ForEach(g => AddRevisionDelegatesKeyBySplitEntityType[g.Key](db, g));
}

private static bool IsLatestReplierUser(string pName, PropertyEntry p, EntityEntry entry)
{
// ThreadCrawlFacade.ParseLatestRepliers() will save users with empty string as portrait
// they may soon be updated by (sub) reply crawler after it find out the latest reply
// so we should ignore its revision update for all fields
// ignore entire record is not possible via IFieldChangeIgnorance.GlobalFieldChangeIgnorance.Revision()
// since it can only determine one field at the time
if (pName != nameof(User.Portrait) || p.OriginalValue is not "") return false;

// invokes OriginalValues.ToObject() to get a new instance
// since entityInTracking is reference to the changed one
var user = (User)entry.OriginalValues.ToObject();

// create another user instance with only fields of latest replier filled
var latestReplier = User.CreateLatestReplier(user.Uid, user.Name, user.DisplayName);

// if they are same by fields values, the original one is the latest replier
// that previously generated by ParseLatestRepliers()
return new User.EqualityComparer().Equals(user, latestReplier);
}
}
23 changes: 23 additions & 0 deletions c#/crawler/src/Tieba/Crawl/Saver/UserSaver.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace tbm.Crawler.Tieba.Crawl.Saver;

public partial class UserSaver
Expand Down Expand Up @@ -31,6 +33,27 @@ protected override bool FieldRevisionIgnorance
_ => false
};

protected override bool ShouldIgnoreEntityRevision(string propName, PropertyEntry propEntry, EntityEntry entityEntry)
{
// ThreadCrawlFacade.ParseLatestRepliers() will save users with empty string as portrait
// they may soon be updated by (sub) reply crawler after it find out the latest reply
// so we should ignore its revision update for all fields
// ignore entire record is not possible via IFieldChangeIgnorance.GlobalFieldChangeIgnorance.Revision()
// since it can only determine one field at the time
if (propName != nameof(User.Portrait) || propEntry.OriginalValue is not "") return false;

// invokes OriginalValues.ToObject() to get a new instance
// since entityInTracking is reference to the changed one
var user = (User)entityEntry.OriginalValues.ToObject();

// create another user instance with only fields of latest replier filled
var latestReplier = User.CreateLatestReplier(user.Uid, user.Name, user.DisplayName);

// if they are same by fields values, the original one is the latest replier
// that previously generated by ParseLatestRepliers()
return User.EqualityComparer.Instance.Equals(user, latestReplier);
}

protected override Dictionary<Type, AddRevisionDelegate>
AddRevisionDelegatesKeyBySplitEntityType { get; } = new()
{
Expand Down
2 changes: 1 addition & 1 deletion c#/imagePipeline/src/Consumer/MetadataConsumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public MetadataConsumer
return new()
{
XxHash3 = xxHash3,
RawBytes = commonXxHash3ToIgnore.Contains(xxHash3, new ByteArrayEqualityComparer()) ? null : rawBytes
RawBytes = commonXxHash3ToIgnore.Contains(xxHash3, ByteArrayEqualityComparer.Instance) ? null : rawBytes
};
}

Expand Down
2 changes: 2 additions & 0 deletions c#/shared/src/ByteArrayEqualityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace tbm.Shared;

public class ByteArrayEqualityComparer : EqualityComparer<byte[]>
{
public static ByteArrayEqualityComparer Instance { get; } = new();

public override bool Equals(byte[]? x, byte[]? y) =>
x == y || (x != null && y != null && x.AsSpan().SequenceEqual(y.AsSpan()));

Expand Down
2 changes: 2 additions & 0 deletions c#/shared/src/Db/NpgsqlCamelCaseNameTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace tbm.Shared.Db;

public class NpgsqlCamelCaseNameTranslator : INpgsqlNameTranslator
{
public static NpgsqlCamelCaseNameTranslator Instance { get; } = new();

public string TranslateTypeName(string clrName) => TranslateMemberName(clrName);

/// <see>https://github.com/efcore/EFCore.NamingConventions/blob/0d19b13a8e62ec20779c3cca03c27f200b5b7458/EFCore.NamingConventions/Internal/CamelCaseNameRewriter.cs#L13</see>
Expand Down
19 changes: 10 additions & 9 deletions c#/shared/src/Db/TbmDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public class TbmDbContext<TModelCacheKeyFactory>(ILogger<TbmDbContext<TModelCach
: TbmDbContext(logger)
where TModelCacheKeyFactory : class, IModelCacheKeyFactory
{
[SuppressMessage("ReSharper", "StaticMemberInGenericType")]
private static Lazy<NpgsqlDataSource>? _dataSourceSingleton;

Check failure on line 100 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (crawler)

A static field in a generic type is not shared among instances of different close constructed types. (https://rules.sonarsource.com/csharp/RSPEC-2743)

Check failure on line 100 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (crawler)

A static field in a generic type is not shared among instances of different close constructed types. (https://rules.sonarsource.com/csharp/RSPEC-2743)

Check failure on line 100 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (imagePipeline)

A static field in a generic type is not shared among instances of different close constructed types. (https://rules.sonarsource.com/csharp/RSPEC-2743)

Check failure on line 100 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (imagePipeline)

A static field in a generic type is not shared among instances of different close constructed types. (https://rules.sonarsource.com/csharp/RSPEC-2743)

Check failure on line 100 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (shared)

A static field in a generic type is not shared among instances of different close constructed types. (https://rules.sonarsource.com/csharp/RSPEC-2743)

Check failure on line 100 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (shared)

A static field in a generic type is not shared among instances of different close constructed types. (https://rules.sonarsource.com/csharp/RSPEC-2743)

// ReSharper disable once UnusedAutoPropertyAccessor.Global
public required IConfiguration Config { private get; init; }
public DbSet<ImageInReply> ImageInReplies => Set<ImageInReply>();
Expand Down Expand Up @@ -139,13 +142,11 @@ protected override void OnModelCreating(ModelBuilder b)

protected virtual void OnBuildingNpgsqlDataSource(NpgsqlDataSourceBuilder builder) { }

protected virtual Lazy<NpgsqlDataSource> GetNpgsqlDataSource(string? connectionString) =>
throw new NotSupportedException();

protected Lazy<NpgsqlDataSource> GetNpgsqlDataSourceFactory(string? connectionString) => new(() =>
{
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
OnBuildingNpgsqlDataSource(dataSourceBuilder);
return dataSourceBuilder.Build();
});
private Lazy<NpgsqlDataSource> GetNpgsqlDataSource(string? connectionString) =>
_dataSourceSingleton ??= new(() =>

Check failure on line 146 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (crawler)

Make the enclosing instance method 'static' or remove this set on the 'static' field. (https://rules.sonarsource.com/csharp/RSPEC-2696)

Check failure on line 146 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (crawler)

Make the enclosing instance method 'static' or remove this set on the 'static' field. (https://rules.sonarsource.com/csharp/RSPEC-2696)

Check failure on line 146 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (imagePipeline)

Make the enclosing instance method 'static' or remove this set on the 'static' field. (https://rules.sonarsource.com/csharp/RSPEC-2696)

Check failure on line 146 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (imagePipeline)

Make the enclosing instance method 'static' or remove this set on the 'static' field. (https://rules.sonarsource.com/csharp/RSPEC-2696)

Check failure on line 146 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (shared)

Make the enclosing instance method 'static' or remove this set on the 'static' field. (https://rules.sonarsource.com/csharp/RSPEC-2696)

Check failure on line 146 in c#/shared/src/Db/TbmDbContext.cs

View workflow job for this annotation

GitHub Actions / build (shared)

Make the enclosing instance method 'static' or remove this set on the 'static' field. (https://rules.sonarsource.com/csharp/RSPEC-2696)
{
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
OnBuildingNpgsqlDataSource(dataSourceBuilder);
return dataSourceBuilder.Build();
});
}

0 comments on commit a0f48f3

Please sign in to comment.