fix: compatibility with older Redis versions without UNLINK command support #69

perf: add waiting indicator for deleting keys and flushing database
This commit is contained in:
tiny-craft 2023-11-10 15:57:19 +08:00
parent 5a58a57cd5
commit 65e077c0c0
4 changed files with 113 additions and 64 deletions

View File

@ -1522,23 +1522,25 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
if strings.HasSuffix(key, "*") {
// delete by prefix
var mutex sync.Mutex
supportUnlink := true
del := func(ctx context.Context, cli redis.UniversalClient) error {
handleDel := func(ks []string) error {
pipe := cli.Pipeline()
for _, k2 := range ks {
if async {
cli.Unlink(ctx, k2)
} else {
cli.Del(ctx, k2)
var delErr error
if async && supportUnlink {
supportUnlink = false
if delErr = cli.Unlink(ctx, ks...).Err(); delErr != nil {
// not support unlink? try del command
delErr = cli.Del(ctx, ks...).Err()
}
} else {
delErr = cli.Del(ctx, ks...).Err()
}
pipe.Exec(ctx)
mutex.Lock()
deletedKeys = append(deletedKeys, ks...)
mutex.Unlock()
return nil
return delErr
}
scanSize := int64(Preferences().GetScanSize())
@ -1546,7 +1548,7 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
resultKeys := make([]string, 0, 100)
for iter.Next(ctx) {
resultKeys = append(resultKeys, iter.Val())
if len(resultKeys) >= 3 {
if len(resultKeys) >= 20 {
handleDel(resultKeys)
resultKeys = resultKeys[:0:cap(resultKeys)]
}
@ -1574,12 +1576,14 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
} else {
// delete key only
if async {
if _, err = client.Unlink(ctx, key).Result(); err != nil {
resp.Msg = err.Error()
return
if err = client.Unlink(ctx, key).Err(); err != nil {
if err = client.Del(ctx, key).Err(); err != nil {
resp.Msg = err.Error()
return
}
}
} else {
if _, err = client.Del(ctx, key).Result(); err != nil {
if err = client.Del(ctx, key).Err(); err != nil {
resp.Msg = err.Error()
return
}
@ -1603,8 +1607,8 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
return
}
flush := func(ctx context.Context, cli redis.UniversalClient) {
cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
flush := func(ctx context.Context, cli redis.UniversalClient, async bool) error {
_, e := cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Select(ctx, db)
if async {
pipe.FlushDBAsync(ctx)
@ -1613,17 +1617,26 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
}
return nil
})
return e
}
client, ctx := item.client, item.ctx
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
flush(ctx, cli)
return nil
return flush(ctx, cli, async)
})
// try sync mode if error cause
if err != nil && async {
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
return flush(ctx, cli, false)
})
}
} else {
flush(ctx, client)
if err = flush(ctx, client, async); err != nil && async {
// try sync mode if error cause
err = flush(ctx, client, false)
}
}
if err != nil {

View File

@ -135,7 +135,7 @@ onMounted(async () => {
paddingLeft: `${logoPaddingLeft}px`,
}">
<n-space :size="3" :wrap="false" :wrap-item="false" align="center">
<n-avatar :size="35" :src="iconUrl" color="#0000" style="min-width: 35px" />
<n-avatar :size="32" :src="iconUrl" color="#0000" style="min-width: 32px" />
<div style="min-width: 68px; font-weight: 800">Tiny RDM</div>
<transition name="fade">
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">

View File

@ -1,5 +1,5 @@
<script setup>
import { reactive, watch } from 'vue'
import { reactive, ref, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import { isEmpty, size } from 'lodash'
@ -29,18 +29,22 @@ watch(
deleteForm.loadingAffected = false
deleteForm.affectedKeys = []
deleteForm.async = true
loading.value = false
}
},
)
const loading = ref(false)
const scanAffectedKey = async () => {
try {
loading.value = true
deleteForm.loadingAffected = true
const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
deleteForm.affectedKeys = keys || []
deleteForm.showAffected = true
} finally {
deleteForm.loadingAffected = false
loading.value = false
}
}
@ -52,6 +56,7 @@ const resetAffected = () => {
const i18n = useI18n()
const onConfirmDelete = async () => {
try {
loading.value = true
const { server, db, key, async } = deleteForm
const success = await browserStore.deleteKeyPrefix(server, db, key, async)
if (success) {
@ -59,6 +64,9 @@ const onConfirmDelete = async () => {
}
} catch (e) {
$message.error(e.message)
return
} finally {
loading.value = false
}
dialogStore.closeDeleteKeyDialog()
}
@ -78,40 +86,53 @@ const onClose = () => {
:title="$t('interface.batch_delete_key')"
preset="dialog"
transform-origin="center">
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('dialogue.key.server')">
<n-input :value="deleteForm.server" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.db_index')">
<n-input :value="deleteForm.db.toString()" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.key_expression')" required>
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item>
<n-form-item :label="$t('dialogue.key.async_delete')" required>
<n-checkbox v-model:checked="deleteForm.async">{{ $t('dialogue.key.async_delete_title') }}</n-checkbox>
</n-form-item>
<n-card v-if="deleteForm.showAffected" :title="$t('dialogue.key.affected_key')" size="small">
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
<n-log
v-else
:line-height="1.5"
:lines="deleteForm.affectedKeys"
:rows="10"
style="user-select: text; cursor: text" />
</n-card>
</n-form>
<n-spin :show="loading">
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('dialogue.key.server')">
<n-input :value="deleteForm.server" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.db_index')">
<n-input :value="deleteForm.db.toString()" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.key_expression')" required>
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
</n-form-item>
<n-form-item :label="$t('dialogue.key.async_delete')" required>
<n-checkbox v-model:checked="deleteForm.async">
{{ $t('dialogue.key.async_delete_title') }}
</n-checkbox>
</n-form-item>
<n-card
v-if="deleteForm.showAffected"
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
size="small">
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
<n-log
v-else
:line-height="1.5"
:lines="deleteForm.affectedKeys"
:rows="10"
style="user-select: text; cursor: text" />
</n-card>
</n-form>
</n-spin>
<template #action>
<div class="flex-item n-dialog__action">
<n-button :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button v-if="!deleteForm.showAffected" :focusable="false" type="primary" @click="scanAffectedKey">
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button
v-if="!deleteForm.showAffected"
:focusable="false"
:loading="loading"
type="primary"
@click="scanAffectedKey">
{{ $t('dialogue.key.show_affected_key') }}
</n-button>
<n-button
v-else
:disabled="isEmpty(deleteForm.affectedKeys)"
:focusable="false"
:loading="loading"
type="error"
@click="onConfirmDelete">
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}

View File

@ -1,5 +1,5 @@
<script setup>
import { reactive, watch } from 'vue'
import { reactive, ref, watch } from 'vue'
import useDialog from 'stores/dialog'
import { useI18n } from 'vue-i18n'
import useBrowserStore from 'stores/browser.js'
@ -23,13 +23,16 @@ watch(
flushForm.db = db
flushForm.async = true
flushForm.confirm = false
loading.value = false
}
},
)
const loading = ref(false)
const i18n = useI18n()
const onConfirmFlush = async () => {
try {
loading.value = true
const { server, db, async } = flushForm
const success = await browserStore.flushDatabase(server, db, async)
if (success) {
@ -37,6 +40,9 @@ const onConfirmFlush = async () => {
}
} catch (e) {
$message.error(e.message)
return
} finally {
loading.value = false
}
dialogStore.closeFlushDBDialog()
}
@ -56,26 +62,35 @@ const onClose = () => {
:title="$t('interface.flush_db')"
preset="dialog"
transform-origin="center">
<n-form :model="flushForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('dialogue.key.server')">
<n-input :value="flushForm.server" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.db_index')">
<n-input :value="flushForm.db.toString()" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.async_delete')" required>
<n-checkbox v-model:checked="flushForm.async">{{ $t('dialogue.key.async_delete_title') }}</n-checkbox>
</n-form-item>
<n-form-item :label="$t('common.warning')" required>
<n-checkbox v-model:checked="flushForm.confirm">
<span style="color: red; font-weight: bold">{{ $t('dialogue.key.confirm_flush') }}</span>
</n-checkbox>
</n-form-item>
</n-form>
<n-spin :show="loading">
<n-form :model="flushForm" :show-require-mark="false" label-placement="top">
<n-form-item :label="$t('dialogue.key.server')">
<n-input :value="flushForm.server" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.db_index')">
<n-input :value="flushForm.db.toString()" readonly />
</n-form-item>
<n-form-item :label="$t('dialogue.key.async_delete')" required>
<n-checkbox v-model:checked="flushForm.async">
{{ $t('dialogue.key.async_delete_title') }}
</n-checkbox>
</n-form-item>
<n-form-item :label="$t('common.warning')" required>
<n-checkbox v-model:checked="flushForm.confirm">
<span style="color: red; font-weight: bold">{{ $t('dialogue.key.confirm_flush') }}</span>
</n-checkbox>
</n-form-item>
</n-form>
</n-spin>
<template #action>
<n-button :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button :disabled="!!!flushForm.confirm" :focusable="false" type="primary" @click="onConfirmFlush">
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
<n-button
:disabled="!!!flushForm.confirm"
:focusable="false"
:loading="loading"
type="primary"
@click="onConfirmFlush">
{{ $t('dialogue.key.confirm_flush_db') }}
</n-button>
</template>