greenplumn runaway_cleaner_test 源码
greenplumn runaway_cleaner_test 代码
文件路径:/src/backend/utils/mmgr/test/runaway_cleaner_test.c
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include "cmockery.h"
#include "../runaway_cleaner.c"
#define EXPECT_EREPORT(LOG_LEVEL) \
expect_any(errstart, elevel); \
expect_any(errstart, domain); \
if (LOG_LEVEL < ERROR) \
{ \
will_return(errstart, false); \
} \
else \
{ \
will_return_with_sideeffect(errstart, false, &_ExceptionalCondition, NULL); \
}
#define CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING() \
will_be_called(write_stderr); \
expect_any(write_stderr, fmt); \
will_be_called(MemoryContextStats); \
expect_any(MemoryContextStats, context); \
/* MySessionState will use the address of this global variable */
static SessionState fakeSessionState;
/* Prepares a fake MySessionState pointer for use in the vmem tracker */
static void
InitFakeSessionState(int activeProcessCount, int cleanupCountdown, RunawayStatus runawayStatus, int pinCount, int vmem)
{
MySessionState = &fakeSessionState;
MySessionState->activeProcessCount = activeProcessCount;
MySessionState->cleanupCountdown = cleanupCountdown;
MySessionState->runawayStatus = runawayStatus;
MySessionState->next = NULL;
MySessionState->pinCount = pinCount;
MySessionState->sessionId = 1234;
MySessionState->sessionVmem = vmem;
MySessionState->spinLock = 0;
}
#undef PG_RE_THROW
#define PG_RE_THROW() siglongjmp(*PG_exception_stack, 1)
/*
* This method will emulate the real ExceptionalCondition
* function by re-throwing the exception, essentially falling
* back to the next available PG_CATCH();
*/
static void
_ExceptionalCondition()
{
PG_RE_THROW();
}
/*
* Checks if RunawayCleaner_StartCleanup() does not start cleanup if
* the current session is not a runaway
*/
static void
test__RunawayCleaner_StartCleanup__IgnoresNonRunaway(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
CLEANUP_COUNTDOWN_BEFORE_RUNAWAY /* cleanupCountdown */,
RunawayStatus_NotRunaway /* runawayStatus */, 2 /* pinCount */, 0 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
beginCleanupRunawayVersion = 0;
RunawayCleaner_StartCleanup();
/* Cleanup shouldn't have begun */
assert_true(beginCleanupRunawayVersion != *latestRunawayVersion);
}
/*
* Checks if RunawayCleaner_StartCleanup() does not execute a duplicate
* cleanup for the same runaway event that it already started cleaning up
*/
static void
test__RunawayCleaner_StartCleanup__IgnoresDuplicateCleanup(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 0 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
beginCleanupRunawayVersion = *latestRunawayVersion;
/*
* As we are not providing IsCommitInProgress, the call itself verifies
* that we are not attempting any cleanup
*/
RunawayCleaner_StartCleanup();
}
/*
* Checks if RunawayCleaner_StartCleanup() starts the cleanup process if
* all conditions are met (i.e., no commit is in progress and vmem tracker
* is initialized) and runaway session is "primary"
*/
static void
test__RunawayCleaner_StartCleanup__StartsPrimaryCleanupIfPossible(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to less than *latestRunawayVersion
* to trigger a cleanup
*/
beginCleanupRunawayVersion = 1;
endCleanupRunawayVersion = 1;
isProcessActive = true;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
CritSectionCount = 0;
InterruptHoldoffCount = 0;
/* We need a valid gp_command_count to execute cleanup */
gp_command_count = 1;
will_return(IsTransactionState, true);
EXPECT_EREPORT(ERROR);
PG_TRY();
{
RunawayCleaner_StartCleanup();
assert_false("Cleanup didn't throw error");
}
PG_CATCH();
{
}
PG_END_TRY();
assert_true(beginCleanupRunawayVersion == *latestRunawayVersion);
/* We should not finish the cleanup as we errored out */
assert_true(endCleanupRunawayVersion == 1);
/* cleanupCountdown shouldn't change as we haven't finished cleanup */
assert_true(MySessionState->cleanupCountdown == 2);
/*
* If we call RunawayCleaner_StartCleanup again for the same runaway event,
* it should be a noop, therefore requiring no "will_be_called" setup
*/
RunawayCleaner_StartCleanup();
}
/*
* Checks if RunawayCleaner_StartCleanup() starts the cleanup process if
* all conditions are met (i.e., no commit is in progress and vmem tracker
* is initialized) and runaway session is "secondary"
*/
static void
test__RunawayCleaner_StartCleanup__StartsSecondaryCleanupIfPossible(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_SecondaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to less than *latestRunawayVersion
* to trigger a cleanup
*/
beginCleanupRunawayVersion = 1;
endCleanupRunawayVersion = 1;
isProcessActive = true;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
CritSectionCount = 0;
InterruptHoldoffCount = 0;
/* We need a valid gp_command_count to execute cleanup */
gp_command_count = 1;
will_return(superuser, false);
will_return(IsTransactionState, true);
EXPECT_EREPORT(ERROR);
PG_TRY();
{
RunawayCleaner_StartCleanup();
assert_false("Cleanup didn't throw error");
}
PG_CATCH();
{
}
PG_END_TRY();
assert_true(beginCleanupRunawayVersion == *latestRunawayVersion);
/* We should not finish the cleanup as we errored out */
assert_true(endCleanupRunawayVersion == 1);
/* cleanupCountdown shouldn't change as we haven't finished cleanup */
assert_true(MySessionState->cleanupCountdown == 2);
/*
* If we call RunawayCleaner_StartCleanup again for the same runaway event,
* it should be a noop, therefore requiring no "will_be_called" setup
*/
RunawayCleaner_StartCleanup();
}
/*
* Checks if RunawayCleaner_StartCleanup() ignores cleanup if in critical section
*/
static void
test__RunawayCleaner_StartCleanup__IgnoresCleanupInCriticalSection(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to less than *latestRunawayVersino
* to trigger a cleanup
*/
beginCleanupRunawayVersion = 1;
endCleanupRunawayVersion = 1;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
isProcessActive = true;
CritSectionCount = 1;
InterruptHoldoffCount = 0;
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
RunawayCleaner_StartCleanup();
assert_true(beginCleanupRunawayVersion == *latestRunawayVersion);
/* Cleanup is done, without ever throwing an ERROR */
assert_true(endCleanupRunawayVersion == beginCleanupRunawayVersion);
/*
* cleanupCountdown is decremented by 1 as there was no error, and therefore
* the cleanup is done within the same call of RunawayCleaner_StartCleanup
*/
assert_true(MySessionState->cleanupCountdown == 1);
CritSectionCount = 0;
}
/*
* Checks if RunawayCleaner_StartCleanup() ignores cleanup if interrupts are held off
*/
static void
test__RunawayCleaner_StartCleanup__IgnoresCleanupInHoldoffInterrupt(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to less than *latestRunawayVersino
* to trigger a cleanup
*/
beginCleanupRunawayVersion = 1;
endCleanupRunawayVersion = 1;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
isProcessActive = true;
CritSectionCount = 0;
InterruptHoldoffCount = 1;
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
RunawayCleaner_StartCleanup();
assert_true(beginCleanupRunawayVersion == *latestRunawayVersion);
/* Cleanup is done, without ever throwing an ERROR */
assert_true(endCleanupRunawayVersion == beginCleanupRunawayVersion);
/*
* cleanupCountdown is decremented by 1 as there was no error, and therefore
* the cleanup is done within the same call of RunawayCleaner_StartCleanup
*/
assert_true(MySessionState->cleanupCountdown == 1);
InterruptHoldoffCount = 0;
}
/*
* Checks if RunawayCleaner_StartCleanup() ignores cleanup if outside of any transaction
*/
static void
test__RunawayCleaner_StartCleanup__IgnoresCleanupOutsideAnyTransaction(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to less than *latestRunawayVersino
* to trigger a cleanup
*/
beginCleanupRunawayVersion = 1;
endCleanupRunawayVersion = 1;
isProcessActive = true;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
CritSectionCount = 0;
InterruptHoldoffCount = 0;
gp_command_count = 1;
/* But it is called outside of a transaction */
will_return(IsTransactionState, false);
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
/* Should not call superuser() as that can cause a PANIC */
RunawayCleaner_StartCleanup();
assert_true(beginCleanupRunawayVersion == *latestRunawayVersion);
/* Cleanup is done, without ever throwing an ERROR */
assert_true(endCleanupRunawayVersion == beginCleanupRunawayVersion);
/*
* cleanupCountdown is decremented by 1 as there was no error, and therefore
* the cleanup is done within the same call of RunawayCleaner_StartCleanup
*/
assert_true(MySessionState->cleanupCountdown == 1);
InterruptHoldoffCount = 0;
}
/*
* Checks if RunawayCleaner_RunawayCleanupDoneForProcess() ignores cleanupCountdown
* if optional cleanup
*/
static void
test__RunawayCleaner_RunawayCleanupDoneForProcess__IgnoresCleanupIfNotRequired(void **state)
{
#define CLEANUP_COUNTDOWN 2
InitFakeSessionState(2 /* activeProcessCount */,
CLEANUP_COUNTDOWN /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to less than *latestRunawayVersino
* to trigger a cleanup
*/
beginCleanupRunawayVersion = 1;
endCleanupRunawayVersion = 1;
/* Make sure the cleanup is not ignored for vmem initialization */
vmemTrackerInited = true;
/* Simulate a deactivation before the runaway */
deactivationVersion = *latestRunawayVersion - 1;
activationVersion = *latestRunawayVersion - 1;
isProcessActive = false;
CritSectionCount = 0;
InterruptHoldoffCount = 0;
RunawayCleaner_StartCleanup();
/* The cleanup shouldn't even start as the QE was deactivated at the time of the runaway*/
assert_true(beginCleanupRunawayVersion == 1);
assert_true(endCleanupRunawayVersion == 1);
/*
* cleanupCountdown should not be decremented as this was an optional cleanup
*/
assert_true(MySessionState->cleanupCountdown == CLEANUP_COUNTDOWN);
assert_true(MySessionState->runawayStatus == RunawayStatus_PrimaryRunawaySession);
/*
* Now simulate a scenario where the we activated but the runaway happened
* before the activation
*/
beginCleanupRunawayVersion = 2;
endCleanupRunawayVersion = 2;
/* Another runaway happened after the last cleanup */
*latestRunawayVersion = beginCleanupRunawayVersion + 2;
activationVersion = 5;
deactivationVersion = 3;
isProcessActive = true;
RunawayCleaner_StartCleanup();
/* The cleanup shouldn't even start as the runaway event happened before the QE became active */
assert_true(beginCleanupRunawayVersion == 2);
assert_true(endCleanupRunawayVersion == 2);
/*
* cleanupCountdown should not be decremented as this was an optional cleanup
*/
assert_true(MySessionState->cleanupCountdown == CLEANUP_COUNTDOWN);
assert_true(MySessionState->runawayStatus == RunawayStatus_PrimaryRunawaySession);
}
/*
* Checks if RunawayCleaner_RunawayCleanupDoneForProcess ignores duplicate cleanup
* for a single runaway event
*/
static void
test__RunawayCleaner_RunawayCleanupDoneForProcess__IgnoresDuplicateCalls(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion and endCleanupRunawayVersion to
* latestRunawayVersion which should make the function call a no-op
*/
beginCleanupRunawayVersion = *latestRunawayVersion;
endCleanupRunawayVersion = *latestRunawayVersion;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
isProcessActive = true;
RunawayCleaner_RunawayCleanupDoneForProcess(false /* ignoredCleanup */);
/* Nothing got cleaned */
assert_true(MySessionState->cleanupCountdown == 2);
}
/*
* Checks if RunawayCleaner_RunawayCleanupDoneForProcess prevents multiple cleanup
* for a single runaway event by properly updating beginCleanupRunawayVersion and
* endCleanupRunawayVersion
*/
static void
test__RunawayCleaner_RunawayCleanupDoneForProcess__PreventsDuplicateCleanup(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, CLEANUP_COUNTDOWN /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Some imaginary cleanup begin/end event version. The idea is to ensure
* that once the RunawayCleaner_RunawayCleanupDoneForProcess call returns
* we will have both set to latestRunawayVersion
*/
beginCleanupRunawayVersion = *latestRunawayVersion;
endCleanupRunawayVersion = 0;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
isProcessActive = true;
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
RunawayCleaner_RunawayCleanupDoneForProcess(false /* ignoredCleanup */);
/* cleanupCountdown should be adjusted */
assert_true(MySessionState->cleanupCountdown == CLEANUP_COUNTDOWN - 1);
assert_true(beginCleanupRunawayVersion == endCleanupRunawayVersion);
assert_true(beginCleanupRunawayVersion == *latestRunawayVersion);
/* Second call shouldn't change anything */
RunawayCleaner_RunawayCleanupDoneForProcess(false /* ignoredCleanup */);
/* cleanupCountdown is unchanged */
assert_true(MySessionState->cleanupCountdown == CLEANUP_COUNTDOWN - 1);
}
/*
* Checks if RunawayCleaner_RunawayCleanupDoneForProcess reactivates a process
* if the deactivation process triggers cleanup for a pending runaway event
*/
static void
test__RunawayCleaner_RunawayCleanupDoneForProcess__UndoDeactivation(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to latestRunawayVersion and endCleanupRunawayVersion
* to a smaller value to simulate an ongoing cleanup
*/
beginCleanupRunawayVersion = *latestRunawayVersion;
endCleanupRunawayVersion = 1;
/* Valid isRunawayDetector is necessary for Assert */
static uint32 fakeIsRunawayDetector = 1;
isRunawayDetector = &fakeIsRunawayDetector;
/* Make sure we became idle after a pending runaway event */
activationVersion = 1;
deactivationVersion = *latestRunawayVersion + 1;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
isProcessActive = false;
/* We must undo the idle state */
will_be_called(IdleTracker_ActivateProcess);
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
RunawayCleaner_RunawayCleanupDoneForProcess(false /* ignoredCleanup */);
/* The cleanupCountdown must be decremented as we cleaned up */
assert_true(MySessionState->cleanupCountdown == 1);
/* We updated the endCleanupRunawayVersion to indicate that we finished cleanup */
assert_true(endCleanupRunawayVersion == beginCleanupRunawayVersion);
}
/*
* Checks if RunawayCleaner_RunawayCleanupDoneForProcess reactivates the runaway detector
* once all the processes of the runaway session are done cleaning
*/
static void
test__RunawayCleaner_RunawayCleanupDoneForProcess__ReactivatesRunawayDetection(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Set beginCleanupRunawayVersion to latestRunawayVersion and endCleanupRunawayVersion
* to a smaller value to simulate an ongoing cleanup
*/
beginCleanupRunawayVersion = *latestRunawayVersion;
endCleanupRunawayVersion = 1;
/* Valid isRunawayDetector is necessary for Assert */
static uint32 fakeIsRunawayDetector = 1;
isRunawayDetector = &fakeIsRunawayDetector;
/* Just an active process that never became idle */
activationVersion = 0;
deactivationVersion = 0;
isProcessActive = true;
/* Make sure the cleanup goes through */
vmemTrackerInited = true;
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
RunawayCleaner_RunawayCleanupDoneForProcess(false /* ignoredCleanup */);
/* The cleanupCountdown must be decremented as we cleaned up */
assert_true(MySessionState->cleanupCountdown == 1);
/* We updated the endCleanupRunawayVersion to indicate that we finished cleanup */
assert_true(endCleanupRunawayVersion == beginCleanupRunawayVersion);
/* The runaway detector promotion should be disabled as we still have 1 QE unclean */
assert_true(*isRunawayDetector == 1);
/*
* Fake a ongoing cleanup by making endCleanupRunawayVersion < beginCleanupRunawayVersion
* so that we can execute cleanup one more time, marking all QEs clean
*/
endCleanupRunawayVersion = 1;
CHECK_FOR_RUNAWAY_CLEANUP_MEMORY_LOGGING();
/*
* cleanupCountdown should reach 0, and immediately afterwards should be set to
* CLEANUP_COUNTDOWN_BEFORE_RUNAWAY
*/
RunawayCleaner_RunawayCleanupDoneForProcess(false /* ignoredCleanup */);
assert_true(MySessionState->cleanupCountdown == CLEANUP_COUNTDOWN_BEFORE_RUNAWAY);
/* Runaway detector should be re-enabled */
assert_true(*isRunawayDetector == 0);
}
/*
* Checks if RunawayCleaner_RunawayCleanupDoneForSession reactivates the runaway detector
*/
static void
test__RunawayCleaner_RunawayCleanupDoneForSession__ResetsRunawayFlagAndReactivateRunawayDetector(void **state)
{
InitFakeSessionState(2 /* activeProcessCount */,
2 /* cleanupCountdown */,
RunawayStatus_PrimaryRunawaySession /* runawayStatus */, 2 /* pinCount */, 12345 /* vmem */);
EventVersion fakeLatestRunawayVersion = 10;
latestRunawayVersion = &fakeLatestRunawayVersion;
/*
* Satisfy asserts
*/
beginCleanupRunawayVersion = *latestRunawayVersion;
endCleanupRunawayVersion = beginCleanupRunawayVersion;
MySessionState->cleanupCountdown = CLEANUP_COUNTDOWN_BEFORE_RUNAWAY;
*isRunawayDetector = 1;
MySessionState->runawayStatus = RunawayStatus_PrimaryRunawaySession;
RunawayCleaner_RunawayCleanupDoneForSession();
assert_true(MySessionState->runawayStatus == RunawayStatus_NotRunaway);
/* Runaway detector should be re-enabled */
assert_true(*isRunawayDetector == 0);
}
int
main(int argc, char* argv[])
{
cmockery_parse_arguments(argc, argv);
const UnitTest tests[] = {
unit_test(test__RunawayCleaner_StartCleanup__IgnoresNonRunaway),
unit_test(test__RunawayCleaner_StartCleanup__IgnoresDuplicateCleanup),
unit_test(test__RunawayCleaner_StartCleanup__StartsPrimaryCleanupIfPossible),
unit_test(test__RunawayCleaner_StartCleanup__StartsSecondaryCleanupIfPossible),
unit_test(test__RunawayCleaner_StartCleanup__IgnoresCleanupInCriticalSection),
unit_test(test__RunawayCleaner_StartCleanup__IgnoresCleanupInHoldoffInterrupt),
unit_test(test__RunawayCleaner_StartCleanup__IgnoresCleanupOutsideAnyTransaction),
unit_test(test__RunawayCleaner_RunawayCleanupDoneForProcess__IgnoresCleanupIfNotRequired),
unit_test(test__RunawayCleaner_RunawayCleanupDoneForProcess__IgnoresDuplicateCalls),
unit_test(test__RunawayCleaner_RunawayCleanupDoneForProcess__PreventsDuplicateCleanup),
unit_test(test__RunawayCleaner_RunawayCleanupDoneForProcess__UndoDeactivation),
unit_test(test__RunawayCleaner_RunawayCleanupDoneForProcess__ReactivatesRunawayDetection),
unit_test(test__RunawayCleaner_RunawayCleanupDoneForSession__ResetsRunawayFlagAndReactivateRunawayDetector),
};
return run_tests(tests);
}
相关信息
相关文章
greenplumn event_version_test 源码
greenplumn idle_tracker_test 源码
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦