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, "*") {
|
if strings.HasSuffix(key, "*") {
|
||||||
// delete by prefix
|
// delete by prefix
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
|
supportUnlink := true
|
||||||
del := func(ctx context.Context, cli redis.UniversalClient) error {
|
del := func(ctx context.Context, cli redis.UniversalClient) error {
|
||||||
handleDel := func(ks []string) error {
|
handleDel := func(ks []string) error {
|
||||||
pipe := cli.Pipeline()
|
var delErr error
|
||||||
for _, k2 := range ks {
|
if async && supportUnlink {
|
||||||
if async {
|
supportUnlink = false
|
||||||
cli.Unlink(ctx, k2)
|
if delErr = cli.Unlink(ctx, ks...).Err(); delErr != nil {
|
||||||
} else {
|
// not support unlink? try del command
|
||||||
cli.Del(ctx, k2)
|
delErr = cli.Del(ctx, ks...).Err()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
delErr = cli.Del(ctx, ks...).Err()
|
||||||
}
|
}
|
||||||
pipe.Exec(ctx)
|
|
||||||
|
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
deletedKeys = append(deletedKeys, ks...)
|
deletedKeys = append(deletedKeys, ks...)
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return delErr
|
||||||
}
|
}
|
||||||
|
|
||||||
scanSize := int64(Preferences().GetScanSize())
|
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)
|
resultKeys := make([]string, 0, 100)
|
||||||
for iter.Next(ctx) {
|
for iter.Next(ctx) {
|
||||||
resultKeys = append(resultKeys, iter.Val())
|
resultKeys = append(resultKeys, iter.Val())
|
||||||
if len(resultKeys) >= 3 {
|
if len(resultKeys) >= 20 {
|
||||||
handleDel(resultKeys)
|
handleDel(resultKeys)
|
||||||
resultKeys = resultKeys[:0:cap(resultKeys)]
|
resultKeys = resultKeys[:0:cap(resultKeys)]
|
||||||
}
|
}
|
||||||
|
@ -1574,12 +1576,14 @@ func (b *browserService) DeleteKey(connName string, db int, k any, async bool) (
|
||||||
} else {
|
} else {
|
||||||
// delete key only
|
// delete key only
|
||||||
if async {
|
if async {
|
||||||
if _, err = client.Unlink(ctx, key).Result(); err != nil {
|
if err = client.Unlink(ctx, key).Err(); err != nil {
|
||||||
resp.Msg = err.Error()
|
if err = client.Del(ctx, key).Err(); err != nil {
|
||||||
return
|
resp.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err = client.Del(ctx, key).Result(); err != nil {
|
if err = client.Del(ctx, key).Err(); err != nil {
|
||||||
resp.Msg = err.Error()
|
resp.Msg = err.Error()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1603,8 +1607,8 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
flush := func(ctx context.Context, cli redis.UniversalClient) {
|
flush := func(ctx context.Context, cli redis.UniversalClient, async bool) error {
|
||||||
cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
|
_, e := cli.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
|
||||||
pipe.Select(ctx, db)
|
pipe.Select(ctx, db)
|
||||||
if async {
|
if async {
|
||||||
pipe.FlushDBAsync(ctx)
|
pipe.FlushDBAsync(ctx)
|
||||||
|
@ -1613,17 +1617,26 @@ func (b *browserService) FlushDB(connName string, db int, async bool) (resp type
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
client, ctx := item.client, item.ctx
|
client, ctx := item.client, item.ctx
|
||||||
if cluster, ok := client.(*redis.ClusterClient); ok {
|
if cluster, ok := client.(*redis.ClusterClient); ok {
|
||||||
// cluster mode
|
// cluster mode
|
||||||
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
|
err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
|
||||||
flush(ctx, cli)
|
return flush(ctx, cli, async)
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
// 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 {
|
} 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 {
|
if err != nil {
|
||||||
|
|
|
@ -135,7 +135,7 @@ onMounted(async () => {
|
||||||
paddingLeft: `${logoPaddingLeft}px`,
|
paddingLeft: `${logoPaddingLeft}px`,
|
||||||
}">
|
}">
|
||||||
<n-space :size="3" :wrap="false" :wrap-item="false" align="center">
|
<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>
|
<div style="min-width: 68px; font-weight: 800">Tiny RDM</div>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">
|
<n-text v-if="tabStore.nav === 'browser'" class="ellipsis" strong style="font-size: 13px">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import useDialog from 'stores/dialog'
|
import useDialog from 'stores/dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { isEmpty, size } from 'lodash'
|
import { isEmpty, size } from 'lodash'
|
||||||
|
@ -29,18 +29,22 @@ watch(
|
||||||
deleteForm.loadingAffected = false
|
deleteForm.loadingAffected = false
|
||||||
deleteForm.affectedKeys = []
|
deleteForm.affectedKeys = []
|
||||||
deleteForm.async = true
|
deleteForm.async = true
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
const scanAffectedKey = async () => {
|
const scanAffectedKey = async () => {
|
||||||
try {
|
try {
|
||||||
|
loading.value = true
|
||||||
deleteForm.loadingAffected = true
|
deleteForm.loadingAffected = true
|
||||||
const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
|
const { keys = [] } = await browserStore.scanKeys(deleteForm.server, deleteForm.db, deleteForm.key)
|
||||||
deleteForm.affectedKeys = keys || []
|
deleteForm.affectedKeys = keys || []
|
||||||
deleteForm.showAffected = true
|
deleteForm.showAffected = true
|
||||||
} finally {
|
} finally {
|
||||||
deleteForm.loadingAffected = false
|
deleteForm.loadingAffected = false
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +56,7 @@ const resetAffected = () => {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const onConfirmDelete = async () => {
|
const onConfirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
|
loading.value = true
|
||||||
const { server, db, key, async } = deleteForm
|
const { server, db, key, async } = deleteForm
|
||||||
const success = await browserStore.deleteKeyPrefix(server, db, key, async)
|
const success = await browserStore.deleteKeyPrefix(server, db, key, async)
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -59,6 +64,9 @@ const onConfirmDelete = async () => {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$message.error(e.message)
|
$message.error(e.message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
dialogStore.closeDeleteKeyDialog()
|
dialogStore.closeDeleteKeyDialog()
|
||||||
}
|
}
|
||||||
|
@ -78,40 +86,53 @@ const onClose = () => {
|
||||||
:title="$t('interface.batch_delete_key')"
|
:title="$t('interface.batch_delete_key')"
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center">
|
transform-origin="center">
|
||||||
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
<n-spin :show="loading">
|
||||||
<n-form-item :label="$t('dialogue.key.server')">
|
<n-form :model="deleteForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-input :value="deleteForm.server" readonly />
|
<n-form-item :label="$t('dialogue.key.server')">
|
||||||
</n-form-item>
|
<n-input :value="deleteForm.server" readonly />
|
||||||
<n-form-item :label="$t('dialogue.key.db_index')">
|
</n-form-item>
|
||||||
<n-input :value="deleteForm.db.toString()" readonly />
|
<n-form-item :label="$t('dialogue.key.db_index')">
|
||||||
</n-form-item>
|
<n-input :value="deleteForm.db.toString()" readonly />
|
||||||
<n-form-item :label="$t('dialogue.key.key_expression')" required>
|
</n-form-item>
|
||||||
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
<n-form-item :label="$t('dialogue.key.key_expression')" required>
|
||||||
</n-form-item>
|
<n-input v-model:value="deleteForm.key" placeholder="" @input="resetAffected" />
|
||||||
<n-form-item :label="$t('dialogue.key.async_delete')" required>
|
</n-form-item>
|
||||||
<n-checkbox v-model:checked="deleteForm.async">{{ $t('dialogue.key.async_delete_title') }}</n-checkbox>
|
<n-form-item :label="$t('dialogue.key.async_delete')" required>
|
||||||
</n-form-item>
|
<n-checkbox v-model:checked="deleteForm.async">
|
||||||
<n-card v-if="deleteForm.showAffected" :title="$t('dialogue.key.affected_key')" size="small">
|
{{ $t('dialogue.key.async_delete_title') }}
|
||||||
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
|
</n-checkbox>
|
||||||
<n-log
|
</n-form-item>
|
||||||
v-else
|
<n-card
|
||||||
:line-height="1.5"
|
v-if="deleteForm.showAffected"
|
||||||
:lines="deleteForm.affectedKeys"
|
:title="$t('dialogue.key.affected_key') + `(${size(deleteForm.affectedKeys)})`"
|
||||||
:rows="10"
|
size="small">
|
||||||
style="user-select: text; cursor: text" />
|
<n-skeleton v-if="deleteForm.loadingAffected" :repeat="10" text />
|
||||||
</n-card>
|
<n-log
|
||||||
</n-form>
|
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>
|
<template #action>
|
||||||
<div class="flex-item n-dialog__action">
|
<div class="flex-item n-dialog__action">
|
||||||
<n-button :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
||||||
<n-button v-if="!deleteForm.showAffected" :focusable="false" type="primary" @click="scanAffectedKey">
|
<n-button
|
||||||
|
v-if="!deleteForm.showAffected"
|
||||||
|
:focusable="false"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
@click="scanAffectedKey">
|
||||||
{{ $t('dialogue.key.show_affected_key') }}
|
{{ $t('dialogue.key.show_affected_key') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-else
|
v-else
|
||||||
:disabled="isEmpty(deleteForm.affectedKeys)"
|
:disabled="isEmpty(deleteForm.affectedKeys)"
|
||||||
:focusable="false"
|
:focusable="false"
|
||||||
|
:loading="loading"
|
||||||
type="error"
|
type="error"
|
||||||
@click="onConfirmDelete">
|
@click="onConfirmDelete">
|
||||||
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
{{ $t('dialogue.key.confirm_delete_key', { num: size(deleteForm.affectedKeys) }) }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import useDialog from 'stores/dialog'
|
import useDialog from 'stores/dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import useBrowserStore from 'stores/browser.js'
|
import useBrowserStore from 'stores/browser.js'
|
||||||
|
@ -23,13 +23,16 @@ watch(
|
||||||
flushForm.db = db
|
flushForm.db = db
|
||||||
flushForm.async = true
|
flushForm.async = true
|
||||||
flushForm.confirm = false
|
flushForm.confirm = false
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const onConfirmFlush = async () => {
|
const onConfirmFlush = async () => {
|
||||||
try {
|
try {
|
||||||
|
loading.value = true
|
||||||
const { server, db, async } = flushForm
|
const { server, db, async } = flushForm
|
||||||
const success = await browserStore.flushDatabase(server, db, async)
|
const success = await browserStore.flushDatabase(server, db, async)
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -37,6 +40,9 @@ const onConfirmFlush = async () => {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$message.error(e.message)
|
$message.error(e.message)
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
dialogStore.closeFlushDBDialog()
|
dialogStore.closeFlushDBDialog()
|
||||||
}
|
}
|
||||||
|
@ -56,26 +62,35 @@ const onClose = () => {
|
||||||
:title="$t('interface.flush_db')"
|
:title="$t('interface.flush_db')"
|
||||||
preset="dialog"
|
preset="dialog"
|
||||||
transform-origin="center">
|
transform-origin="center">
|
||||||
<n-form :model="flushForm" :show-require-mark="false" label-placement="top">
|
<n-spin :show="loading">
|
||||||
<n-form-item :label="$t('dialogue.key.server')">
|
<n-form :model="flushForm" :show-require-mark="false" label-placement="top">
|
||||||
<n-input :value="flushForm.server" readonly />
|
<n-form-item :label="$t('dialogue.key.server')">
|
||||||
</n-form-item>
|
<n-input :value="flushForm.server" readonly />
|
||||||
<n-form-item :label="$t('dialogue.key.db_index')">
|
</n-form-item>
|
||||||
<n-input :value="flushForm.db.toString()" readonly />
|
<n-form-item :label="$t('dialogue.key.db_index')">
|
||||||
</n-form-item>
|
<n-input :value="flushForm.db.toString()" readonly />
|
||||||
<n-form-item :label="$t('dialogue.key.async_delete')" required>
|
</n-form-item>
|
||||||
<n-checkbox v-model:checked="flushForm.async">{{ $t('dialogue.key.async_delete_title') }}</n-checkbox>
|
<n-form-item :label="$t('dialogue.key.async_delete')" required>
|
||||||
</n-form-item>
|
<n-checkbox v-model:checked="flushForm.async">
|
||||||
<n-form-item :label="$t('common.warning')" required>
|
{{ $t('dialogue.key.async_delete_title') }}
|
||||||
<n-checkbox v-model:checked="flushForm.confirm">
|
</n-checkbox>
|
||||||
<span style="color: red; font-weight: bold">{{ $t('dialogue.key.confirm_flush') }}</span>
|
</n-form-item>
|
||||||
</n-checkbox>
|
<n-form-item :label="$t('common.warning')" required>
|
||||||
</n-form-item>
|
<n-checkbox v-model:checked="flushForm.confirm">
|
||||||
</n-form>
|
<span style="color: red; font-weight: bold">{{ $t('dialogue.key.confirm_flush') }}</span>
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-spin>
|
||||||
|
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
<n-button :disabled="loading" :focusable="false" @click="onClose">{{ $t('common.cancel') }}</n-button>
|
||||||
<n-button :disabled="!!!flushForm.confirm" :focusable="false" type="primary" @click="onConfirmFlush">
|
<n-button
|
||||||
|
:disabled="!!!flushForm.confirm"
|
||||||
|
:focusable="false"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
@click="onConfirmFlush">
|
||||||
{{ $t('dialogue.key.confirm_flush_db') }}
|
{{ $t('dialogue.key.confirm_flush_db') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue