* Portions Copyright (c) 1996-2006, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
- * $PostgreSQL: pgsql/src/backend/access/transam/xlog.c,v 1.245 2006/07/30 02:07:18 alvherre Exp $
+ * $PostgreSQL: pgsql/src/backend/access/transam/xlog.c,v 1.246 2006/08/06 03:53:44 tgl Exp $
*
*-------------------------------------------------------------------------
*/
* CheckpointLock: must be held to do a checkpoint (ensures only one
* checkpointer at a time; even though the postmaster won't launch
* parallel checkpoint processes, we need this because manual checkpoints
- * could be launched simultaneously).
+ * could be launched simultaneously). XXX now that all checkpoints are
+ * done by the bgwriter, isn't this lock redundant?
*
*----------
*/
static bool XLogCheckBuffer(XLogRecData *rdata, bool doPageWrites,
XLogRecPtr *lsn, BkpBlock *bkpb);
-static bool AdvanceXLInsertBuffer(void);
-static void XLogWrite(XLogwrtRqst WriteRqst, bool flexible);
+static bool AdvanceXLInsertBuffer(bool new_segment);
+static void XLogWrite(XLogwrtRqst WriteRqst, bool flexible, bool xlog_switch);
static int XLogFileInit(uint32 log, uint32 seg,
bool *use_existent, bool use_lock);
static bool InstallXLogFileSegment(uint32 *log, uint32 *seg, char *tmppath,
XLogwrtRqst LogwrtRqst;
bool updrqst;
bool doPageWrites;
- bool no_tran = (rmid == RM_XLOG_ID) ? true : false;
+ bool isLogSwitch = (rmid == RM_XLOG_ID && info == XLOG_SWITCH);
+ bool no_tran = (rmid == RM_XLOG_ID);
if (info & XLR_INFO_MASK)
{
}
/*
- * NOTE: the test for len == 0 here is somewhat fishy, since in theory all
- * of the rmgr data might have been suppressed in favor of backup blocks.
- * Currently, all callers of XLogInsert provide at least some
- * not-in-a-buffer data and so len == 0 should never happen, but that may
- * not be true forever. If you need to remove the len == 0 check, also
- * remove the check for xl_len == 0 in ReadRecord, below.
+ * NOTE: We disallow len == 0 because it provides a useful bit of extra
+ * error checking in ReadRecord. This means that all callers of
+ * XLogInsert must supply at least some not-in-a-buffer data. However,
+ * we make an exception for XLOG SWITCH records because we don't want
+ * them to ever cross a segment boundary.
*/
- if (len == 0)
+ if (len == 0 && !isLogSwitch)
elog(PANIC, "invalid xlog record length %u", len);
START_CRIT_SECTION();
*/
LogwrtResult = XLogCtl->Write.LogwrtResult;
if (XLByteLT(LogwrtResult.Write, LogwrtRqst.Write))
- XLogWrite(LogwrtRqst, true);
+ XLogWrite(LogwrtRqst, true, false);
LWLockRelease(WALWriteLock);
}
}
freespace = INSERT_FREESPACE(Insert);
if (freespace < SizeOfXLogRecord)
{
- updrqst = AdvanceXLInsertBuffer();
+ updrqst = AdvanceXLInsertBuffer(false);
freespace = INSERT_FREESPACE(Insert);
}
+ /* Compute record's XLOG location */
curridx = Insert->curridx;
- record = (XLogRecord *) Insert->currpos;
+ INSERT_RECPTR(RecPtr, Insert, curridx);
+
+ /*
+ * If the record is an XLOG_SWITCH, and we are exactly at the start
+ * of a segment, we need not insert it (and don't want to because
+ * we'd like consecutive switch requests to be no-ops). Instead,
+ * make sure everything is written and flushed through the end of
+ * the prior segment, and return the prior segment's end address.
+ */
+ if (isLogSwitch &&
+ (RecPtr.xrecoff % XLogSegSize) == SizeOfXLogLongPHD)
+ {
+ /* We can release insert lock immediately */
+ LWLockRelease(WALInsertLock);
+
+ RecPtr.xrecoff -= SizeOfXLogLongPHD;
+ if (RecPtr.xrecoff == 0)
+ {
+ /* crossing a logid boundary */
+ RecPtr.xlogid -= 1;
+ RecPtr.xrecoff = XLogFileSize;
+ }
+
+ LWLockAcquire(WALWriteLock, LW_EXCLUSIVE);
+ LogwrtResult = XLogCtl->Write.LogwrtResult;
+ if (!XLByteLE(RecPtr, LogwrtResult.Flush))
+ {
+ XLogwrtRqst FlushRqst;
+
+ FlushRqst.Write = RecPtr;
+ FlushRqst.Flush = RecPtr;
+ XLogWrite(FlushRqst, false, false);
+ }
+ LWLockRelease(WALWriteLock);
+
+ END_CRIT_SECTION();
+
+ return RecPtr;
+ }
/* Insert record header */
+ record = (XLogRecord *) Insert->currpos;
record->xl_prev = Insert->PrevRecord;
record->xl_xid = GetCurrentTransactionIdIfAny();
record->xl_tot_len = SizeOfXLogRecord + write_len;
FIN_CRC32(rdata_crc);
record->xl_crc = rdata_crc;
- /* Compute record's XLOG location */
- INSERT_RECPTR(RecPtr, Insert, curridx);
-
#ifdef WAL_DEBUG
if (XLOG_DEBUG)
{
StringInfoData buf;
initStringInfo(&buf);
- appendStringInfo(&buf, "INSERT @ %X/%X: ",
- RecPtr.xlogid, RecPtr.xrecoff);
+ appendStringInfo(&buf, "INSERT @ %X/%X: ",
+ RecPtr.xlogid, RecPtr.xrecoff);
xlog_outrec(&buf, record);
if (rdata->data != NULL)
{
}
/* Use next buffer */
- updrqst = AdvanceXLInsertBuffer();
+ updrqst = AdvanceXLInsertBuffer(false);
curridx = Insert->curridx;
/* Insert cont-record header */
Insert->currpage->xlp_info |= XLP_FIRST_IS_CONTRECORD;
*/
INSERT_RECPTR(RecPtr, Insert, curridx);
- /* Need to update shared LogwrtRqst if some block was filled up */
- if (freespace < SizeOfXLogRecord)
- updrqst = true; /* curridx is filled and available for writing
- * out */
+ /*
+ * If the record is an XLOG_SWITCH, we must now write and flush all the
+ * existing data, and then forcibly advance to the start of the next
+ * segment. It's not good to do this I/O while holding the insert lock,
+ * but there seems too much risk of confusion if we try to release the
+ * lock sooner. Fortunately xlog switch needn't be a high-performance
+ * operation anyway...
+ */
+ if (isLogSwitch)
+ {
+ XLogCtlWrite *Write = &XLogCtl->Write;
+ XLogwrtRqst FlushRqst;
+ XLogRecPtr OldSegEnd;
+
+ LWLockAcquire(WALWriteLock, LW_EXCLUSIVE);
+
+ /*
+ * Flush through the end of the page containing XLOG_SWITCH,
+ * and perform end-of-segment actions (eg, notifying archiver).
+ */
+ WriteRqst = XLogCtl->xlblocks[curridx];
+ FlushRqst.Write = WriteRqst;
+ FlushRqst.Flush = WriteRqst;
+ XLogWrite(FlushRqst, false, true);
+
+ /* Set up the next buffer as first page of next segment */
+ /* Note: AdvanceXLInsertBuffer cannot need to do I/O here */
+ (void) AdvanceXLInsertBuffer(true);
+
+ /* There should be no unwritten data */
+ curridx = Insert->curridx;
+ Assert(curridx == Write->curridx);
+
+ /* Compute end address of old segment */
+ OldSegEnd = XLogCtl->xlblocks[curridx];
+ OldSegEnd.xrecoff -= XLOG_BLCKSZ;
+ if (OldSegEnd.xrecoff == 0)
+ {
+ /* crossing a logid boundary */
+ OldSegEnd.xlogid -= 1;
+ OldSegEnd.xrecoff = XLogFileSize;
+ }
+
+ /* Make it look like we've written and synced all of old segment */
+ LogwrtResult.Write = OldSegEnd;
+ LogwrtResult.Flush = OldSegEnd;
+
+ /*
+ * Update shared-memory status --- this code should match XLogWrite
+ */
+ {
+ /* use volatile pointer to prevent code rearrangement */
+ volatile XLogCtlData *xlogctl = XLogCtl;
+
+ SpinLockAcquire(&xlogctl->info_lck);
+ xlogctl->LogwrtResult = LogwrtResult;
+ if (XLByteLT(xlogctl->LogwrtRqst.Write, LogwrtResult.Write))
+ xlogctl->LogwrtRqst.Write = LogwrtResult.Write;
+ if (XLByteLT(xlogctl->LogwrtRqst.Flush, LogwrtResult.Flush))
+ xlogctl->LogwrtRqst.Flush = LogwrtResult.Flush;
+ SpinLockRelease(&xlogctl->info_lck);
+ }
+
+ Write->LogwrtResult = LogwrtResult;
+
+ LWLockRelease(WALWriteLock);
+
+ updrqst = false; /* done already */
+ }
else
- curridx = PrevBufIdx(curridx);
- WriteRqst = XLogCtl->xlblocks[curridx];
+ {
+ /* normal case, ie not xlog switch */
+
+ /* Need to update shared LogwrtRqst if some block was filled up */
+ if (freespace < SizeOfXLogRecord)
+ {
+ /* curridx is filled and available for writing out */
+ updrqst = true;
+ }
+ else
+ {
+ /* if updrqst already set, write through end of previous buf */
+ curridx = PrevBufIdx(curridx);
+ }
+ WriteRqst = XLogCtl->xlblocks[curridx];
+ }
LWLockRelease(WALInsertLock);
* Advance the Insert state to the next buffer page, writing out the next
* buffer if it still contains unwritten data.
*
+ * If new_segment is TRUE then we set up the next buffer page as the first
+ * page of the next xlog segment file, possibly but not usually the next
+ * consecutive file page.
+ *
* The global LogwrtRqst.Write pointer needs to be advanced to include the
* just-filled page. If we can do this for free (without an extra lock),
* we do so here. Otherwise the caller must do it. We return TRUE if the
* Must be called with WALInsertLock held.
*/
static bool
-AdvanceXLInsertBuffer(void)
+AdvanceXLInsertBuffer(bool new_segment)
{
XLogCtlInsert *Insert = &XLogCtl->Insert;
XLogCtlWrite *Write = &XLogCtl->Write;
WriteRqst.Write = OldPageRqstPtr;
WriteRqst.Flush.xlogid = 0;
WriteRqst.Flush.xrecoff = 0;
- XLogWrite(WriteRqst, false);
+ XLogWrite(WriteRqst, false, false);
LWLockRelease(WALWriteLock);
Insert->LogwrtResult = LogwrtResult;
}
* output page.
*/
NewPageEndPtr = XLogCtl->xlblocks[Insert->curridx];
+
+ if (new_segment)
+ {
+ /* force it to a segment start point */
+ NewPageEndPtr.xrecoff += XLogSegSize - 1;
+ NewPageEndPtr.xrecoff -= NewPageEndPtr.xrecoff % XLogSegSize;
+ }
+
if (NewPageEndPtr.xrecoff >= XLogFileSize)
{
/* crossing a logid boundary */
* This option allows us to avoid uselessly issuing multiple writes when a
* single one would do.
*
+ * If xlog_switch == TRUE, we are intending an xlog segment switch, so
+ * perform end-of-segment actions after writing the last page, even if
+ * it's not physically the end of its segment. (NB: this will work properly
+ * only if caller specifies WriteRqst == page-end and flexible == false,
+ * and there is some data to write.)
+ *
* Must be called with WALWriteLock held.
*/
static void
-XLogWrite(XLogwrtRqst WriteRqst, bool flexible)
+XLogWrite(XLogwrtRqst WriteRqst, bool flexible, bool xlog_switch)
{
XLogCtlWrite *Write = &XLogCtl->Write;
bool ispartialpage;
+ bool last_iteration;
bool finishing_seg;
bool use_existent;
int curridx;
* contiguous in memory), or if we are at the end of the logfile
* segment.
*/
+ last_iteration = !XLByteLT(LogwrtResult.Write, WriteRqst.Write);
+
finishing_seg = !ispartialpage &&
(startoffset + npages * XLOG_BLCKSZ) >= XLogSegSize;
- if (!XLByteLT(LogwrtResult.Write, WriteRqst.Write) ||
+ if (last_iteration ||
curridx == XLogCtl->XLogCacheBlck ||
finishing_seg)
{
* later. Doing it here ensures that one and only one backend will
* perform this fsync.
*
+ * We also do this if this is the last page written for an xlog
+ * switch.
+ *
* This is also the right place to notify the Archiver that the
* segment is ready to copy to archival storage.
*/
- if (finishing_seg)
+ if (finishing_seg || (xlog_switch && last_iteration))
{
issue_xlog_fsync();
LogwrtResult.Flush = LogwrtResult.Write; /* end of page */
WriteRqst.Write = WriteRqstPtr;
WriteRqst.Flush = record;
}
- XLogWrite(WriteRqst, false);
+ XLogWrite(WriteRqst, false, false);
}
LWLockRelease(WALWriteLock);
}
got_record:;
/*
- * Currently, xl_len == 0 must be bad data, but that might not be true
- * forever. See note in XLogInsert.
+ * xl_len == 0 is bad data for everything except XLOG SWITCH, where
+ * it is required.
*/
- if (record->xl_len == 0)
+ if (record->xl_rmid == RM_XLOG_ID && record->xl_info == XLOG_SWITCH)
+ {
+ if (record->xl_len != 0)
+ {
+ ereport(emode,
+ (errmsg("invalid xlog switch record at %X/%X",
+ RecPtr->xlogid, RecPtr->xrecoff)));
+ goto next_record_is_invalid;
+ }
+ }
+ else if (record->xl_len == 0)
{
ereport(emode,
(errmsg("record with zero length at %X/%X",
pageHeaderSize +
MAXALIGN(SizeOfXLogContRecord + contrecord->xl_rem_len);
ReadRecPtr = *RecPtr;
+ /* needn't worry about XLOG SWITCH, it can't cross page boundaries */
return record;
}
EndRecPtr.xrecoff = RecPtr->xrecoff + MAXALIGN(total_len);
ReadRecPtr = *RecPtr;
memcpy(buffer, record, total_len);
+ /*
+ * Special processing if it's an XLOG SWITCH record
+ */
+ if (record->xl_rmid == RM_XLOG_ID && record->xl_info == XLOG_SWITCH)
+ {
+ /* Pretend it extends to end of segment */
+ EndRecPtr.xrecoff += XLogSegSize - 1;
+ EndRecPtr.xrecoff -= EndRecPtr.xrecoff % XLogSegSize;
+ nextRecord = NULL; /* definitely not on same page */
+ /*
+ * Pretend that readBuf contains the last page of the segment.
+ * This is just to avoid Assert failure in StartupXLOG if XLOG
+ * ends with this segment.
+ */
+ readOff = XLogSegSize - XLOG_BLCKSZ;
+ }
return (XLogRecord *) buffer;
next_record_is_invalid:;
freespace = INSERT_FREESPACE(Insert);
if (freespace < SizeOfXLogRecord)
{
- (void) AdvanceXLInsertBuffer();
+ (void) AdvanceXLInsertBuffer(false);
/* OK to ignore update return flag, since we will do flush anyway */
freespace = INSERT_FREESPACE(Insert);
}
}
/*
+ * Write an XLOG SWITCH record.
+ *
+ * Here we just blindly issue an XLogInsert request for the record.
+ * All the magic happens inside XLogInsert.
+ *
+ * The return value is either the end+1 address of the switch record,
+ * or the end+1 address of the prior segment if we did not need to
+ * write a switch record because we are already at segment start.
+ */
+static XLogRecPtr
+RequestXLogSwitch(void)
+{
+ XLogRecPtr RecPtr;
+ XLogRecData rdata;
+
+ /* XLOG SWITCH, alone among xlog record types, has no data */
+ rdata.buffer = InvalidBuffer;
+ rdata.data = NULL;
+ rdata.len = 0;
+ rdata.next = NULL;
+
+ RecPtr = XLogInsert(RM_XLOG_ID, XLOG_SWITCH, &rdata);
+
+ return RecPtr;
+}
+
+/*
* XLOG resource manager's routines
*/
void
(errmsg("unexpected timeline ID %u (should be %u) in checkpoint record",
checkPoint.ThisTimeLineID, ThisTimeLineID)));
}
+ else if (info == XLOG_SWITCH)
+ {
+ /* nothing to do here */
+ }
}
void
memcpy(&nextOid, rec, sizeof(Oid));
appendStringInfo(buf, "nextOid: %u", nextOid);
}
+ else if (info == XLOG_SWITCH)
+ {
+ appendStringInfo(buf, "xlog switch");
+ }
else
appendStringInfo(buf, "UNKNOWN");
}
* where it will be archived as part of the backup dump. The label file
* contains the user-supplied label string (typically this would be used
* to tell where the backup dump will be stored) and the starting time and
- * starting WAL offset for the dump.
+ * starting WAL location for the dump.
*/
Datum
pg_start_backup(PG_FUNCTION_ARGS)
PG_END_TRY();
/*
- * We're done. As a convenience, return the starting WAL offset.
+ * We're done. As a convenience, return the starting WAL location.
*/
snprintf(xlogfilename, sizeof(xlogfilename), "%X/%X",
startpoint.xlogid, startpoint.xrecoff);
* We remove the backup label file created by pg_start_backup, and instead
* create a backup history file in pg_xlog (whence it will immediately be
* archived). The backup history file contains the same info found in
- * the label file, plus the backup-end time and WAL offset.
+ * the label file, plus the backup-end time and WAL location.
*/
Datum
pg_stop_backup(PG_FUNCTION_ARGS)
{
text *result;
- XLogCtlInsert *Insert = &XLogCtl->Insert;
XLogRecPtr startpoint;
XLogRecPtr stoppoint;
time_t stamp_time;
(errmsg("must be superuser to run a backup"))));
/*
- * Get the current end-of-WAL position; it will be unsafe to use this dump
- * to restore to a point in advance of this time. We can also clear
- * forcePageWrites here.
+ * OK to clear forcePageWrites
*/
LWLockAcquire(WALInsertLock, LW_EXCLUSIVE);
- INSERT_RECPTR(stoppoint, Insert, Insert->curridx);
XLogCtl->Insert.forcePageWrites = false;
LWLockRelease(WALInsertLock);
+ /*
+ * Force a switch to a new xlog segment file, so that the backup
+ * is valid as soon as archiver moves out the current segment file.
+ * We'll report the end address of the XLOG SWITCH record as the backup
+ * stopping point.
+ */
+ stoppoint = RequestXLogSwitch();
+
XLByteToSeg(stoppoint, _logId, _logSeg);
XLogFileName(stopxlogfilename, ThisTimeLineID, _logId, _logSeg);
CleanupBackupHistory();
/*
- * We're done. As a convenience, return the ending WAL offset.
+ * We're done. As a convenience, return the ending WAL location.
*/
snprintf(stopxlogfilename, sizeof(stopxlogfilename), "%X/%X",
stoppoint.xlogid, stoppoint.xrecoff);
}
/*
+ * pg_switch_xlog: switch to next xlog file
+ */
+Datum
+pg_switch_xlog(PG_FUNCTION_ARGS)
+{
+ text *result;
+ XLogRecPtr switchpoint;
+ char location[MAXFNAMELEN];
+
+ if (!superuser())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ (errmsg("must be superuser to switch xlog files"))));
+
+ switchpoint = RequestXLogSwitch();
+
+ /*
+ * As a convenience, return the WAL location of the switch record
+ */
+ snprintf(location, sizeof(location), "%X/%X",
+ switchpoint.xlogid, switchpoint.xrecoff);
+ result = DatumGetTextP(DirectFunctionCall1(textin,
+ CStringGetDatum(location)));
+ PG_RETURN_TEXT_P(result);
+}
+
+/*
+ * Report the current WAL location (same format as pg_start_backup etc)
+ */
+Datum
+pg_current_xlog_location(PG_FUNCTION_ARGS)
+{
+ text *result;
+ XLogCtlInsert *Insert = &XLogCtl->Insert;
+ XLogRecPtr current_recptr;
+ char location[MAXFNAMELEN];
+
+ /*
+ * Get the current end-of-WAL position ... shared lock is sufficient
+ */
+ LWLockAcquire(WALInsertLock, LW_SHARED);
+ INSERT_RECPTR(current_recptr, Insert, Insert->curridx);
+ LWLockRelease(WALInsertLock);
+
+ snprintf(location, sizeof(location), "%X/%X",
+ current_recptr.xlogid, current_recptr.xrecoff);
+
+ result = DatumGetTextP(DirectFunctionCall1(textin,
+ CStringGetDatum(location)));
+ PG_RETURN_TEXT_P(result);
+}
+
+/*
+ * Compute an xlog file name and decimal byte offset given a WAL location,
+ * such as is returned by pg_stop_backup() or pg_xlog_switch().
+ *
+ * Note that a location exactly at a segment boundary is taken to be in
+ * the previous segment. This is usually the right thing, since the
+ * expected usage is to determine which xlog file(s) are ready to archive.
+ */
+Datum
+pg_xlogfile_name_offset(PG_FUNCTION_ARGS)
+{
+ text *location = PG_GETARG_TEXT_P(0);
+ text *result;
+ char *locationstr;
+ unsigned int uxlogid;
+ unsigned int uxrecoff;
+ uint32 xlogid;
+ uint32 xlogseg;
+ uint32 xrecoff;
+ XLogRecPtr locationpoint;
+ char xlogfilename[MAXFNAMELEN];
+
+ locationstr = DatumGetCString(DirectFunctionCall1(textout,
+ PointerGetDatum(location)));
+
+ if (sscanf(locationstr, "%X/%X", &uxlogid, &uxrecoff) != 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("could not parse xlog location \"%s\"",
+ locationstr)));
+
+ locationpoint.xlogid = uxlogid;
+ locationpoint.xrecoff = uxrecoff;
+
+ XLByteToPrevSeg(locationpoint, xlogid, xlogseg);
+ XLogFileName(xlogfilename, ThisTimeLineID, xlogid, xlogseg);
+
+ xrecoff = locationpoint.xrecoff - xlogseg * XLogSegSize;
+ snprintf(xlogfilename + strlen(xlogfilename),
+ sizeof(xlogfilename) - strlen(xlogfilename),
+ " %u",
+ (unsigned int) xrecoff);
+
+ result = DatumGetTextP(DirectFunctionCall1(textin,
+ CStringGetDatum(xlogfilename)));
+ PG_RETURN_TEXT_P(result);
+}
+
+/*
+ * Compute an xlog file name given a WAL location,
+ * such as is returned by pg_stop_backup() or pg_xlog_switch().
+ */
+Datum
+pg_xlogfile_name(PG_FUNCTION_ARGS)
+{
+ text *location = PG_GETARG_TEXT_P(0);
+ text *result;
+ char *locationstr;
+ unsigned int uxlogid;
+ unsigned int uxrecoff;
+ uint32 xlogid;
+ uint32 xlogseg;
+ XLogRecPtr locationpoint;
+ char xlogfilename[MAXFNAMELEN];
+
+ locationstr = DatumGetCString(DirectFunctionCall1(textout,
+ PointerGetDatum(location)));
+
+ if (sscanf(locationstr, "%X/%X", &uxlogid, &uxrecoff) != 2)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("could not parse xlog location \"%s\"",
+ locationstr)));
+
+ locationpoint.xlogid = uxlogid;
+ locationpoint.xrecoff = uxrecoff;
+
+ XLByteToPrevSeg(locationpoint, xlogid, xlogseg);
+ XLogFileName(xlogfilename, ThisTimeLineID, xlogid, xlogseg);
+
+ result = DatumGetTextP(DirectFunctionCall1(textin,
+ CStringGetDatum(xlogfilename)));
+ PG_RETURN_TEXT_P(result);
+}
+
+/*
* read_backup_label: check to see if a backup_label file is present
*
* If we see a backup_label during recovery, we assume that we are recovering
StringInfoData buf;
initStringInfo(&buf);
- RmgrTable[record->xl_rmid].rm_desc(&buf,
- record->xl_info,
+ RmgrTable[record->xl_rmid].rm_desc(&buf,
+ record->xl_info,
XLogRecGetData(record));
/* don't bother emitting empty description */