Initialize CBDD solution and add a .NET-focused gitignore for generated artifacts.
This commit is contained in:
118
src/CBDD.Core/Compression/CompressedPayloadHeader.cs
Normal file
118
src/CBDD.Core/Compression/CompressedPayloadHeader.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Fixed header prefix for compressed payload blobs.
|
||||
/// </summary>
|
||||
public readonly struct CompressedPayloadHeader
|
||||
{
|
||||
public const int Size = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Compression codec used for payload bytes.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Original uncompressed payload length.
|
||||
/// </summary>
|
||||
public int OriginalLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compressed payload length.
|
||||
/// </summary>
|
||||
public int CompressedLength { get; }
|
||||
|
||||
/// <summary>
|
||||
/// CRC32 checksum of compressed payload bytes.
|
||||
/// </summary>
|
||||
public uint Checksum { get; }
|
||||
|
||||
public CompressedPayloadHeader(CompressionCodec codec, int originalLength, int compressedLength, uint checksum)
|
||||
{
|
||||
if (originalLength < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(originalLength));
|
||||
if (compressedLength < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(compressedLength));
|
||||
|
||||
Codec = codec;
|
||||
OriginalLength = originalLength;
|
||||
CompressedLength = compressedLength;
|
||||
Checksum = checksum;
|
||||
}
|
||||
|
||||
public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength, ReadOnlySpan<byte> compressedPayload)
|
||||
{
|
||||
var checksum = ComputeChecksum(compressedPayload);
|
||||
return new CompressedPayloadHeader(codec, originalLength, compressedPayload.Length, checksum);
|
||||
}
|
||||
|
||||
public void WriteTo(Span<byte> destination)
|
||||
{
|
||||
if (destination.Length < Size)
|
||||
throw new ArgumentException($"Destination must be at least {Size} bytes.", nameof(destination));
|
||||
|
||||
destination[0] = (byte)Codec;
|
||||
destination[1] = 0;
|
||||
destination[2] = 0;
|
||||
destination[3] = 0;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(4, 4), OriginalLength);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(8, 4), CompressedLength);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12, 4), Checksum);
|
||||
}
|
||||
|
||||
public static CompressedPayloadHeader ReadFrom(ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (source.Length < Size)
|
||||
throw new ArgumentException($"Source must be at least {Size} bytes.", nameof(source));
|
||||
|
||||
var codec = (CompressionCodec)source[0];
|
||||
var originalLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(4, 4));
|
||||
var compressedLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(8, 4));
|
||||
var checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(12, 4));
|
||||
return new CompressedPayloadHeader(codec, originalLength, compressedLength, checksum);
|
||||
}
|
||||
|
||||
public bool ValidateChecksum(ReadOnlySpan<byte> compressedPayload)
|
||||
{
|
||||
return Checksum == ComputeChecksum(compressedPayload);
|
||||
}
|
||||
|
||||
public static uint ComputeChecksum(ReadOnlySpan<byte> payload) => Crc32Calculator.Compute(payload);
|
||||
|
||||
private static class Crc32Calculator
|
||||
{
|
||||
private const uint Polynomial = 0xEDB88320u;
|
||||
private static readonly uint[] Table = CreateTable();
|
||||
|
||||
public static uint Compute(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
uint crc = 0xFFFFFFFFu;
|
||||
for (int i = 0; i < payload.Length; i++)
|
||||
{
|
||||
var index = (crc ^ payload[i]) & 0xFF;
|
||||
crc = (crc >> 8) ^ Table[index];
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
private static uint[] CreateTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
for (uint i = 0; i < table.Length; i++)
|
||||
{
|
||||
uint value = i;
|
||||
for (int bit = 0; bit < 8; bit++)
|
||||
{
|
||||
value = (value & 1) != 0 ? (value >> 1) ^ Polynomial : value >> 1;
|
||||
}
|
||||
|
||||
table[i] = value;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/CBDD.Core/Compression/CompressionCodec.cs
Normal file
11
src/CBDD.Core/Compression/CompressionCodec.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Supported payload compression codecs.
|
||||
/// </summary>
|
||||
public enum CompressionCodec : byte
|
||||
{
|
||||
None = 0,
|
||||
Brotli = 1,
|
||||
Deflate = 2
|
||||
}
|
||||
71
src/CBDD.Core/Compression/CompressionOptions.cs
Normal file
71
src/CBDD.Core/Compression/CompressionOptions.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Compression configuration for document payload processing.
|
||||
/// </summary>
|
||||
public sealed class CompressionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Default compression options (compression disabled).
|
||||
/// </summary>
|
||||
public static CompressionOptions Default { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Enables payload compression for new writes.
|
||||
/// </summary>
|
||||
public bool EnableCompression { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum payload size (bytes) required before compression is attempted.
|
||||
/// </summary>
|
||||
public int MinSizeBytes { get; init; } = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum percentage of size reduction required to keep compressed output.
|
||||
/// </summary>
|
||||
public int MinSavingsPercent { get; init; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Preferred default codec for new writes.
|
||||
/// </summary>
|
||||
public CompressionCodec Codec { get; init; } = CompressionCodec.Brotli;
|
||||
|
||||
/// <summary>
|
||||
/// Compression level passed to codec implementations.
|
||||
/// </summary>
|
||||
public CompressionLevel Level { get; init; } = CompressionLevel.Fastest;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed decompressed payload size.
|
||||
/// </summary>
|
||||
public int MaxDecompressedSizeBytes { get; init; } = 16 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Optional maximum input size allowed for compression attempts.
|
||||
/// </summary>
|
||||
public int? MaxCompressionInputBytes { get; init; }
|
||||
|
||||
internal static CompressionOptions Normalize(CompressionOptions? options)
|
||||
{
|
||||
var candidate = options ?? Default;
|
||||
|
||||
if (candidate.MinSizeBytes < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MinSizeBytes), "MinSizeBytes must be non-negative.");
|
||||
|
||||
if (candidate.MinSavingsPercent is < 0 or > 100)
|
||||
throw new ArgumentOutOfRangeException(nameof(MinSavingsPercent), "MinSavingsPercent must be between 0 and 100.");
|
||||
|
||||
if (!Enum.IsDefined(candidate.Codec))
|
||||
throw new ArgumentOutOfRangeException(nameof(Codec), $"Unsupported codec: {candidate.Codec}.");
|
||||
|
||||
if (candidate.MaxDecompressedSizeBytes <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxDecompressedSizeBytes), "MaxDecompressedSizeBytes must be greater than 0.");
|
||||
|
||||
if (candidate.MaxCompressionInputBytes is <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxCompressionInputBytes), "MaxCompressionInputBytes must be greater than 0 when provided.");
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
166
src/CBDD.Core/Compression/CompressionService.cs
Normal file
166
src/CBDD.Core/Compression/CompressionService.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Compression codec registry and utility service.
|
||||
/// </summary>
|
||||
public sealed class CompressionService
|
||||
{
|
||||
private readonly ConcurrentDictionary<CompressionCodec, ICompressionCodec> _codecs = new();
|
||||
|
||||
public CompressionService(IEnumerable<ICompressionCodec>? additionalCodecs = null)
|
||||
{
|
||||
RegisterCodec(new NoneCompressionCodec());
|
||||
RegisterCodec(new BrotliCompressionCodec());
|
||||
RegisterCodec(new DeflateCompressionCodec());
|
||||
|
||||
if (additionalCodecs == null)
|
||||
return;
|
||||
|
||||
foreach (var codec in additionalCodecs)
|
||||
{
|
||||
RegisterCodec(codec);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterCodec(ICompressionCodec codec)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(codec);
|
||||
_codecs[codec.Codec] = codec;
|
||||
}
|
||||
|
||||
public bool TryGetCodec(CompressionCodec codec, out ICompressionCodec compressionCodec)
|
||||
{
|
||||
return _codecs.TryGetValue(codec, out compressionCodec!);
|
||||
}
|
||||
|
||||
public ICompressionCodec GetCodec(CompressionCodec codec)
|
||||
{
|
||||
if (_codecs.TryGetValue(codec, out var compressionCodec))
|
||||
return compressionCodec;
|
||||
|
||||
throw new InvalidOperationException($"Compression codec '{codec}' is not registered.");
|
||||
}
|
||||
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionCodec codec, CompressionLevel level)
|
||||
{
|
||||
return GetCodec(codec).Compress(input, level);
|
||||
}
|
||||
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, CompressionCodec codec, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return GetCodec(codec).Decompress(input, expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
|
||||
public byte[] Roundtrip(ReadOnlySpan<byte> input, CompressionCodec codec, CompressionLevel level, int maxDecompressedSizeBytes)
|
||||
{
|
||||
var compressed = Compress(input, codec, level);
|
||||
return Decompress(compressed, codec, input.Length, maxDecompressedSizeBytes);
|
||||
}
|
||||
|
||||
private sealed class NoneCompressionCodec : ICompressionCodec
|
||||
{
|
||||
public CompressionCodec Codec => CompressionCodec.None;
|
||||
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level) => input.ToArray();
|
||||
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
if (input.Length > maxDecompressedSizeBytes)
|
||||
throw new InvalidDataException($"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes).");
|
||||
|
||||
if (expectedLength >= 0 && expectedLength != input.Length)
|
||||
throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {input.Length}.");
|
||||
|
||||
return input.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BrotliCompressionCodec : ICompressionCodec
|
||||
{
|
||||
public CompressionCodec Codec => CompressionCodec.Brotli;
|
||||
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, leaveOpen: true));
|
||||
}
|
||||
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input, stream => new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DeflateCompressionCodec : ICompressionCodec
|
||||
{
|
||||
public CompressionCodec Codec => CompressionCodec.Deflate;
|
||||
|
||||
public byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level)
|
||||
{
|
||||
return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, leaveOpen: true));
|
||||
}
|
||||
|
||||
public byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes)
|
||||
{
|
||||
return DecompressWithCodecStream(input, stream => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] CompressWithCodecStream(ReadOnlySpan<byte> input, Func<Stream, Stream> streamFactory)
|
||||
{
|
||||
using var output = new MemoryStream(capacity: input.Length);
|
||||
using (var codecStream = streamFactory(output))
|
||||
{
|
||||
codecStream.Write(input);
|
||||
codecStream.Flush();
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] DecompressWithCodecStream(
|
||||
ReadOnlySpan<byte> input,
|
||||
Func<Stream, Stream> streamFactory,
|
||||
int expectedLength,
|
||||
int maxDecompressedSizeBytes)
|
||||
{
|
||||
if (maxDecompressedSizeBytes <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxDecompressedSizeBytes));
|
||||
|
||||
using var compressed = new MemoryStream(input.ToArray(), writable: false);
|
||||
using var codecStream = streamFactory(compressed);
|
||||
using var output = expectedLength > 0
|
||||
? new MemoryStream(capacity: expectedLength)
|
||||
: new MemoryStream();
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||
try
|
||||
{
|
||||
int totalWritten = 0;
|
||||
while (true)
|
||||
{
|
||||
var bytesRead = codecStream.Read(buffer, 0, buffer.Length);
|
||||
if (bytesRead <= 0)
|
||||
break;
|
||||
|
||||
totalWritten += bytesRead;
|
||||
if (totalWritten > maxDecompressedSizeBytes)
|
||||
throw new InvalidDataException($"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes).");
|
||||
|
||||
output.Write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
if (expectedLength >= 0 && totalWritten != expectedLength)
|
||||
throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {totalWritten}.");
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/CBDD.Core/Compression/CompressionStats.cs
Normal file
16
src/CBDD.Core/Compression/CompressionStats.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of aggregated compression and decompression telemetry.
|
||||
/// </summary>
|
||||
public readonly struct CompressionStats
|
||||
{
|
||||
public long CompressedDocumentCount { get; init; }
|
||||
public long BytesBeforeCompression { get; init; }
|
||||
public long BytesAfterCompression { get; init; }
|
||||
public long CompressionCpuTicks { get; init; }
|
||||
public long DecompressionCpuTicks { get; init; }
|
||||
public long CompressionFailureCount { get; init; }
|
||||
public long ChecksumFailureCount { get; init; }
|
||||
public long SafetyLimitRejectionCount { get; init; }
|
||||
}
|
||||
88
src/CBDD.Core/Compression/CompressionTelemetry.cs
Normal file
88
src/CBDD.Core/Compression/CompressionTelemetry.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe counters for compression/decompression lifecycle events.
|
||||
/// </summary>
|
||||
public sealed class CompressionTelemetry
|
||||
{
|
||||
private long _compressionAttempts;
|
||||
private long _compressionSuccesses;
|
||||
private long _compressionFailures;
|
||||
private long _compressionSkippedTooSmall;
|
||||
private long _compressionSkippedInsufficientSavings;
|
||||
private long _decompressionAttempts;
|
||||
private long _decompressionSuccesses;
|
||||
private long _decompressionFailures;
|
||||
private long _compressionInputBytes;
|
||||
private long _compressionOutputBytes;
|
||||
private long _decompressionOutputBytes;
|
||||
private long _compressedDocumentCount;
|
||||
private long _compressionCpuTicks;
|
||||
private long _decompressionCpuTicks;
|
||||
private long _checksumFailureCount;
|
||||
private long _safetyLimitRejectionCount;
|
||||
|
||||
public long CompressionAttempts => Interlocked.Read(ref _compressionAttempts);
|
||||
public long CompressionSuccesses => Interlocked.Read(ref _compressionSuccesses);
|
||||
public long CompressionFailures => Interlocked.Read(ref _compressionFailures);
|
||||
public long CompressionSkippedTooSmall => Interlocked.Read(ref _compressionSkippedTooSmall);
|
||||
public long CompressionSkippedInsufficientSavings => Interlocked.Read(ref _compressionSkippedInsufficientSavings);
|
||||
public long DecompressionAttempts => Interlocked.Read(ref _decompressionAttempts);
|
||||
public long DecompressionSuccesses => Interlocked.Read(ref _decompressionSuccesses);
|
||||
public long DecompressionFailures => Interlocked.Read(ref _decompressionFailures);
|
||||
public long CompressionInputBytes => Interlocked.Read(ref _compressionInputBytes);
|
||||
public long CompressionOutputBytes => Interlocked.Read(ref _compressionOutputBytes);
|
||||
public long DecompressionOutputBytes => Interlocked.Read(ref _decompressionOutputBytes);
|
||||
public long CompressedDocumentCount => Interlocked.Read(ref _compressedDocumentCount);
|
||||
public long CompressionCpuTicks => Interlocked.Read(ref _compressionCpuTicks);
|
||||
public long DecompressionCpuTicks => Interlocked.Read(ref _decompressionCpuTicks);
|
||||
public long ChecksumFailureCount => Interlocked.Read(ref _checksumFailureCount);
|
||||
public long SafetyLimitRejectionCount => Interlocked.Read(ref _safetyLimitRejectionCount);
|
||||
|
||||
public void RecordCompressionAttempt(int inputBytes)
|
||||
{
|
||||
Interlocked.Increment(ref _compressionAttempts);
|
||||
Interlocked.Add(ref _compressionInputBytes, inputBytes);
|
||||
}
|
||||
|
||||
public void RecordCompressionSuccess(int outputBytes)
|
||||
{
|
||||
Interlocked.Increment(ref _compressionSuccesses);
|
||||
Interlocked.Increment(ref _compressedDocumentCount);
|
||||
Interlocked.Add(ref _compressionOutputBytes, outputBytes);
|
||||
}
|
||||
|
||||
public void RecordCompressionFailure() => Interlocked.Increment(ref _compressionFailures);
|
||||
public void RecordCompressionSkippedTooSmall() => Interlocked.Increment(ref _compressionSkippedTooSmall);
|
||||
public void RecordCompressionSkippedInsufficientSavings() => Interlocked.Increment(ref _compressionSkippedInsufficientSavings);
|
||||
public void RecordDecompressionAttempt() => Interlocked.Increment(ref _decompressionAttempts);
|
||||
public void RecordCompressionCpuTicks(long ticks) => Interlocked.Add(ref _compressionCpuTicks, ticks);
|
||||
public void RecordDecompressionCpuTicks(long ticks) => Interlocked.Add(ref _decompressionCpuTicks, ticks);
|
||||
public void RecordChecksumFailure() => Interlocked.Increment(ref _checksumFailureCount);
|
||||
public void RecordSafetyLimitRejection() => Interlocked.Increment(ref _safetyLimitRejectionCount);
|
||||
|
||||
public void RecordDecompressionSuccess(int outputBytes)
|
||||
{
|
||||
Interlocked.Increment(ref _decompressionSuccesses);
|
||||
Interlocked.Add(ref _decompressionOutputBytes, outputBytes);
|
||||
}
|
||||
|
||||
public void RecordDecompressionFailure() => Interlocked.Increment(ref _decompressionFailures);
|
||||
|
||||
public CompressionStats GetSnapshot()
|
||||
{
|
||||
return new CompressionStats
|
||||
{
|
||||
CompressedDocumentCount = CompressedDocumentCount,
|
||||
BytesBeforeCompression = CompressionInputBytes,
|
||||
BytesAfterCompression = CompressionOutputBytes,
|
||||
CompressionCpuTicks = CompressionCpuTicks,
|
||||
DecompressionCpuTicks = DecompressionCpuTicks,
|
||||
CompressionFailureCount = CompressionFailures,
|
||||
ChecksumFailureCount = ChecksumFailureCount,
|
||||
SafetyLimitRejectionCount = SafetyLimitRejectionCount
|
||||
};
|
||||
}
|
||||
}
|
||||
24
src/CBDD.Core/Compression/ICompressionCodec.cs
Normal file
24
src/CBDD.Core/Compression/ICompressionCodec.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace ZB.MOM.WW.CBDD.Core.Compression;
|
||||
|
||||
/// <summary>
|
||||
/// Codec abstraction for payload compression and decompression.
|
||||
/// </summary>
|
||||
public interface ICompressionCodec
|
||||
{
|
||||
/// <summary>
|
||||
/// Codec identifier.
|
||||
/// </summary>
|
||||
CompressionCodec Codec { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Compresses input bytes.
|
||||
/// </summary>
|
||||
byte[] Compress(ReadOnlySpan<byte> input, CompressionLevel level);
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses payload bytes with output bounds validation.
|
||||
/// </summary>
|
||||
byte[] Decompress(ReadOnlySpan<byte> input, int expectedLength, int maxDecompressedSizeBytes);
|
||||
}
|
||||
Reference in New Issue
Block a user