Design
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, computed } from 'vue'
|
||||
import { useSettingsStore } from './useSettingsStore'
|
||||
import {
|
||||
DEFAULT_DB_VERSION,
|
||||
OS_LINUX,
|
||||
OS_WINDOWS,
|
||||
OS_MAC,
|
||||
DB_TYPE_WEB,
|
||||
DB_TYPE_OLTP,
|
||||
DB_TYPE_DW,
|
||||
DB_TYPE_DESKTOP,
|
||||
DB_TYPE_MIXED,
|
||||
SIZE_UNIT_GB,
|
||||
HARD_DRIVE_SSD,
|
||||
HARD_DRIVE_HDD,
|
||||
HARD_DRIVE_SAN,
|
||||
HARD_DRIVE_NVME,
|
||||
DB_SIZE_LESS_RAM,
|
||||
DB_SIZE_MID_RAM,
|
||||
DB_SIZE_GREATER_RAM,
|
||||
} from '/src/utils/constants/configuration.js'
|
||||
|
||||
const SIZE_UNIT_MAP = {
|
||||
KB: 1024,
|
||||
MB: 1048576,
|
||||
GB: 1073741824,
|
||||
TB: 1099511627776,
|
||||
PB: 1125899906842624,
|
||||
}
|
||||
|
||||
const DEFAULT_DB_SETTINGS = {
|
||||
default: {
|
||||
['max_worker_processes']: 8,
|
||||
['max_parallel_workers_per_gather']: 2,
|
||||
['max_parallel_workers']: 8,
|
||||
},
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
dbVersion: DEFAULT_DB_VERSION,
|
||||
osType: OS_LINUX,
|
||||
dbType: DB_TYPE_WEB,
|
||||
totalMemory: null,
|
||||
totalMemoryUnit: SIZE_UNIT_GB,
|
||||
cpuNum: null,
|
||||
connectionNum: null,
|
||||
hdType: HARD_DRIVE_SSD,
|
||||
dbSize: DB_SIZE_MID_RAM,
|
||||
}
|
||||
|
||||
const selectCheckpointCompletionTarget = 0.9
|
||||
|
||||
export const useConfigurationStore = defineStore('configuration', () => {
|
||||
const state = reactive({ ...initialState })
|
||||
|
||||
const getConfiguration = computed(() => ({ ...state }))
|
||||
const getDBVersion = computed(() => getConfiguration.value.dbVersion)
|
||||
const getOSType = computed(() => getConfiguration.value.osType)
|
||||
const getDBType = computed(() => getConfiguration.value.dbType)
|
||||
const getTotalMemory = computed(() => getConfiguration.value.totalMemory)
|
||||
const getTotalMemoryUnit = computed(
|
||||
() => getConfiguration.value.totalMemoryUnit,
|
||||
)
|
||||
const getCPUNum = computed(() => getConfiguration.value.cpuNum || null)
|
||||
const getConnectionNum = computed(
|
||||
() => getConfiguration.value.connectionNum || null,
|
||||
)
|
||||
const getHDType = computed(() => getConfiguration.value.hdType)
|
||||
const getDBSize = computed(() => getConfiguration.value.dbSize)
|
||||
const getTotalMemoryInBytes = computed(
|
||||
() => getTotalMemory.value * SIZE_UNIT_MAP[getTotalMemoryUnit.value],
|
||||
)
|
||||
const getTotalMemoryInKb = computed(
|
||||
() => getTotalMemoryInBytes.value / SIZE_UNIT_MAP['KB'],
|
||||
)
|
||||
const getDBDefaultValues = computed(
|
||||
() => DEFAULT_DB_SETTINGS[getDBVersion] || DEFAULT_DB_VERSION.default,
|
||||
)
|
||||
const getIsConfigured = computed(() => !!getTotalMemory.value)
|
||||
const getMaxConnections = computed(
|
||||
() =>
|
||||
getConnectionNum.value ??
|
||||
{
|
||||
[DB_TYPE_WEB]: 200,
|
||||
[DB_TYPE_OLTP]: 300,
|
||||
[DB_TYPE_DW]: 40,
|
||||
[DB_TYPE_DESKTOP]: 20,
|
||||
[DB_TYPE_MIXED]: 100,
|
||||
}[getDBType.value],
|
||||
)
|
||||
const getSharedBuffers = computed(() => {
|
||||
const totalMemoryKb = getTotalMemoryInKb.value
|
||||
|
||||
const val = {
|
||||
[DB_TYPE_WEB]: Math.floor(totalMemoryKb / 4),
|
||||
[DB_TYPE_OLTP]: Math.floor(totalMemoryKb / 4),
|
||||
[DB_TYPE_DW]: Math.floor(totalMemoryKb / 4),
|
||||
[DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 16),
|
||||
[DB_TYPE_MIXED]: Math.floor(totalMemoryKb / 4),
|
||||
}[getDBType.value]
|
||||
|
||||
return getDBVersion.value < 10 && OS_WINDOWS === getOSType.value
|
||||
? Math.min(val, (512 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'])
|
||||
: val
|
||||
})
|
||||
const getHugePages = computed(() =>
|
||||
getOSType.value !== OS_MAC &&
|
||||
getSharedBuffers.value >= (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB']
|
||||
? 'try'
|
||||
: 'off',
|
||||
)
|
||||
const getEffectiveCacheSize = computed(() => {
|
||||
const totalMemoryKb = getTotalMemoryInKb.value
|
||||
|
||||
return {
|
||||
[DB_TYPE_WEB]: Math.floor((totalMemoryKb * 3) / 4),
|
||||
[DB_TYPE_OLTP]: Math.floor((totalMemoryKb * 3) / 4),
|
||||
[DB_TYPE_DW]: Math.floor((totalMemoryKb * 3) / 4),
|
||||
[DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 4),
|
||||
[DB_TYPE_MIXED]: Math.floor((totalMemoryKb * 3) / 4),
|
||||
}[getDBType.value]
|
||||
})
|
||||
const getMaintenanceWorkMem = computed(() => {
|
||||
const totalMemoryKb = getTotalMemoryInKb.value
|
||||
// 1. Вычисляем базовое значение по типу БД
|
||||
const baseMemValue = {
|
||||
[DB_TYPE_WEB]: Math.floor(totalMemoryKb / 16),
|
||||
[DB_TYPE_OLTP]: Math.floor(totalMemoryKb / 16),
|
||||
[DB_TYPE_DW]: Math.floor(totalMemoryKb / 8),
|
||||
[DB_TYPE_DESKTOP]: Math.floor(totalMemoryKb / 16),
|
||||
[DB_TYPE_MIXED]: Math.floor(totalMemoryKb / 16),
|
||||
}[getDBType.value]
|
||||
|
||||
// 2. Определяем, действует ли строгое ограничение для Windows
|
||||
const isLegacyWindows =
|
||||
OS_WINDOWS === getOSType.value && getDBVersion.value <= 17
|
||||
|
||||
// 3. Задаем лимит памяти (2GB для старых Windows, иначе 8GB)
|
||||
const maxLimitGb = isLegacyWindows ? 2 : 8
|
||||
const memoryLimit = (maxLimitGb * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB']
|
||||
|
||||
// 4. Если превышен лимит на Windows, вычитаем 1MB, иначе просто возвращаем минимум
|
||||
return baseMemValue >= memoryLimit && isLegacyWindows
|
||||
? memoryLimit - (1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB']
|
||||
: Math.min(baseMemValue, memoryLimit)
|
||||
})
|
||||
const getCheckpointSegment = computed(() => [
|
||||
{
|
||||
key: 'min_wal_size',
|
||||
value: {
|
||||
[DB_TYPE_WEB]: (1024 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_OLTP]: (2048 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_DW]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_DESKTOP]: (100 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_MIXED]: (1024 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
}[getDBType.value],
|
||||
},
|
||||
{
|
||||
key: 'max_wal_size',
|
||||
value: {
|
||||
[DB_TYPE_WEB]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_OLTP]: (8192 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_DW]: (16384 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_DESKTOP]: (2048 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
[DB_TYPE_MIXED]: (4096 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB'],
|
||||
}[getDBType.value],
|
||||
},
|
||||
])
|
||||
const getWalBuffers = computed(() => {
|
||||
const KB_IN_MB = SIZE_UNIT_MAP['MB'] / SIZE_UNIT_MAP['KB']
|
||||
const baseWal = Math.floor((3 * sharedBuffersValue) / 100)
|
||||
|
||||
return Math.max(
|
||||
32,
|
||||
baseWal > 14 * KB_IN_MB
|
||||
? 16 * KB_IN_MB
|
||||
: Math.min(baseWal, 16 * KB_IN_MB),
|
||||
)
|
||||
})
|
||||
const getDefaultStatisticsTarget = computed(
|
||||
() =>
|
||||
({
|
||||
[DB_TYPE_WEB]: 100,
|
||||
[DB_TYPE_OLTP]: 100,
|
||||
[DB_TYPE_DW]: 500,
|
||||
[DB_TYPE_DESKTOP]: 100,
|
||||
[DB_TYPE_MIXED]: 100,
|
||||
})[getDBType.value],
|
||||
)
|
||||
const getRandomPageCost = computed(() => {
|
||||
// Если база помещается в RAM, поиск на диске не важен
|
||||
if (getDBSize.value === DB_SIZE_LESS_RAM) return 1.1
|
||||
|
||||
// HDD или аналитические БД (DW), не помещающиеся в RAM, требуют дефолтной стоимости (4.0)
|
||||
if (getHDType.value === HARD_DRIVE_HDD || getDBType.value === DB_TYPE_DW)
|
||||
return 4
|
||||
|
||||
return 1.1
|
||||
})
|
||||
const getEffectiveIOConcurrency = computed(() =>
|
||||
getOSType.value === OS_LINUX
|
||||
? {
|
||||
[HARD_DRIVE_HDD]: 2,
|
||||
[HARD_DRIVE_SSD]: 200,
|
||||
[HARD_DRIVE_SAN]: 300,
|
||||
[HARD_DRIVE_NVME]: 1000,
|
||||
}[getHDType.value]
|
||||
: null,
|
||||
)
|
||||
const getParallelSettings = computed(() => {
|
||||
const cpuNum = getCPUNum.value
|
||||
const dbVersion = getDBVersion.value
|
||||
const dbType = getDBType.value
|
||||
|
||||
if (!cpuNum || cpuNum < 4) return []
|
||||
|
||||
const halfCpu = Math.ceil(cpuNum / 2)
|
||||
const cappedWorkers = Math.min(4, halfCpu) // Ограничение в 4 воркера
|
||||
|
||||
const config = [
|
||||
{ key: 'max_worker_processes', value: cpuNum },
|
||||
{
|
||||
key: 'max_parallel_workers_per_gather',
|
||||
value: dbType !== DB_TYPE_DW ? cappedWorkers : halfCpu,
|
||||
},
|
||||
...(dbVersion >= 10
|
||||
? [{ key: 'max_parallel_workers', value: cpuNum }]
|
||||
: []),
|
||||
...(dbVersion >= 11
|
||||
? [{ key: 'max_parallel_maintenance_workers', value: cappedWorkers }]
|
||||
: []),
|
||||
]
|
||||
|
||||
return config
|
||||
})
|
||||
const getWorkMem = computed(() => {
|
||||
const dbDefaultValues = getDBDefaultValues.value
|
||||
|
||||
const maxWorkerProcesses = getParallelSettings.value.find(
|
||||
(param) => param['key'] === 'max_worker_processes',
|
||||
)
|
||||
|
||||
const parallelForWorkMem =
|
||||
maxWorkerProcesses?.value > 0
|
||||
? maxWorkerProcesses.value
|
||||
: dbDefaultValues['max_worker_processes'] > 0
|
||||
? dbDefaultValues['max_worker_processes']
|
||||
: 1
|
||||
|
||||
// Базовая формула расчёта work_mem
|
||||
const baseWorkMem =
|
||||
(getTotalMemoryInKb.value - sharedBuffersValue) /
|
||||
((maxConnectionsValue + parallelForWorkMem) * 3)
|
||||
|
||||
// Коэффициенты для разных типов БД
|
||||
const dbTypeCoefficients = {
|
||||
[DB_TYPE_WEB]: 1,
|
||||
[DB_TYPE_OLTP]: 1,
|
||||
[DB_TYPE_DW]: 0.5,
|
||||
[DB_TYPE_DESKTOP]: 1 / 6,
|
||||
[DB_TYPE_MIXED]: 0.5,
|
||||
}
|
||||
|
||||
// Базовая формула с коэффициентом типа БД
|
||||
const baseWorkMemResult = Math.floor(
|
||||
baseWorkMem * dbTypeCoefficients[dbType],
|
||||
)
|
||||
|
||||
// Корректировка в зависимости от размера БД относительно RAM
|
||||
const sizeAdjustedWorkMem =
|
||||
dbSize === DB_SIZE_LESS_RAM
|
||||
? Math.floor(baseWorkMemResult * 1.3)
|
||||
: dbSize === DB_SIZE_GREATER_RAM
|
||||
? Math.floor(baseWorkMemResult * 0.9)
|
||||
: baseWorkMemResult
|
||||
|
||||
// Применяем минимальное значение 4 MB
|
||||
const minWorkMem = 4 * 1024 // 4 MB в KB
|
||||
const workMemWithMin = Math.max(sizeAdjustedWorkMem, minWorkMem)
|
||||
|
||||
// Применяем ограничение для Windows 64-bit до PostgreSQL 17
|
||||
const winMemoryLimit = 2 * 1024 * 1024 - 1024 // ~2GB - 1MB в KB
|
||||
const workMemResult =
|
||||
osType === OS_WINDOWS && dbVersion <= 17
|
||||
? Math.min(workMemWithMin, winMemoryLimit)
|
||||
: workMemWithMin
|
||||
|
||||
return workMemResult
|
||||
})
|
||||
const getWalLevel = computed(() =>
|
||||
getDBType.value === DB_TYPE_DESKTOP
|
||||
? [
|
||||
{
|
||||
key: 'wal_level',
|
||||
value: 'minimal',
|
||||
},
|
||||
// max_wal_senders must be 0 when wal_level=minimal
|
||||
{
|
||||
key: 'max_wal_senders',
|
||||
value: '0',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
const getJit = computed(() =>
|
||||
getDBVersion.value >= 12 &&
|
||||
[DB_TYPE_WEB, DB_TYPE_OLTP, DB_TYPE_MIXED].includes(getDBType.value)
|
||||
? 'off'
|
||||
: null,
|
||||
)
|
||||
const getWalCompression = computed(() => {
|
||||
const ver = getDBVersion.value
|
||||
|
||||
return ver >= 15 ? 'lz4' : ver >= 10 ? 'on' : null
|
||||
})
|
||||
const getAutovacuumMaxWorkers = computed(() => {
|
||||
const cpuNum = getCPUNum.value
|
||||
|
||||
if (!cpuNum) return null
|
||||
if (cpuNum >= 32) return 5
|
||||
if (cpuNum >= 16) return 4
|
||||
return null
|
||||
})
|
||||
const getAutovacuumWorkMem = computed(() => {
|
||||
const threshold = (2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB']
|
||||
|
||||
// Windows 64-bit has a strict 2GB limit up to PostgreSQL 17
|
||||
const winMemoryLimit =
|
||||
(2 * SIZE_UNIT_MAP['GB']) / SIZE_UNIT_MAP['KB'] -
|
||||
(1 * SIZE_UNIT_MAP['MB']) / SIZE_UNIT_MAP['KB']
|
||||
|
||||
const isWindowsWithLowLimit =
|
||||
getOSType.value === OS_WINDOWS && getDBVersion.value <= 17
|
||||
|
||||
if (getMaintenanceWorkMem.value < threshold) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isWindowsWithLowLimit) {
|
||||
return Math.min(threshold, winMemoryLimit)
|
||||
}
|
||||
|
||||
return threshold
|
||||
})
|
||||
const getIOMethod = computed(() =>
|
||||
getDBVersion.value < 18
|
||||
? null
|
||||
: getOSType.value === OS_LINUX
|
||||
? 'io_uring'
|
||||
: 'worker',
|
||||
)
|
||||
const getIOWorkers = computed(() => {
|
||||
if (dbVersion < 18 || !cpuNum || ioMethod === 'io_uring') return null
|
||||
|
||||
return Math.max(4, Math.min(32, Math.floor(cpuNum / 4))) || null
|
||||
})
|
||||
const getWarningInfoMessages = computed(() => {
|
||||
const warnings = []
|
||||
const totalMemory = getTotalMemoryInBytes.value
|
||||
|
||||
// Memory warnings
|
||||
if (totalMemory < 256 * SIZE_UNIT_MAP['MB']) {
|
||||
warnings.push('this tool not being optimal', 'for low memory systems')
|
||||
} else if (totalMemory > 100 * SIZE_UNIT_MAP['GB']) {
|
||||
warnings.push(
|
||||
'this tool not being optimal',
|
||||
'for very high memory systems',
|
||||
)
|
||||
}
|
||||
|
||||
// Advanced features compilation warnings
|
||||
if (getWalCompression.value === 'lz4') {
|
||||
if (warnings.length > 0) warnings.push('')
|
||||
|
||||
warnings.push(
|
||||
'wal_compression = lz4 requires PostgreSQL',
|
||||
'to be compiled with --with-lz4',
|
||||
)
|
||||
}
|
||||
|
||||
if (getIOMethod.value === 'io_uring') {
|
||||
if (warnings.length > 0) warnings.push('')
|
||||
|
||||
warnings.push(
|
||||
'io_method = io_uring requires PostgreSQL',
|
||||
'to be compiled with --with-liburing',
|
||||
)
|
||||
}
|
||||
|
||||
// I/O Cost Warning for Analytical DBs
|
||||
if (
|
||||
getDBType.value === DB_TYPE_DW &&
|
||||
getHDType.value !== HARD_DRIVE_HDD &&
|
||||
getDBSize.value !== DB_SIZE_LESS_RAM
|
||||
) {
|
||||
const driveName = {
|
||||
[HARD_DRIVE_SSD]: 'SSDs',
|
||||
[HARD_DRIVE_NVME]: 'NVMe drives',
|
||||
[HARD_DRIVE_SAN]: 'SAN storage',
|
||||
}[hdType]
|
||||
|
||||
if (warnings.length > 0) warnings.push('')
|
||||
|
||||
warnings.push(
|
||||
`Cost parameters for Data Warehouses on ${driveName} are left at defaults`,
|
||||
'to avoid catastrophic index scan selections',
|
||||
'Monitor query planner behavior and adjust random_page_cost if necessary',
|
||||
)
|
||||
}
|
||||
|
||||
return warnings.length > 0 ? ['WARNING', ...warnings] : []
|
||||
})
|
||||
|
||||
function setState(payload) {
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
state.value = {
|
||||
dbVersion: parseFloat(payload.dbVersion),
|
||||
osType: payload.osType,
|
||||
dbType: payload.dbType,
|
||||
totalMemory: parseInt(payload.totalMemory, 10),
|
||||
totalMemoryUnit: payload.totalMemoryUnit,
|
||||
cpuNum: payload.cpuNum ? parseInt(payload.cpuNum, 10) : null,
|
||||
connectionNum: payload.connectionNum
|
||||
? parseInt(payload.connectionNum, 10)
|
||||
: null,
|
||||
hdType: payload.hdType,
|
||||
dbSize: payload.dbSize,
|
||||
}
|
||||
|
||||
settingsStore.onSubmitConfiguration()
|
||||
}
|
||||
|
||||
const $reset = () => ({ ...initialState })
|
||||
|
||||
return {
|
||||
state,
|
||||
|
||||
// Getters
|
||||
getDBVersion,
|
||||
getOSType,
|
||||
getDBType,
|
||||
getTotalMemory,
|
||||
getTotalMemoryUnit,
|
||||
getCPUNum,
|
||||
getConnectionNum,
|
||||
getHDType,
|
||||
getDBSize,
|
||||
getTotalMemoryInBytes,
|
||||
getTotalMemoryInKb,
|
||||
getDBDefaultValues,
|
||||
getIsConfigured,
|
||||
getMaxConnections,
|
||||
getSharedBuffers,
|
||||
getHugePages,
|
||||
getEffectiveCacheSize,
|
||||
getMaintenanceWorkMem,
|
||||
getCheckpointSegment,
|
||||
getWalBuffers,
|
||||
getDefaultStatisticsTarget,
|
||||
getRandomPageCost,
|
||||
getEffectiveIOConcurrency,
|
||||
getParallelSettings,
|
||||
getWorkMem,
|
||||
getWalLevel,
|
||||
getJit,
|
||||
getWalCompression,
|
||||
getAutovacuumMaxWorkers,
|
||||
getAutovacuumWorkMem,
|
||||
getIOMethod,
|
||||
getIOWorkers,
|
||||
getWarningInfoMessages,
|
||||
|
||||
// Actions
|
||||
setState,
|
||||
$reset,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, computed } from 'vue'
|
||||
|
||||
import { TAB_CONFIG, APP_THEMES_LIGHT } from '/src/utils/constants/settings.js'
|
||||
|
||||
const initialState = {
|
||||
tabState: TAB_CONFIG,
|
||||
theme: APP_THEMES_LIGHT,
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const state = reactive({ ...initialState })
|
||||
|
||||
const getSettings = computed(() => state)
|
||||
const getThemeSettings = computed(() => state.theme)
|
||||
const getTabSettings = computed(() => state.tabState)
|
||||
|
||||
function toggleTheme() {
|
||||
state.theme =
|
||||
APP_THEMES_LIGHT === state.theme ? APP_THEMES_DARK : APP_THEMES_LIGHT
|
||||
}
|
||||
function onSubmitConfiguration() {
|
||||
state.tabState = TAB_CONFIG
|
||||
}
|
||||
function $reset() {
|
||||
state = { ...initialState }
|
||||
}
|
||||
return {
|
||||
// STATE
|
||||
state,
|
||||
|
||||
// GETTERS
|
||||
getSettings,
|
||||
getThemeSettings,
|
||||
getTabSettings,
|
||||
toggleTheme,
|
||||
|
||||
// ACTIONS
|
||||
onSubmitConfiguration,
|
||||
$reset,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useConfigurationStore = defineStore('sw', () => {
|
||||
const state = ref()
|
||||
|
||||
return { state }
|
||||
})
|
||||
Reference in New Issue
Block a user