Selama sepuluh tahun terakhir, gerakan open-source telah menjadi salah satu pendorong utama pengembangan industri TI, dan komponen krusialnya. Peran proyek-proyek sumber terbuka menjadi semakin menonjol tidak hanya dalam hal kuantitas tetapi juga dalam hal kualitas, yang mengubah konsep bagaimana mereka diposisikan di pasar TI pada umumnya. Tim PVS-Studio kami yang berani tidak duduk diam dan mengambil peran aktif dalam memperkuat keberadaan perangkat lunak sumber terbuka dengan menemukan bug tersembunyi di kedalaman basis kode yang sangat besar dan menawarkan opsi lisensi gratis kepada penulis proyek semacam itu. Artikel ini hanyalah bagian lain dari aktivitas itu! Hari ini kita akan berbicara tentang Apache Hive. Saya sudah mendapat laporannya - dan ada beberapa hal yang layak untuk dilihat.
Tentang PVS-Studio
Penganalisa kode statis
PVS-Studio , yang telah ada selama lebih dari 10 tahun sekarang, adalah solusi perangkat lunak multi-fungsional dan mudah diintegrasikan. Saat ini, ia mendukung C, C ++, C #, dan Java dan berjalan pada Windows, Linux, dan macOS.
PVS-Studio adalah solusi B2B berbayar yang digunakan oleh banyak tim di sejumlah perusahaan. Jika Anda ingin mencoba penganalisa, kunjungi
halaman ini untuk mengunduh distribusi dan meminta kunci percobaan.
Jika Anda seorang geek sumber terbuka atau, misalnya, seorang siswa, Anda dapat memanfaatkan salah satu
opsi lisensi gratis kami.
Tentang Apache Hive
Jumlah data telah tumbuh pada tingkat yang sangat besar selama beberapa tahun terakhir. Basis data standar tidak dapat lagi menghadapi pertumbuhan yang cepat ini, yang merupakan asal istilah Big Data berasal bersama dengan gagasan terkait lainnya (seperti pemrosesan, penyimpanan, dan operasi lainnya pada data besar).
Apache Hadoop saat ini dianggap sebagai salah satu teknologi Big Data perintis. Tugas utamanya adalah menyimpan, memproses, dan mengelola sejumlah besar data. Komponen utama yang terdiri dari kerangka kerja adalah Hadoop Common,
HDFS ,
Hadoop MapReduce , dan
Hadoop YARN . Seiring waktu, ekosistem besar proyek dan teknologi terkait telah berkembang di sekitar Hadoop, banyak di antaranya awalnya dimulai sebagai bagian dari proyek dan kemudian beranjak untuk menjadi mandiri.
Apache Hive adalah salah satunya.
Apache Hive adalah gudang data terdistribusi. Ini mengelola data yang disimpan dalam HDFS dan menyediakan bahasa query berdasarkan SQL (HiveQL) untuk menangani data itu. Detail lebih lanjut tentang proyek ini dapat ditemukan di
sini .
Menjalankan analisis
Tidak butuh banyak usaha atau waktu untuk memulai analisis. Inilah algoritma saya:
- Unduh Apache Hive dari GitHub ;
- Baca panduan tentang memulai analisis Java dan meluncurkan analisis;
- Dapatkan laporan penganalisa, mempelajarinya, dan menulis kasus yang paling menarik.
Hasil analisis adalah sebagai berikut: 1456 peringatan tingkat Tinggi dan Sedang (masing-masing 602 dan 854) pada 6500+ file.
Tidak semua peringatan merujuk pada bug asli. Itu cukup normal; Anda harus mengubah pengaturan penganalisa sebelum mulai menggunakannya secara teratur. Setelah itu, Anda biasanya mengharapkan tingkat positif palsu yang cukup rendah (
contoh ).
Saya mengabaikan 407 peringatan (177 Tinggi dan 230 Tingkat Menengah) yang dipicu oleh file uji. Saya juga mengabaikan diagnostik
V6022 (karena Anda tidak dapat dengan andal membedakan fragmen yang salah dan benar ketika Anda tidak terbiasa dengan kode), yang dipicu sebanyak 482 kali. Saya juga tidak memeriksa 179 peringatan yang dihasilkan oleh diagnostik
V6021 .
Pada akhirnya, saya masih memiliki cukup peringatan untuk diikuti, dan karena saya tidak mengubah pengaturan, masih ada beberapa persentase positif palsu di antara mereka. Tidak ada gunanya memasukkan terlalu banyak peringatan dalam artikel seperti ini :). Jadi kita hanya akan berbicara tentang apa yang menarik perhatian saya dan terlihat cukup penasaran.
Kondisi yang ditentukan sebelumnya
Di antara diagnostik yang diperiksa untuk analisis ini,
V6007 memegang rekor untuk jumlah peringatan yang dikeluarkan. Sedikit lebih dari 200 pesan !!! Beberapa terlihat tidak berbahaya, yang lain mencurigakan, dan beberapa yang lain adalah bug asli! Mari kita lihat beberapa di antaranya.
V6007 Expression 'key.startsWith ("hplsql.")' Selalu benar. Exec.java (675)
void initOptions() { .... if (key == null || value == null || !key.startsWith("hplsql.")) {
Itu cukup panjang jika-jika-jika membangun! Penganalisis tidak menyukai kondisi di
if terakhir
(key.startsWith ("hplsql.")) Karena jika eksekusi mencapainya, itu berarti itu benar. Memang, jika Anda melihat baris pertama dari seluruh konstruksi if-else-if ini, Anda akan melihat bahwa itu sudah berisi cek yang berlawanan, jadi jika string tidak dimulai dengan
"hplsql." , eksekusi akan segera dilewati ke iterasi berikutnya.
Ekspresi
V6007 '
columnNameProperty.length () == 0' selalu salah. OrcRecordUpdater.java (238)
private static TypeDescription getTypeDescriptionFromTableProperties(....) { .... if (tableProperties != null) { final String columnNameProperty = ....; final String columnTypeProperty = ....; if ( !Strings.isNullOrEmpty(columnNameProperty) && !Strings.isNullOrEmpty(columnTypeProperty)) { List<String> columnNames = columnNameProperty.length() == 0 ? new ArrayList<String>() : ....; List<TypeInfo> columnTypes = columnTypeProperty.length() == 0 ? new ArrayList<TypeInfo>() : ....; .... } } } .... }
Perbandingan panjang string
columnNameProperty dengan nol akan selalu menghasilkan
false . Ini terjadi karena perbandingan ini mengikuti
cek! Strings.isNullOrEmpty (columnNameProperty) . Jadi jika eksekusi mencapai kondisi kita, itu berarti bahwa string
columnNameProperty pasti bukan nol atau kosong.
Hal yang sama berlaku untuk string
columnTypeProperty satu baris nanti:
- Ekspresi V6007 'columnTypeProperty.length () == 0' selalu salah. OrcRecordUpdater.java (239)
Ekspresi
V6007 'colOrScalar1.equals ("Column")' selalu salah. GenVectorCode.java (3469)
private void generateDateTimeArithmeticIntervalYearMonth(String[] tdesc) throws Exception { .... String colOrScalar1 = tdesc[4]; .... String colOrScalar2 = tdesc[6]; .... if (colOrScalar1.equals("Col") && colOrScalar1.equals("Column"))
Copy-paste tua yang bagus. Dari sudut pandang logika saat ini, string
colOrScalar1 mungkin memiliki dua nilai yang berbeda sekaligus, yang tidak mungkin. Jelas, cek harus memiliki variabel
colOrScalar1 di sebelah kiri dan
colOrScalar2 di sebelah kanan.
Peringatan serupa beberapa baris di bawah ini:
- Ekspresi V6007 'colOrScalar1.equals ("Scalar")' selalu salah. GenVectorCode.java (3475)
- Ekspresi V6007 'colOrScalar1.equals ("Column")' selalu salah. GenVectorCode.java (3486)
Akibatnya, konstruksi if-else-if ini tidak akan pernah melakukan apa pun.
Beberapa peringatan
V6007 lagi:
- Ekspresi V6007 'karakter == null' selalu salah. RandomTypeUtil.java (43)
- Ekspresi V6007 'writeIdHwm> 0' selalu salah. TxnHandler.java (1603)
- Ekspresi V6007 'fields.equals ("*")' selalu benar. Server.java (983)
- Ekspresi V6007 'currentGroups! = Null' selalu benar. GenericUDFCurrentGroups.java (90)
- Ekspresi V6007 'this.wh == null' selalu salah. Pengembalian baru bukan-null referensi. StorageBasedAuthorizationProvider.java (93), StorageBasedAuthorizationProvider.java (92)
- dan seterusnya ...
NPE
V6008 Potensi null dereference dari 'dagLock'. QueryTracker.java (557), QueryTracker.java (553)
private void handleFragmentCompleteExternalQuery(QueryInfo queryInfo) { if (queryInfo.isExternalQuery()) { ReadWriteLock dagLock = getDagLock(queryInfo.getQueryIdentifier()); if (dagLock == null) { LOG.warn("Ignoring fragment completion for unknown query: {}", queryInfo.getQueryIdentifier()); } boolean locked = dagLock.writeLock().tryLock(); ..... } }
Objek nol ditangkap, dicatat, dan ... program tetap berjalan. Akibatnya, pemeriksaan diikuti oleh dereference pointer nol. Aduh!
Pengembang harus benar-benar menginginkan program untuk keluar dari fungsi atau melemparkan beberapa pengecualian khusus dalam kasus mendapatkan referensi nol.
V6008 Null dereference 'buffer' dalam fungsi 'unlockSingleBuffer'. MetadataCache.java (410), MetadataCache.java (465)
private boolean lockBuffer(LlapBufferOrBuffers buffers, ....) { LlapAllocatorBuffer buffer = buffers.getSingleLlapBuffer(); if (buffer != null) {
NPE potensial lainnya. Jika eksekusi mencapai metode
unlockSingleBuffer , itu berarti objek
buffer adalah nol. Misalkan itulah yang terjadi! Jika Anda melihat metode
unlockSingleBuffer , Anda akan melihat bagaimana objek kami ditereferensi langsung di baris pertama. Gotcha!
Pergeseran menjadi liar
V6034 Shift dengan nilai 'bitShiftsInWord - 1' bisa tidak konsisten dengan ukuran tipe: 'bitShiftsInWord - 1' = [-1 ... 30]. UnsignedInt128.java (1791)
private void shiftRightDestructive(int wordShifts, int bitShiftsInWord, boolean roundUp) { if (wordShifts == 0 && bitShiftsInWord == 0) { return; } assert (wordShifts >= 0); assert (bitShiftsInWord >= 0); assert (bitShiftsInWord < 32); if (wordShifts >= 4) { zeroClear(); return; } final int shiftRestore = 32 - bitShiftsInWord;
Ini adalah potensi pergeseran sebesar -1. Jika metode ini dipanggil dengan, katakanlah,
wordShifts == 3 dan
bitShiftsInWord == 0 , baris yang dilaporkan akan berakhir dengan 1 << -1. Apakah itu perilaku yang direncanakan?
V6034 Shift dengan nilai 'j' bisa tidak konsisten dengan ukuran tipe: 'j' = [0 ... 63]. IoTrace.java (272)
public void logSargResult(int stripeIx, boolean[] rgsToRead) { .... for (int i = 0, valOffset = 0; i < elements; ++i, valOffset += 64) { long val = 0; for (int j = 0; j < 64; ++j) { int ix = valOffset + j; if (rgsToRead.length == ix) break; if (!rgsToRead[ix]) continue; val = val | (1 << j);
Pada baris yang dilaporkan, variabel
j dapat memiliki nilai dalam rentang [0 ... 63]. Karena itu, perhitungan nilai
val dalam loop dapat berjalan secara tak terduga. Dalam ekspresi
(1 << j) , nilai 1 adalah tipe
int , jadi menggesernya dengan 32 bit dan lebih banyak membawa kita melampaui batas kisaran tipe. Ini dapat diperbaiki dengan menulis
((panjang) 1 << j) .
Dibawa oleh logging
V6046 Format salah. Jumlah item format yang berbeda diharapkan. Argumen tidak digunakan: 1, 2. StatsSources.java (89)
private static ImmutableList<PersistedRuntimeStats> extractStatsFromPlanMapper (....) { .... if (stat.size() > 1 || sig.size() > 1) { StringBuffer sb = new StringBuffer(); sb.append(String.format( "expected(stat-sig) 1-1, got {}-{} ;",
Saat menulis kode untuk memformat string menggunakan
String.format () , pengembang menggunakan sintaks yang salah. Akibatnya, parameter yang diteruskan tidak pernah sampai ke string yang dihasilkan. Dugaan saya adalah bahwa pengembang telah mengerjakan penebangan sebelum menulis ini, di mana mereka meminjam sintaks.
Pengecualian yang dicuri
V6051 Penggunaan pernyataan 'kembali' di blok 'akhirnya' dapat menyebabkan hilangnya pengecualian yang tidak ditangani. ObjectStore.java (9080)
private List<MPartitionColumnStatistics> getMPartitionColumnStatistics(....) throws NoSuchObjectException, MetaException { boolean committed = false; try { .... committed = commitTransaction(); return result; } catch (Exception ex) { LOG.error("Error retrieving statistics via jdo", ex); if (ex instanceof MetaException) { throw (MetaException) ex; } throw new MetaException(ex.getMessage()); } finally { if (!committed) { rollbackTransaction(); return Lists.newArrayList(); } } }
Mengembalikan apa pun dari blok
terakhir adalah praktik yang sangat buruk, dan contoh ini dengan jelas menunjukkan alasannya.
Di blok
percobaan , program membentuk permintaan dan mengakses penyimpanan. Variabel yang
dikomit memiliki nilai
false secara default dan mengubah statusnya hanya setelah semua tindakan sebelumnya di blok
percobaan telah berhasil dieksekusi. Ini berarti bahwa jika pengecualian dikemukakan, variabel itu akan selalu
salah . Blok
penangkap akan menangkap pengecualian, sesuaikan sedikit, dan buang. Jadi ketika giliran blok
akhirnya , eksekusi akan memasuki kondisi dari mana daftar kosong akan dikembalikan. Berapa pengembalian ini kepada kita? Yah, itu biaya kita mencegah pengecualian tertangkap dari dibuang ke luar di mana itu bisa ditangani dengan benar. Tidak ada pengecualian yang ditentukan dalam tanda tangan metode yang akan dilemparkan; mereka hanya menyesatkan.
Pesan diagnostik serupa:
- V6051 Penggunaan pernyataan 'kembali' di blok 'akhirnya' dapat menyebabkan hilangnya pengecualian yang tidak ditangani. ObjectStore.java (808)
Lain-lain
Fungsi
V6009 'compareTo' menerima argumen aneh. Objek 'o2.getWorkerIdentity ()' digunakan sebagai argumen untuk metode sendiri. LlapFixedRegistryImpl.java (244)
@Override public List<LlapServiceInstance> getAllInstancesOrdered(....) { .... Collections.sort(list, new Comparator<LlapServiceInstance>() { @Override public int compare(LlapServiceInstance o1, LlapServiceInstance o2) { return o2.getWorkerIdentity().compareTo(o2.getWorkerIdentity());
Mungkin ada sejumlah penyebab yang menyebabkan kesalahan konyol: copy-paste, kecerobohan, terburu-buru, dan sebagainya. Kita sering melihat kesalahan seperti itu dalam proyek open-source dan bahkan memiliki seluruh
artikel tentang itu.
V6020 Bagilah dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (265)
public static long divideUnsignedLong(long dividend, long divisor) { if (divisor < 0L) { return (compareUnsignedLong(dividend, divisor)) < 0 ? 0L : 1L; } if (dividend >= 0) {
Yang ini cukup sepele. Serangkaian cek tidak berdaya untuk mencegah pembagian dengan nol.
Beberapa peringatan lagi:
- V6020 Mod dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (309)
- V6020 Bagilah dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (276)
- V6020 Bagilah dengan nol. Kisaran nilai penyebut 'pembagi' termasuk nol. SqlMathUtil.java (312)
V6030 Metode yang terletak di sebelah kanan '|' operator akan dipanggil terlepas dari nilai operan kiri. Mungkin, lebih baik menggunakan '||'. OperatorUtils.java (573)
public static Operator<? extends OperatorDesc> findSourceRS(....) { .... List<Operator<? extends OperatorDesc>> parents = ....; if (parents == null | parents.isEmpty()) {
Programmer menulis operator bitwise | bukannya logis ||. Itu berarti bagian kanan akan dieksekusi tidak peduli hasil dari yang kiri. Jika
orang tua == null , kesalahan ketik ini akan berakhir dengan NPE tepat di subekspresi logis berikutnya.
V6042 Ekspresi diperiksa untuk kompatibilitas dengan tipe 'A' tetapi dilemparkan ke tipe 'B'. VectorColumnAssignFactory.java (347)
public static VectorColumnAssign buildObjectAssign(VectorizedRowBatch outputBatch, int outColIndex, PrimitiveCategory category) throws HiveException { VectorColumnAssign outVCA = null; ColumnVector destCol = outputBatch.cols[outColIndex]; if (destCol == null) { .... } else if (destCol instanceof LongColumnVector) { switch(category) { .... case LONG: outVCA = new VectorLongColumnAssign() { .... } .init(.... , (LongColumnVector) destCol); break; case TIMESTAMP: outVCA = new VectorTimestampColumnAssign() { .... }.init(...., (TimestampColumnVector) destCol);
Kami tertarik pada kelas yang
LongColumnVector memperluas ColumnVector dan
TimestampColumnVector memperluas ColumnVector . Pemeriksaan bahwa objek
destCol adalah turunan dari
LongColumnVector secara eksplisit menunjukkan bahwa itu adalah objek kelas ini yang akan ditangani dalam tubuh pernyataan bersyarat. Meskipun demikian, bagaimanapun, itu masih dilemparkan ke
TimestampColumnVector ! Seperti yang Anda lihat, ini adalah kelas yang berbeda kecuali bahwa mereka berasal dari orangtua yang sama. Sebagai hasilnya, kami mendapatkan
ClassCastException .
Hal yang sama berlaku untuk casting ke
IntervalDayTimeColumnVector :
- V6042 Ekspresi diperiksa untuk kompatibilitas dengan tipe 'A' tetapi dilemparkan ke tipe 'B'. VectorColumnAssignFactory.java (390)
V6060 Referensi 'var' digunakan sebelum diverifikasi terhadap nol. Var.java (402), Var.java (395)
@Override public boolean equals(Object obj) { if (getClass() != obj.getClass()) {
Di sini Anda melihat pemeriksaan aneh objek
var untuk
null setelah dereference telah terjadi. Dalam konteks ini,
var dan
obj adalah objek yang sama (
var = (Var) obj ). Kehadiran cek
nol menyiratkan bahwa objek yang dikirimkan mungkin nol. Jadi, memanggil
equals (null) akan menghasilkan NPE, bukannya
false yang diharapkan, tepat di baris pertama. Ya, ceknya ada di sana, tapi, sayangnya, ada di tempat yang salah.
Beberapa kasus serupa lainnya, di mana objek digunakan sebelum pemeriksaan:
- V6060 Referensi 'nilai' digunakan sebelum diverifikasi terhadap nol. ParquetRecordReaderWrapper.java (168), ParquetRecordReaderWrapper.java (166)
- V6060 Referensi 'defaultConstraintCols' digunakan sebelum diverifikasi terhadap nol. HiveMetaStore.java (2539), HiveMetaStore.java (2530)
- V6060 Referensi 'projIndxLst' digunakan sebelum diverifikasi terhadap nol. RelOptHiveTable.java (683), RelOptHiveTable.java (682)
- V6060 Referensi 'oldp' digunakan sebelum diverifikasi terhadap nol. ObjectStore.java (4343), ObjectStore.java (4339)
- dan seterusnya ...
Kesimpulan
Jika Anda pernah tertarik pada Big Data jika hanya sedikit, maka Anda tidak akan menyadari betapa pentingnya Apache Hive. Ini adalah proyek yang populer, dan cukup besar, terdiri dari lebih dari 6500 file sumber (* .java). Banyak pengembang telah menulisnya selama bertahun-tahun, yang berarti ada banyak hal yang dapat ditemukan oleh penganalisa statis di sana. Ini membuktikan sekali lagi bahwa analisis statis sangat penting dan berguna ketika mengembangkan proyek-proyek menengah dan besar!
Catatan Pemeriksaan satu kali seperti yang saya lakukan di sini baik untuk menunjukkan kemampuan penganalisa tetapi skenario yang benar-benar tidak tepat untuk menggunakannya. Gagasan ini diuraikan di
sini dan di
sini . Analisis statis akan digunakan secara teratur!
Pemeriksaan Hive ini mengungkapkan beberapa cacat dan fragmen yang mencurigakan. Jika penulis Apache Hive menemukan artikel ini, kami akan dengan senang hati membantu kerja keras meningkatkan proyek.
Anda tidak dapat membayangkan Apache Hive tanpa Apache Hadoop, jadi Unicorn dari PVS-Studio juga dapat berkunjung ke sana. Tapi itu saja untuk hari ini. Sementara itu, saya mengundang Anda untuk
mengunduh penganalisa dan memeriksa proyek Anda sendiri.