提交 99d6c3ee authored 作者: Thomas Mueller's avatar Thomas Mueller

Formatting

上级 1f55186b
......@@ -259,7 +259,7 @@ abstract class ScriptBase extends Prepared implements DataHandler {
public JavaObjectSerializer getJavaObjectSerializer() {
return session.getDataHandler().getJavaObjectSerializer();
}
@Override
public CompareMode getCompareMode() {
return session.getDataHandler().getCompareMode();
......
......@@ -36,7 +36,9 @@ public class Engine implements SessionFactory {
private Engine() {
// use getInstance()
ThreadDeadlockDetector.init();
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
ThreadDeadlockDetector.init();
}
}
public static Engine getInstance() {
......
......@@ -90,7 +90,7 @@ public class SessionRemote extends SessionWithState implements DataHandler {
private JavaObjectSerializer javaObjectSerializer;
private volatile boolean javaObjectSerializerInitialized;
private CompareMode compareMode = CompareMode.getInstance(null, 0);
public SessionRemote(ConnectionInfo ci) {
......@@ -848,7 +848,7 @@ public class SessionRemote extends SessionWithState implements DataHandler {
public void addTemporaryLob(Value v) {
// do nothing
}
@Override
public CompareMode getCompareMode() {
return compareMode;
......
......@@ -453,6 +453,14 @@ public class SysProperties {
public static final boolean TRACE_IO =
Utils.getProperty("h2.traceIO", false);
/**
* System property <code>h2.threadDeadlockDetector</code>
* (default: false).<br />
* Detect thread deadlocks in a background thread.
*/
public static final boolean THREAD_DEADLOCK_DETECTOR =
Utils.getProperty("h2.threadDeadlockDetector", false);
/**
* System property <code>h2.implicitRelativePath</code>
* (default: true for version 1.3, false for version 1.4).<br />
......
......@@ -2058,7 +2058,7 @@ public class Function extends Expression implements FunctionCall {
return hc;
}
@Override
public int getType() {
return dataType;
......
......@@ -1937,7 +1937,7 @@ public class JdbcConnection extends TraceObject implements Connection {
}
CompareMode getCompareMode() {
return session.getDataHandler().getCompareMode();
return session.getDataHandler().getCompareMode();
}
/**
......
......@@ -2314,7 +2314,7 @@ public class MVStore {
releaseWriteBuffer(buff);
// only really needed if we remove many chunks, when writes are
// re-ordered - but we do it always, because rollback is not
// performance criticial
// performance critical
sync();
}
lastChunk = keep;
......
......@@ -49,9 +49,32 @@ import org.h2.value.Value;
*/
public class MVTable extends TableBase {
public static final DebuggingThreadLocal<String> WAITING_FOR_LOCK = new DebuggingThreadLocal<String>();
public static final DebuggingThreadLocal<ArrayList<String>> EXCLUSIVE_LOCKS = new DebuggingThreadLocal<ArrayList<String>>();
public static final DebuggingThreadLocal<ArrayList<String>> SHARED_LOCKS = new DebuggingThreadLocal<ArrayList<String>>();
/**
* The table name this thread is waiting to lock.
*/
public static final DebuggingThreadLocal<String> WAITING_FOR_LOCK;
/**
* The table names this thread has exclusively locked.
*/
public static final DebuggingThreadLocal<ArrayList<String>> EXCLUSIVE_LOCKS;
/**
* The tables names this thread has a shared lock on.
*/
public static final DebuggingThreadLocal<ArrayList<String>> SHARED_LOCKS;
static {
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
WAITING_FOR_LOCK = new DebuggingThreadLocal<String>();
EXCLUSIVE_LOCKS = new DebuggingThreadLocal<ArrayList<String>>();
SHARED_LOCKS = new DebuggingThreadLocal<ArrayList<String>>();
} else {
WAITING_FOR_LOCK = null;
EXCLUSIVE_LOCKS = null;
SHARED_LOCKS = null;
}
}
private MVPrimaryIndex primaryIndex;
private final ArrayList<Index> indexes = New.arrayList();
......@@ -136,13 +159,17 @@ public class MVTable extends TableBase {
return true;
}
session.setWaitForLock(this, Thread.currentThread());
WAITING_FOR_LOCK.set(getName());
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
WAITING_FOR_LOCK.set(getName());
}
waitingSessions.addLast(session);
try {
doLock1(session, lockMode, exclusive);
} finally {
session.setWaitForLock(null, null);
WAITING_FOR_LOCK.remove();
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
WAITING_FOR_LOCK.remove();
}
waitingSessions.remove(session);
}
}
......@@ -226,19 +253,23 @@ public class MVTable extends TableBase {
traceLock(session, exclusive, "added for");
session.addLock(this);
lockExclusiveSession = session;
if (EXCLUSIVE_LOCKS.get() == null) {
EXCLUSIVE_LOCKS.set(new ArrayList<String>());
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (EXCLUSIVE_LOCKS.get() == null) {
EXCLUSIVE_LOCKS.set(new ArrayList<String>());
}
EXCLUSIVE_LOCKS.get().add(getName());
}
EXCLUSIVE_LOCKS.get().add(getName());
return true;
} else if (lockSharedSessions.size() == 1 &&
lockSharedSessions.containsKey(session)) {
traceLock(session, exclusive, "add (upgraded) for ");
lockExclusiveSession = session;
if (EXCLUSIVE_LOCKS.get() == null) {
EXCLUSIVE_LOCKS.set(new ArrayList<String>());
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (EXCLUSIVE_LOCKS.get() == null) {
EXCLUSIVE_LOCKS.set(new ArrayList<String>());
}
EXCLUSIVE_LOCKS.get().add(getName());
}
EXCLUSIVE_LOCKS.get().add(getName());
return true;
}
}
......@@ -260,10 +291,12 @@ public class MVTable extends TableBase {
traceLock(session, exclusive, "ok");
session.addLock(this);
lockSharedSessions.put(session, session);
if (SHARED_LOCKS.get() == null) {
SHARED_LOCKS.set(new ArrayList<String>());
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (SHARED_LOCKS.get() == null) {
SHARED_LOCKS.set(new ArrayList<String>());
}
SHARED_LOCKS.get().add(getName());
}
SHARED_LOCKS.get().add(getName());
}
return true;
}
......@@ -377,15 +410,19 @@ public class MVTable extends TableBase {
traceLock(s, lockExclusiveSession == s, "unlock");
if (lockExclusiveSession == s) {
lockExclusiveSession = null;
if (EXCLUSIVE_LOCKS.get() != null) {
EXCLUSIVE_LOCKS.get().remove(getName());
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (EXCLUSIVE_LOCKS.get() != null) {
EXCLUSIVE_LOCKS.get().remove(getName());
}
}
}
synchronized (getLockSyncObject()) {
if (lockSharedSessions.size() > 0) {
lockSharedSessions.remove(s);
if (SHARED_LOCKS.get() != null) {
SHARED_LOCKS.get().remove(getName());
if (SysProperties.THREAD_DEADLOCK_DETECTOR) {
if (SHARED_LOCKS.get() != null) {
SHARED_LOCKS.get().remove(getName());
}
}
}
if (!waitingSessions.isEmpty()) {
......@@ -849,6 +886,12 @@ public class MVTable extends TableBase {
}
}
/**
* Convert the illegal state exception to a database exception.
*
* @param e the illegal state exception
* @return the database exception
*/
DbException convertException(IllegalStateException e) {
if (DataUtils.getErrorCode(e.getMessage()) ==
DataUtils.ERROR_TRANSACTION_LOCKED) {
......
......@@ -141,6 +141,13 @@ public class MVTableEngine implements TableEngine {
private String fileName;
/**
* Open the store for this database.
*
* @param db the database
* @param builder the builder
* @param encrypted whether the store is encrypted
*/
void open(Database db, MVStore.Builder builder, boolean encrypted) {
this.encrypted = encrypted;
try {
......@@ -161,6 +168,13 @@ public class MVTableEngine implements TableEngine {
}
}
/**
* Convert the illegal state exception to the correct database
* exception.
*
* @param e the illegal state exception
* @return the database exception
*/
DbException convertIllegalStateException(IllegalStateException e) {
int errorCode = DataUtils.getErrorCode(e.getMessage());
if (errorCode == DataUtils.ERROR_FILE_CORRUPT) {
......
......@@ -33,8 +33,10 @@ public class Sequence extends SchemaObjectBase {
private long maxValue;
private boolean cycle;
private boolean belongsToTable;
/**
* The last valueWithMargin we flushed. We do a little dance with this to avoid an ABBA deadlock.
* The last valueWithMargin we flushed. We do a little dance with this to
* avoid an ABBA deadlock.
*/
private long lastFlushValueWithMargin;
......@@ -260,7 +262,7 @@ public class Sequence extends SchemaObjectBase {
if (cycle) {
value = increment > 0 ? minValue : maxValue;
valueWithMargin = value + (increment * cacheSize);
flushValueWithMargin = valueWithMargin;
flushValueWithMargin = valueWithMargin;
needsFlush = true;
} else {
throw DbException.get(ErrorCode.SEQUENCE_EXHAUSTED, getName());
......@@ -289,6 +291,7 @@ public class Sequence extends SchemaObjectBase {
* Flush the current value, including the margin, to disk.
*
* @param session the session
* @param flushValueWithMargin whether to reserve more entries
*/
public void flush(Session session, long flushValueWithMargin) {
if (session == null || !database.isSysTableLockedBy(session)) {
......
......@@ -114,10 +114,10 @@ public interface DataHandler {
* column of type OTHER
*/
JavaObjectSerializer getJavaObjectSerializer();
/**
* Return compare mode.
*
*
* @return Compare mode.
*/
CompareMode getCompareMode();
......
......@@ -202,6 +202,12 @@ public class FilePathMem extends FilePath {
return name.equals(getScheme() + ":");
}
/**
* Get the canonical path for this file name.
*
* @param fileName the file name
* @return the canonical path
*/
protected static String getCanonicalPath(String fileName) {
fileName = fileName.replace('\\', '/');
int idx = fileName.indexOf(':') + 1;
......
......@@ -266,7 +266,8 @@ public class Column {
* @return the new or converted value
*/
public Value validateConvertUpdateSequence(Session session, Value value) {
// take a local copy of defaultExpression to avoid holding the lock while calling getValue
// take a local copy of defaultExpression to avoid holding the lock
// while calling getValue
final Expression localDefaultExpression;
synchronized (this) {
localDefaultExpression = defaultExpression;
......
......@@ -1730,7 +1730,7 @@ public class Recover extends Tool implements DataHandler {
public JavaObjectSerializer getJavaObjectSerializer() {
return null;
}
@Override
public CompareMode getCompareMode() {
return CompareMode.getInstance(null, 0);
......
......@@ -437,6 +437,7 @@ public class Server extends Tool implements Runnable, ShutdownHandler {
* If no port is specified, the default port is used if possible,
* and if this port is already used, a random port is used.
* Use getPort() or getURL() after starting to retrieve the port.
* </p>
*
* @param args the argument list
* @return the server
......@@ -463,6 +464,7 @@ public class Server extends Tool implements Runnable, ShutdownHandler {
* If no port is specified, the default port is used if possible,
* and if this port is already used, a random port is used.
* Use getPort() or getURL() after starting to retrieve the port.
* </p>
*
* @param args the argument list
* @return the server
......
......@@ -9,7 +9,7 @@ import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* Similar to ThreadLocal, except that it allows it's data to be read from other
* Similar to ThreadLocal, except that it allows its data to be read from other
* threads - useful for debugging info.
*
* @param <T> the type
......@@ -22,6 +22,9 @@ public class DebuggingThreadLocal<T> {
map.put(Thread.currentThread().getId(), value);
}
/**
* Remove the value for the current thread.
*/
public void remove() {
map.remove(Thread.currentThread().getId());
}
......@@ -31,14 +34,12 @@ public class DebuggingThreadLocal<T> {
}
/**
* Get a snapshot of the data of all threads.
*
* @return a HashMap containing a mapping from thread-id to value
*/
public HashMap<Long, T> getSnapshotOfAllThreads() {
return new HashMap<Long, T>(map);
}
public T deepCopy(T value) {
return value;
}
}
......@@ -20,8 +20,8 @@ import java.util.TimerTask;
import org.h2.mvstore.db.MVTable;
/**
* Detects deadlocks between threads. Prints out data in the same format as the CTRL-BREAK handler,
* but includes information about table locks.
* Detects deadlocks between threads. Prints out data in the same format as the
* CTRL-BREAK handler, but includes information about table locks.
*/
public class ThreadDeadlockDetector {
......@@ -29,13 +29,13 @@ public class ThreadDeadlockDetector {
private static ThreadDeadlockDetector detector;
private final ThreadMXBean tmbean;
private final ThreadMXBean threadBean;
// a daemon thread
private final Timer threadCheck = new Timer("ThreadDeadlockDetector", true);
private ThreadDeadlockDetector() {
this.tmbean = ManagementFactory.getThreadMXBean();
this.threadBean = ManagementFactory.getThreadMXBean();
// delay: 10 ms
// period: 10000 ms (100 seconds)
threadCheck.schedule(new TimerTask() {
......@@ -46,6 +46,9 @@ public class ThreadDeadlockDetector {
}, 10, 10000);
}
/**
* Initialize the detector.
*/
public static synchronized void init() {
if (detector == null) {
detector = new ThreadDeadlockDetector();
......@@ -53,11 +56,12 @@ public class ThreadDeadlockDetector {
}
/**
* Checks if any threads are deadlocked. If any, print the thread dump information.
* Checks if any threads are deadlocked. If any, print the thread dump
* information.
*/
void checkForDeadlocks() {
long[] tids = tmbean.findDeadlockedThreads();
if (tids == null) {
long[] ids = threadBean.findDeadlockedThreads();
if (ids == null) {
return;
}
......@@ -65,24 +69,24 @@ public class ThreadDeadlockDetector {
final PrintWriter print = new PrintWriter(stringWriter);
print.println("ThreadDeadlockDetector - deadlock found :");
final ThreadInfo[] infos = tmbean.getThreadInfo(tids, true, true);
final HashMap<Long, String> mvtableWaitingForLockMap =
final ThreadInfo[] infos = threadBean.getThreadInfo(ids, true, true);
final HashMap<Long, String> tableWaitingForLockMap =
MVTable.WAITING_FOR_LOCK.getSnapshotOfAllThreads();
final HashMap<Long, ArrayList<String>> mvtableExclusiveLocksMap =
final HashMap<Long, ArrayList<String>> tableExclusiveLocksMap =
MVTable.EXCLUSIVE_LOCKS.getSnapshotOfAllThreads();
final HashMap<Long, ArrayList<String>> mvtableSharedLocksMap =
final HashMap<Long, ArrayList<String>> tableSharedLocksMap =
MVTable.SHARED_LOCKS.getSnapshotOfAllThreads();
for (ThreadInfo ti : infos) {
printThreadInfo(print, ti);
printLockInfo(print, ti.getLockedSynchronizers(),
mvtableWaitingForLockMap.get(ti.getThreadId()),
mvtableExclusiveLocksMap.get(ti.getThreadId()),
mvtableSharedLocksMap.get(ti.getThreadId()));
tableWaitingForLockMap.get(ti.getThreadId()),
tableExclusiveLocksMap.get(ti.getThreadId()),
tableSharedLocksMap.get(ti.getThreadId()));
}
print.flush();
// Dump it to system.out in one block, so it doesn't get mixed up with other stuff when we're
// using a logging subsystem.
// Dump it to system.out in one block, so it doesn't get mixed up with
// other stuff when we're using a logging subsystem.
System.out.println(stringWriter.getBuffer());
}
......@@ -91,11 +95,11 @@ public class ThreadDeadlockDetector {
printThread(print, ti);
// print stack trace with locks
StackTraceElement[] stacktrace = ti.getStackTrace();
StackTraceElement[] stackTrace = ti.getStackTrace();
MonitorInfo[] monitors = ti.getLockedMonitors();
for (int i = 0; i < stacktrace.length; i++) {
StackTraceElement ste = stacktrace[i];
print.println(INDENT + "at " + ste.toString());
for (int i = 0; i < stackTrace.length; i++) {
StackTraceElement e = stackTrace[i];
print.println(INDENT + "at " + e.toString());
for (MonitorInfo mi : monitors) {
if (mi.getLockedStackDepth() == i) {
print.println(INDENT + " - locked " + mi);
......@@ -125,25 +129,25 @@ public class ThreadDeadlockDetector {
}
private static void printLockInfo(PrintWriter print, LockInfo[] locks,
String mvtableWaitingForLock,
ArrayList<String> mvtableExclusiveLocks,
ArrayList<String> mvtableSharedLocksMap) {
String tableWaitingForLock,
ArrayList<String> tableExclusiveLocks,
ArrayList<String> tableSharedLocksMap) {
print.println(INDENT + "Locked synchronizers: count = " + locks.length);
for (LockInfo li : locks) {
print.println(INDENT + " - " + li);
}
if (mvtableWaitingForLock != null) {
print.println(INDENT + "Waiting for table: " + mvtableWaitingForLock);
if (tableWaitingForLock != null) {
print.println(INDENT + "Waiting for table: " + tableWaitingForLock);
}
if (mvtableExclusiveLocks != null) {
print.println(INDENT + "Exclusive table locks: count = " + mvtableExclusiveLocks.size());
for (String name : mvtableExclusiveLocks) {
if (tableExclusiveLocks != null) {
print.println(INDENT + "Exclusive table locks: count = " + tableExclusiveLocks.size());
for (String name : tableExclusiveLocks) {
print.println(INDENT + " - " + name);
}
}
if (mvtableSharedLocksMap != null) {
print.println(INDENT + "Shared table locks: count = " + mvtableSharedLocksMap.size());
for (String name : mvtableSharedLocksMap) {
if (tableSharedLocksMap != null) {
print.println(INDENT + "Shared table locks: count = " + tableSharedLocksMap.size());
for (String name : tableSharedLocksMap) {
print.println(INDENT + " - " + name);
}
}
......
......@@ -1005,7 +1005,7 @@ public abstract class TestBase {
/**
* Check that executing the specified query results in the specified error.
*
* @param expectedErrorMessage the expected error message
* @param expectedErrorCode the expected error code
* @param stat the statement
* @param sql the SQL statement to execute
*/
......
......@@ -130,8 +130,10 @@ public class TestCompatibilityOracle extends TestBase {
Connection conn = getConnection("oracle;MODE=Oracle");
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE TEST(ID INT PRIMARY KEY, U##NAME VARCHAR(255))");
stat.execute("INSERT INTO TEST VALUES(1, 'Hello'), (2, 'HelloWorld'), (3, 'HelloWorldWorld')");
stat.execute(
"CREATE TABLE TEST(ID INT PRIMARY KEY, U##NAME VARCHAR(255))");
stat.execute(
"INSERT INTO TEST VALUES(1, 'Hello'), (2, 'HelloWorld'), (3, 'HelloWorldWorld')");
assertResult("1", stat, "SELECT ID FROM TEST where U##NAME ='Hello'");
......
......@@ -1227,7 +1227,8 @@ public class TestFunctions extends TestBase implements AggregateFunction {
Connection conn = getConnection("functions");
Statement stat = conn.createStatement();
String testStr = "foo";
assertResult(String.valueOf("foo".hashCode()), stat, String.format("SELECT ORA_HASH('%s') FROM DUAL", testStr));
assertResult(String.valueOf("foo".hashCode()), stat,
String.format("SELECT ORA_HASH('%s') FROM DUAL", testStr));
assertResult(String.valueOf("foo".hashCode()), stat,
String.format("SELECT ORA_HASH('%s', 0) FROM DUAL", testStr));
assertResult(String.valueOf("foo".hashCode()), stat,
......@@ -1327,8 +1328,6 @@ public class TestFunctions extends TestBase implements AggregateFunction {
Timestamp.valueOf("1979-11-12 08:12:34.560"));
assertResult(expected, stat,
"SELECT TO_CHAR(X, 'DL') FROM T");
// assertResult("Monday, November 12, 1979", stat,
// "SELECT TO_CHAR(X, 'DL', 'NLS_DATE_LANGUAGE = English') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'DS') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'Ds') FROM T");
assertResult("11/12/1979", stat, "SELECT TO_CHAR(X, 'dS') FROM T");
......
......@@ -59,7 +59,7 @@ public class TestReorderWrites extends TestBase {
store.commit();
store.getFileStore().sync();
int stop = 4 + r.nextInt(20);
log("synched start");
log("countdown start");
fs.setPowerOffCountdown(stop, i);
try {
for (int j = 1; j < 100; j++) {
......
......@@ -171,7 +171,7 @@ public class TestValueHashMap extends TestBase implements DataHandler {
public JavaObjectSerializer getJavaObjectSerializer() {
return null;
}
@Override
public CompareMode getCompareMode() {
return compareMode;
......
......@@ -288,7 +288,7 @@ public class TestValueMemory extends TestBase implements DataHandler {
public JavaObjectSerializer getJavaObjectSerializer() {
return null;
}
@Override
public CompareMode getCompareMode() {
return CompareMode.getInstance(null, 0);
......
......@@ -25,6 +25,9 @@ import org.h2.util.IOUtils;
*/
public class FilePathReorderWrites extends FilePathWrapper {
/**
* Whether trace output of all method calls is enabled.
*/
static final boolean TRACE = false;
private static final FilePathReorderWrites INSTANCE = new FilePathReorderWrites();
......@@ -48,8 +51,8 @@ public class FilePathReorderWrites extends FilePathWrapper {
}
/**
* Set the number of write operations before a simulated power failure, and the
* random seed (for partial writes).
* Set the number of write operations before a simulated power failure, and
* the random seed (for partial writes).
*
* @param count the number of write operations (0 to never fail,
* Integer.MAX_VALUE to count the operations)
......@@ -150,7 +153,8 @@ class FileReorderWrites extends FileBase {
private final FileChannel base;
/**
* The base channel that is used for reading, where all operations are immediately applied to get a consistent view before a power failure.
* The base channel that is used for reading, where all operations are
* immediately applied to get a consistent view before a power failure.
*/
private final FileChannel readBase;
......@@ -216,12 +220,12 @@ class FileReorderWrites extends FileBase {
trace("op " + op);
checkError();
notAppliedList.add(op);
long now = op.time;
long now = op.getTime();
for (int i = 0; i < notAppliedList.size() - 1; i++) {
FileOperation old = notAppliedList.get(i);
boolean applyOld = false;
// String reason = "";
if (old.time + 45000 < now) {
if (old.getTime() + 45000 < now) {
// reason = "old";
applyOld = true;
} else if (old.overlaps(op)) {
......@@ -238,7 +242,7 @@ class FileReorderWrites extends FileBase {
i--;
}
}
return op.apply(readBase);
return op.apply(readBase);
}
private void applyAll() throws IOException {
......@@ -295,10 +299,10 @@ class FileReorderWrites extends FileBase {
* be applied on power failure).
*/
static class FileOperation {
final int id;
final long time;
final ByteBuffer buffer;
final long position;
private final int id;
private final long time;
private final ByteBuffer buffer;
private final long position;
FileOperation(int id, long position, ByteBuffer src) {
this.id = id;
......@@ -314,7 +318,18 @@ class FileReorderWrites extends FileBase {
this.position = position;
}
public boolean overlaps(FileOperation other) {
public long getTime() {
return time;
}
/**
* Check whether the file region of this operation overlaps with
* another operation.
*
* @param other the other operation
* @return if there is an overlap
*/
boolean overlaps(FileOperation other) {
if (isTruncate() && other.isTruncate()) {
// we just keep the latest truncate operation
return true;
......@@ -340,6 +355,12 @@ class FileReorderWrites extends FileBase {
return buffer == null ? 0 : buffer.limit() - buffer.position();
}
/**
* Apply the operation to the channel.
*
* @param channel the channel
* @return the return value of the operation
*/
int apply(FileChannel channel) throws IOException {
if (isTruncate()) {
channel.truncate(position);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论