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:
parent
5a58a57cd5
commit
65e077c0c0
|
@ -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)
|
||||
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 {
|
||||
cli.Del(ctx, k2)
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,6 +86,7 @@ const onClose = () => {
|
|||
:title="$t('interface.batch_delete_key')"
|
||||
preset="dialog"
|
||||
transform-origin="center">
|
||||
<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 />
|
||||
|
@ -89,9 +98,14 @@ const onClose = () => {
|
|||
<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-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-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
|
||||
|
@ -101,17 +115,24 @@ const onClose = () => {
|
|||
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) }) }}
|
||||
|
|
|
@ -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,6 +62,7 @@ const onClose = () => {
|
|||
:title="$t('interface.flush_db')"
|
||||
preset="dialog"
|
||||
transform-origin="center">
|
||||
<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 />
|
||||
|
@ -64,7 +71,9 @@ const onClose = () => {
|
|||
<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-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">
|
||||
|
@ -72,10 +81,16 @@ const onClose = () => {
|
|||
</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>
|
||||
|
|
Loading…
Reference in New Issue